dsynth - Add skip count for ignored and other failure conditions
[dragonfly.git] / usr.bin / dsynth / html.c
1 /*
2  * Copyright (c) 2019-2020 The DragonFly Project.  All rights reserved.
3  *
4  * This code is derived from software contributed to The DragonFly Project
5  * by Matthew Dillon <dillon@backplane.com>
6  *
7  * This code uses concepts and configuration based on 'synth', by
8  * John R. Marino <draco@marino.st>, which was written in ada.
9  *
10  * Redistribution and use in source and binary forms, with or without
11  * modification, are permitted provided that the following conditions
12  * are met:
13  *
14  * 1. Redistributions of source code must retain the above copyright
15  *    notice, this list of conditions and the following disclaimer.
16  * 2. Redistributions in binary form must reproduce the above copyright
17  *    notice, this list of conditions and the following disclaimer in
18  *    the documentation and/or other materials provided with the
19  *    distribution.
20  * 3. Neither the name of The DragonFly Project nor the names of its
21  *    contributors may be used to endorse or promote products derived
22  *    from this software without specific, prior written permission.
23  *
24  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
25  * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
26  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
27  * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
28  * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
29  * INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING,
30  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
31  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
32  * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
33  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
34  * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
35  * SUCH DAMAGE.
36  */
37 #include "dsynth.h"
38
39 #define SNPRINTF(buf, ctl, ...)         \
40         snprintf((buf), sizeof(buf), ctl, ## __VA_ARGS__)
41
42 static char *ReportPath;
43 static int HistNum;
44 static int EntryNum;
45 static char KickOff_Buf[64];
46
47 const char *CopyFilesAry[] = {
48         "favicon.png",
49         "progress.html",
50         "progress.css",
51         "progress.js",
52         "dsynth.png",
53         NULL
54 };
55
56 char **HtmlSlots;
57 time_t HtmlStart;
58 time_t HtmlLast;
59
60 /*
61  * Get rid of stuff that might blow up the json output.
62  */
63 static const char *
64 dequote(const char *reason)
65 {
66         static char *buf;
67         int i;
68
69         for (i = 0; reason[i]; ++i) {
70                 if (reason[i] == '\"' || reason[i] == '\n' ||
71                     reason[i] == '\\') {
72                         if (reason != buf) {
73                                 if (buf)
74                                         free(buf);
75                                 buf = strdup(reason);
76                                 reason = buf;
77                         }
78                         buf[i] = ' ';
79                 }
80         }
81         return reason;
82 }
83
84 static void
85 HtmlInit(void)
86 {
87         struct dirent *den;
88         DIR *dir;
89         struct stat st;
90         struct tm tmm;
91         size_t len;
92         char *src;
93         char *dst;
94         time_t t;
95         int i;
96
97         HtmlSlots = calloc(sizeof(char *), MaxWorkers);
98         HtmlLast = 0;
99         HtmlStart = time(NULL);
100
101         asprintf(&ReportPath, "%s/Report", LogsPath);
102         if (stat(ReportPath, &st) < 0 && mkdir(ReportPath, 0755) < 0)
103                 dfatal("Unable to create %s", ReportPath);
104         for (i = 0; CopyFilesAry[i]; ++i) {
105                 asprintf(&src, "%s/%s", SCRIPTPATH(SCRIPTDIR), CopyFilesAry[i]);
106                 if (strcmp(CopyFilesAry[i], "progress.html") == 0) {
107                         asprintf(&dst, "%s/index.html", ReportPath);
108                 } else {
109                         asprintf(&dst, "%s/%s", ReportPath, CopyFilesAry[i]);
110                 }
111                 copyfile(src, dst);
112                 free(src);
113                 free(dst);
114         }
115
116         asprintf(&src, "%s/summary.json", ReportPath);
117         remove(src);
118         free(src);
119
120         t = time(NULL);
121         gmtime_r(&t, &tmm);
122         strftime(KickOff_Buf, sizeof(KickOff_Buf),
123                  " %d-%b-%Y %H:%M:%S %Z", &tmm);
124
125         dir = opendir(ReportPath);
126         if (dir == NULL)
127                 dfatal("Unable to scan %s", ReportPath);
128         while ((den = readdir(dir)) != NULL) {
129                 len = strlen(den->d_name);
130                 if (len > 13 &&
131                     strcmp(den->d_name + len - 13, "_history.json") == 0) {
132                         asprintf(&src, "%s/%s", ReportPath, den->d_name);
133                         remove(src);
134                         free(src);
135                 }
136         }
137         closedir(dir);
138
139         /*
140          * First history file
141          */
142         HistNum = 0;
143         EntryNum = 1;
144 }
145
146 static void
147 HtmlDone(void)
148 {
149         int i;
150
151         for (i = 0; i < MaxWorkers; ++i) {
152                 if (HtmlSlots[i])
153                         free(HtmlSlots[i]);
154         }
155         free(HtmlSlots);
156         HtmlSlots = NULL;
157 }
158
159 static void
160 HtmlReset(void)
161 {
162 }
163
164 static void
165 HtmlUpdate(worker_t *work, const char *portdir)
166 {
167         const char *phase;
168         const char *origin;
169         time_t t;
170         int i = work->index;
171         int h;
172         int m;
173         int s;
174         int clear;
175         char elapsed_buf[32];
176         char lines_buf[32];
177
178         phase = "Unknown";
179         origin = "";
180         clear = 0;
181
182         switch(work->state) {
183         case WORKER_NONE:
184                 phase = "None";
185                 /* fall through */
186         case WORKER_IDLE:
187                 if (work->state == WORKER_IDLE)
188                         phase = "Idle";
189                 clear = 1;
190                 break;
191         case WORKER_FAILED:
192                 if (work->state == WORKER_FAILED)
193                         phase = "Failed";
194                 /* fall through */
195         case WORKER_EXITING:
196                 if (work->state == WORKER_EXITING)
197                         phase = "Exiting";
198                 return;
199                 /* NOT REACHED */
200         case WORKER_PENDING:
201                 phase = "Pending";
202                 break;
203         case WORKER_RUNNING:
204                 phase = "Running";
205                 break;
206         case WORKER_DONE:
207                 phase = "Done";
208                 break;
209         case WORKER_FROZEN:
210                 phase = "FROZEN";
211                 break;
212         default:
213                 break;
214         }
215
216         if (clear) {
217                 SNPRINTF(elapsed_buf, "%s", " --:--:--");
218                 SNPRINTF(lines_buf, "%s", "");
219                 origin = "";
220         } else {
221                 t = time(NULL) - work->start_time;
222                 s = t % 60;
223                 m = t / 60 % 60;
224                 h = t / 60 / 60;
225                 if (h > 99)
226                         SNPRINTF(elapsed_buf, "%3d:%02d:%02d", h, m, s);
227                 else
228                         SNPRINTF(elapsed_buf, " %02d:%02d:%02d", h, m, s);
229
230                 if (work->state == WORKER_RUNNING)
231                         phase = getphasestr(work->phase);
232
233                 /*
234                  * When called from the monitor frontend portdir has to be
235                  * passed in directly because work->pkg is not mapped.
236                  */
237                 if (portdir)
238                         origin = portdir;
239                 else if (work->pkg)
240                         origin = work->pkg->portdir;
241                 else
242                         origin = "";
243
244                 SNPRINTF(lines_buf, "%ld", work->lines);
245         }
246
247         /*
248          * Update the summary information
249          */
250         if (HtmlSlots[i])
251                 free(HtmlSlots[i]);
252         asprintf(&HtmlSlots[i],
253                  "  {\n"
254                  "     \"ID\":\"%02d\"\n"
255                  "     ,\"elapsed\":\"%s\"\n"
256                  "     ,\"phase\":\"%s\"\n"
257                  "     ,\"origin\":\"%s\"\n"
258                  "     ,\"lines\":\"%s\"\n"
259                  "  }\n",
260                  i,
261                  elapsed_buf,
262                  phase,
263                  origin,
264                  lines_buf
265         );
266 }
267
268 static void
269 HtmlUpdateTop(topinfo_t *info)
270 {
271         char *path;
272         char *dst;
273         FILE *fp;
274         int i;
275         char elapsed_buf[32];
276         char swap_buf[32];
277         char load_buf[32];
278
279         /*
280          * Be sure to do the first update and final update, but otherwise
281          * only update every 10 seconds or so.
282          */
283         if (HtmlLast && (int)(time(NULL) - HtmlLast) < 10 && info->active)
284                 return;
285         HtmlLast = time(NULL);
286
287         if (info->h > 99) {
288                 SNPRINTF(elapsed_buf, "%3d:%02d:%02d",
289                          info->h, info->m, info->s);
290         } else {
291                 SNPRINTF(elapsed_buf, " %02d:%02d:%02d",
292                          info->h, info->m, info->s);
293         }
294
295         if (info->noswap)
296                 SNPRINTF(swap_buf, "-    ");
297         else
298                 SNPRINTF(swap_buf, "%5.1f", info->dswap);
299
300         if (info->dload[0] > 999.9)
301                 SNPRINTF(load_buf, "%5.0f", info->dload[0]);
302         else
303                 SNPRINTF(load_buf, "%5.1f", info->dload[0]);
304
305         asprintf(&path, "%s/summary.json.new", ReportPath);
306         asprintf(&dst, "%s/summary.json", ReportPath);
307         fp = fopen(path, "we");
308         if (!fp)
309                 ddassert(0);
310         if (fp) {
311                 fprintf(fp,
312                         "{\n"
313                         "  \"profile\":\"%s\"\n"
314                         "  ,\"kickoff\":\"%s\"\n"
315                         "  ,\"kfiles\":%d\n"
316                         "  ,\"active\":%d\n"
317                         "  ,\"stats\":{\n"
318                         "    \"queued\":%d\n"
319                         "    ,\"built\":%d\n"
320                         "    ,\"failed\":%d\n"
321                         "    ,\"ignored\":%d\n"
322                         "    ,\"skipped\":%d\n"
323                         "    ,\"remains\":%d\n"
324                         "    ,\"elapsed\":\"%s\"\n"
325                         "    ,\"pkghour\":%d\n"
326                         "    ,\"impulse\":%d\n"
327                         "    ,\"swapinfo\":\"%s\"\n"
328                         "    ,\"load\":\"%s\"\n"
329                         "  }\n",
330                         Profile,
331                         KickOff_Buf,
332                         HistNum,                /* kfiles */
333                         info->active,           /* active */
334
335                         info->total,            /* queued */
336                         info->successful,       /* built */
337                         info->failed,           /* failed */
338                         info->ignored,          /* ignored */
339                         info->skipped,          /* skipped */
340                         info->remaining,        /* remaining */
341                         elapsed_buf,            /* elapsed */
342                         info->pkgrate,          /* pkghour */
343                         info->pkgimpulse,       /* impulse */
344                         swap_buf,               /* swapinfo */
345                         load_buf                /* load */
346                 );
347                 fprintf(fp,
348                         "  ,\"builders\":[\n"
349                 );
350                 for (i = 0; i < MaxWorkers; ++i) {
351                         if (HtmlSlots[i]) {
352                                 if (i)
353                                         fprintf(fp, ",");
354                                 fwrite(HtmlSlots[i], 1,
355                                        strlen(HtmlSlots[i]), fp);
356                         } else {
357                                 fprintf(fp,
358                                         "   %s{\n"
359                                         "     \"ID\":\"%02d\"\n"
360                                         "     ,\"elapsed\":\"Shutdown\"\n"
361                                         "     ,\"phase\":\"\"\n"
362                                         "     ,\"origin\":\"\"\n"
363                                         "     ,\"lines\":\"\"\n"
364                                         "    }\n",
365                                         (i ? "," : ""),
366                                         i
367                                 );
368                         }
369                 }
370                 fprintf(fp,
371                         "  ]\n"
372                         "}\n");
373                 fflush(fp);
374                 fclose(fp);
375         }
376         rename(path, dst);
377         free(path);
378         free(dst);
379 }
380
381 static void
382 HtmlUpdateLogs(void)
383 {
384 }
385
386 static void
387 HtmlUpdateCompletion(worker_t *work, int dlogid, pkg_t *pkg,
388                      const char *reason, const char *skipbuf)
389 {
390         FILE *fp;
391         char *path;
392         char elapsed_buf[64];
393         struct stat st;
394         time_t t;
395         int s, m, h;
396         int slot;
397         const char *result;
398         char *mreason;
399
400         if (skipbuf[0] == 0)
401                 skipbuf = "0";
402         else if (skipbuf[0] == ' ')
403                 ++skipbuf;
404
405         mreason = NULL;
406         if (work) {
407                 t = time(NULL) - work->start_time;
408                 s = t % 60;
409                 m = t / 60 % 60;
410                 h = t / 60 / 60;
411                 SNPRINTF(elapsed_buf, "%02d:%02d:%02d", h, m, s);
412                 slot = work->index;
413         } else {
414                 slot = -1;
415                 elapsed_buf[0] = 0;
416         }
417
418         switch(dlogid) {
419         case DLOG_SUCC:
420                 result = "built";
421                 break;
422         case DLOG_FAIL:
423                 result = "failed";
424                 if (work) {
425                         asprintf(&mreason, "%s:%s",
426                                  getphasestr(work->phase),
427                                  reason);
428                 } else {
429                         asprintf(&mreason, "unknown:%s", reason);
430                 }
431                 reason = mreason;
432                 break;
433         case DLOG_IGN:
434                 result = "ignored";
435                 asprintf(&mreason, "%s:|:%s", reason, skipbuf);
436                 reason = mreason;
437                 break;
438         case DLOG_SKIP:
439                 result = "skipped";
440                 break;
441         default:
442                 result = "Unknown";
443                 break;
444         }
445
446         t = time(NULL) - HtmlStart;
447         s = t % 60;
448         m = t / 60 % 60;
449         h = t / 60 / 60;
450
451         /*
452          * Cycle history file as appropriate, includes initial file handling.
453          */
454         if (HistNum == 0)
455                 HistNum = 1;
456         asprintf(&path, "%s/%02d_history.json", ReportPath, HistNum);
457         if (stat(path, &st) < 0) {
458                 fp = fopen(path, "we");
459         } else if (st.st_size > 50000) {
460                 ++HistNum;
461                 free(path);
462                 asprintf(&path, "%s/%02d_history.json", ReportPath, HistNum);
463                 fp = fopen(path, "we");
464         } else {
465                 fp = fopen(path, "r+e");
466                 fseek(fp, 0, SEEK_END);
467         }
468
469         if (fp) {
470                 if (ftell(fp) == 0) {
471                         fprintf(fp, "[\n");
472                 } else {
473                         fseek(fp, -2, SEEK_END);
474                 }
475                 fprintf(fp,
476                         "  %s{\n"
477                         "   \"entry\":%d\n"
478                         "   ,\"elapsed\":\"%02d:%02d:%02d\"\n"
479                         "   ,\"ID\":\"%02d\"\n"
480                         "   ,\"result\":\"%s\"\n"
481                         "   ,\"origin\":\"%s\"\n"
482                         "   ,\"info\":\"%s\"\n"
483                         "   ,\"duration\":\"%s\"\n"
484                         "  }\n"
485                         "]\n",
486                         ((ftell(fp) > 10) ? "," : ""),
487                         EntryNum,
488                         h, m, s,
489                         slot,
490                         result,
491                         pkg->portdir,
492                         dequote(reason),
493                         elapsed_buf
494                 );
495                 ++EntryNum;
496                 fclose(fp);
497
498         }
499         free(path);
500         if (mreason)
501                 free(mreason);
502 }
503
504 static void
505 HtmlSync(void)
506 {
507 }
508
509 runstats_t HtmlRunStats = {
510         .init = HtmlInit,
511         .done = HtmlDone,
512         .reset = HtmlReset,
513         .update = HtmlUpdate,
514         .updateTop = HtmlUpdateTop,
515         .updateLogs = HtmlUpdateLogs,
516         .updateCompletion = HtmlUpdateCompletion,
517         .sync = HtmlSync
518 };