xref: /dragonfly/usr.bin/dsynth/html.c (revision 2b7dbe20)
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 static const char *CopyFilesAry[] = {
48 	"favicon.png",
49 	"progress.html",
50 	"progress.css",
51 	"progress.js",
52 	"dsynth.png",
53 	NULL
54 };
55 
56 static char **HtmlSlots;
57 static time_t HtmlStart;
58 static 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 };
519