dsynth - Fix escaping, skipped count
[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         char buf[256];
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                                 snprintf(buf, sizeof(buf), "%s", reason);
74                                 reason = buf;
75                         }
76                         buf[i] = ' ';
77                 }
78         }
79         return reason;
80 }
81
82 static void
83 HtmlInit(void)
84 {
85         struct dirent *den;
86         DIR *dir;
87         struct stat st;
88         struct tm tmm;
89         size_t len;
90         char *src;
91         char *dst;
92         time_t t;
93         int i;
94
95         HtmlSlots = calloc(sizeof(char *), MaxWorkers);
96         HtmlLast = 0;
97         HtmlStart = time(NULL);
98
99         asprintf(&ReportPath, "%s/Report", LogsPath);
100         if (stat(ReportPath, &st) < 0 && mkdir(ReportPath, 0755) < 0)
101                 dfatal("Unable to create %s", ReportPath);
102         for (i = 0; CopyFilesAry[i]; ++i) {
103                 asprintf(&src, "%s/%s", SCRIPTPATH(SCRIPTDIR), CopyFilesAry[i]);
104                 if (strcmp(CopyFilesAry[i], "progress.html") == 0) {
105                         asprintf(&dst, "%s/index.html", ReportPath);
106                 } else {
107                         asprintf(&dst, "%s/%s", ReportPath, CopyFilesAry[i]);
108                 }
109                 copyfile(src, dst);
110                 free(src);
111                 free(dst);
112         }
113
114         asprintf(&src, "%s/summary.json", ReportPath);
115         remove(src);
116         free(src);
117
118         t = time(NULL);
119         gmtime_r(&t, &tmm);
120         strftime(KickOff_Buf, sizeof(KickOff_Buf),
121                  " %d-%b-%Y %H:%M:%S %Z", &tmm);
122
123         dir = opendir(ReportPath);
124         if (dir == NULL)
125                 dfatal("Unable to scan %s", ReportPath);
126         while ((den = readdir(dir)) != NULL) {
127                 len = strlen(den->d_name);
128                 if (len > 13 &&
129                     strcmp(den->d_name + len - 13, "_history.json") == 0) {
130                         asprintf(&src, "%s/%s", ReportPath, den->d_name);
131                         remove(src);
132                         free(src);
133                 }
134         }
135         closedir(dir);
136
137         /*
138          * First history file
139          */
140         HistNum = 0;
141         EntryNum = 1;
142 }
143
144 static void
145 HtmlDone(void)
146 {
147         int i;
148
149         for (i = 0; i < MaxWorkers; ++i) {
150                 if (HtmlSlots[i])
151                         free(HtmlSlots[i]);
152         }
153         free(HtmlSlots);
154         HtmlSlots = NULL;
155 }
156
157 static void
158 HtmlReset(void)
159 {
160 }
161
162 static void
163 HtmlUpdate(worker_t *work, const char *portdir)
164 {
165         const char *phase;
166         const char *origin;
167         time_t t;
168         int i = work->index;
169         int h;
170         int m;
171         int s;
172         int clear;
173         char elapsed_buf[32];
174         char lines_buf[32];
175
176         phase = "Unknown";
177         origin = "";
178         clear = 0;
179
180         switch(work->state) {
181         case WORKER_NONE:
182                 phase = "None";
183                 /* fall through */
184         case WORKER_IDLE:
185                 if (work->state == WORKER_IDLE)
186                         phase = "Idle";
187                 clear = 1;
188                 break;
189         case WORKER_FAILED:
190                 if (work->state == WORKER_FAILED)
191                         phase = "Failed";
192                 /* fall through */
193         case WORKER_EXITING:
194                 if (work->state == WORKER_EXITING)
195                         phase = "Exiting";
196                 return;
197                 /* NOT REACHED */
198         case WORKER_PENDING:
199                 phase = "Pending";
200                 break;
201         case WORKER_RUNNING:
202                 phase = "Running";
203                 break;
204         case WORKER_DONE:
205                 phase = "Done";
206                 break;
207         case WORKER_FROZEN:
208                 phase = "FROZEN";
209                 break;
210         default:
211                 break;
212         }
213
214         if (clear) {
215                 SNPRINTF(elapsed_buf, "%s", " --:--:--");
216                 SNPRINTF(lines_buf, "%s", "");
217                 origin = "";
218         } else {
219                 t = time(NULL) - work->start_time;
220                 s = t % 60;
221                 m = t / 60 % 60;
222                 h = t / 60 / 60;
223                 if (h > 99)
224                         SNPRINTF(elapsed_buf, "%3d:%02d:%02d", h, m, s);
225                 else
226                         SNPRINTF(elapsed_buf, " %02d:%02d:%02d", h, m, s);
227
228                 if (work->state == WORKER_RUNNING)
229                         phase = getphasestr(work->phase);
230
231                 /*
232                  * When called from the monitor frontend portdir has to be
233                  * passed in directly because work->pkg is not mapped.
234                  */
235                 if (portdir)
236                         origin = portdir;
237                 else if (work->pkg)
238                         origin = work->pkg->portdir;
239                 else
240                         origin = "";
241
242                 SNPRINTF(lines_buf, "%ld", work->lines);
243         }
244
245         /*
246          * Update the summary information
247          */
248         if (HtmlSlots[i])
249                 free(HtmlSlots[i]);
250         asprintf(&HtmlSlots[i],
251                  "  {\n"
252                  "     \"ID\":\"%02d\"\n"
253                  "     ,\"elapsed\":\"%s\"\n"
254                  "     ,\"phase\":\"%s\"\n"
255                  "     ,\"origin\":\"%s\"\n"
256                  "     ,\"lines\":\"%s\"\n"
257                  "  }\n",
258                  i,
259                  elapsed_buf,
260                  phase,
261                  origin,
262                  lines_buf
263         );
264 }
265
266 static void
267 HtmlUpdateTop(topinfo_t *info)
268 {
269         char *path;
270         char *dst;
271         FILE *fp;
272         int i;
273         char elapsed_buf[32];
274         char swap_buf[32];
275         char load_buf[32];
276
277         /*
278          * Be sure to do the first update and final update, but otherwise
279          * only update every 10 seconds or so.
280          */
281         if (HtmlLast && (int)(time(NULL) - HtmlLast) < 10 && info->active)
282                 return;
283         HtmlLast = time(NULL);
284
285         if (info->h > 99) {
286                 SNPRINTF(elapsed_buf, "%3d:%02d:%02d",
287                          info->h, info->m, info->s);
288         } else {
289                 SNPRINTF(elapsed_buf, " %02d:%02d:%02d",
290                          info->h, info->m, info->s);
291         }
292
293         if (info->noswap)
294                 SNPRINTF(swap_buf, "-    ");
295         else
296                 SNPRINTF(swap_buf, "%5.1f", info->dswap);
297
298         if (info->dload[0] > 999.9)
299                 SNPRINTF(load_buf, "%5.0f", info->dload[0]);
300         else
301                 SNPRINTF(load_buf, "%5.1f", info->dload[0]);
302
303         asprintf(&path, "%s/summary.json.new", ReportPath);
304         asprintf(&dst, "%s/summary.json", ReportPath);
305         fp = fopen(path, "we");
306         if (!fp)
307                 ddassert(0);
308         if (fp) {
309                 fprintf(fp,
310                         "{\n"
311                         "  \"profile\":\"%s\"\n"
312                         "  ,\"kickoff\":\"%s\"\n"
313                         "  ,\"kfiles\":%d\n"
314                         "  ,\"active\":%d\n"
315                         "  ,\"stats\":{\n"
316                         "    \"queued\":%d\n"
317                         "    ,\"built\":%d\n"
318                         "    ,\"failed\":%d\n"
319                         "    ,\"ignored\":%d\n"
320                         "    ,\"skipped\":%d\n"
321                         "    ,\"remains\":%d\n"
322                         "    ,\"elapsed\":\"%s\"\n"
323                         "    ,\"pkghour\":%d\n"
324                         "    ,\"impulse\":%d\n"
325                         "    ,\"swapinfo\":\"%s\"\n"
326                         "    ,\"load\":\"%s\"\n"
327                         "  }\n",
328                         Profile,
329                         KickOff_Buf,
330                         HistNum,                /* kfiles */
331                         info->active,           /* active */
332
333                         info->total,            /* queued */
334                         info->successful,       /* built */
335                         info->failed,           /* failed */
336                         info->ignored,          /* ignored */
337                         info->skipped,          /* skipped */
338                         info->remaining,        /* remaining */
339                         elapsed_buf,            /* elapsed */
340                         info->pkgrate,          /* pkghour */
341                         info->pkgimpulse,       /* impulse */
342                         swap_buf,               /* swapinfo */
343                         load_buf                /* load */
344                 );
345                 fprintf(fp,
346                         "  ,\"builders\":[\n"
347                 );
348                 for (i = 0; i < MaxWorkers; ++i) {
349                         if (HtmlSlots[i]) {
350                                 if (i)
351                                         fprintf(fp, ",");
352                                 fwrite(HtmlSlots[i], 1,
353                                        strlen(HtmlSlots[i]), fp);
354                         } else {
355                                 fprintf(fp,
356                                         "   %s{\n"
357                                         "     \"ID\":\"%02d\"\n"
358                                         "     ,\"elapsed\":\"Shutdown\"\n"
359                                         "     ,\"phase\":\"\"\n"
360                                         "     ,\"origin\":\"\"\n"
361                                         "     ,\"lines\":\"\"\n"
362                                         "    }\n",
363                                         (i ? "," : ""),
364                                         i
365                                 );
366                         }
367                 }
368                 fprintf(fp,
369                         "  ]\n"
370                         "}\n");
371                 fflush(fp);
372                 fclose(fp);
373         }
374         rename(path, dst);
375         free(path);
376         free(dst);
377 }
378
379 static void
380 HtmlUpdateLogs(void)
381 {
382 }
383
384 static void
385 HtmlUpdateCompletion(worker_t *work, int dlogid, pkg_t *pkg, const char *reason)
386 {
387         FILE *fp;
388         char *path;
389         char elapsed_buf[64];
390         struct stat st;
391         time_t t;
392         int s, m, h;
393         int slot;
394         const char *result;
395         char *mreason;
396
397         mreason = NULL;
398         if (work) {
399                 t = time(NULL) - work->start_time;
400                 s = t % 60;
401                 m = t / 60 % 60;
402                 h = t / 60 / 60;
403                 SNPRINTF(elapsed_buf, "%02d:%02d:%02d", h, m, s);
404                 slot = work->index;
405         } else {
406                 slot = -1;
407                 elapsed_buf[0] = 0;
408         }
409
410         switch(dlogid) {
411         case DLOG_SUCC:
412                 result = "built";
413                 break;
414         case DLOG_FAIL:
415                 result = "failed";
416                 if (work) {
417                         asprintf(&mreason, "%s:%s",
418                                  getphasestr(work->phase),
419                                  reason);
420                 } else {
421                         asprintf(&mreason, "unknown:%s", reason);
422                 }
423                 reason = mreason;
424                 break;
425         case DLOG_IGN:
426                 result = "ignored";
427                 asprintf(&mreason, "%s:|:0", reason);
428                 reason = mreason;
429                 break;
430         case DLOG_SKIP:
431                 result = "skipped";
432                 break;
433         default:
434                 result = "Unknown";
435                 break;
436         }
437
438         t = time(NULL) - HtmlStart;
439         s = t % 60;
440         m = t / 60 % 60;
441         h = t / 60 / 60;
442
443         /*
444          * Cycle history file as appropriate, includes initial file handling.
445          */
446         if (HistNum == 0)
447                 HistNum = 1;
448         asprintf(&path, "%s/%02d_history.json", ReportPath, HistNum);
449         if (stat(path, &st) < 0) {
450                 fp = fopen(path, "we");
451         } else if (st.st_size > 50000) {
452                 ++HistNum;
453                 free(path);
454                 asprintf(&path, "%s/%02d_history.json", ReportPath, HistNum);
455                 fp = fopen(path, "we");
456         } else {
457                 fp = fopen(path, "r+e");
458                 fseek(fp, 0, SEEK_END);
459         }
460
461         if (fp) {
462                 if (ftell(fp) == 0) {
463                         fprintf(fp, "[\n");
464                 } else {
465                         fseek(fp, -2, SEEK_END);
466                 }
467                 fprintf(fp,
468                         "  %s{\n"
469                         "   \"entry\":%d\n"
470                         "   ,\"elapsed\":\"%02d:%02d:%02d\"\n"
471                         "   ,\"ID\":\"%02d\"\n"
472                         "   ,\"result\":\"%s\"\n"
473                         "   ,\"origin\":\"%s\"\n"
474                         "   ,\"info\":\"%s\"\n"
475                         "   ,\"duration\":\"%s\"\n"
476                         "  }\n"
477                         "]\n",
478                         ((ftell(fp) > 10) ? "," : ""),
479                         EntryNum,
480                         h, m, s,
481                         slot,
482                         result,
483                         pkg->portdir,
484                         dequote(reason),
485                         elapsed_buf
486                 );
487                 ++EntryNum;
488                 fclose(fp);
489
490         }
491         free(path);
492         if (mreason)
493                 free(mreason);
494 }
495
496 static void
497 HtmlSync(void)
498 {
499 }
500
501 runstats_t HtmlRunStats = {
502         .init = HtmlInit,
503         .done = HtmlDone,
504         .reset = HtmlReset,
505         .update = HtmlUpdate,
506         .updateTop = HtmlUpdateTop,
507         .updateLogs = HtmlUpdateLogs,
508         .updateCompletion = HtmlUpdateCompletion,
509         .sync = HtmlSync
510 };