xref: /dragonfly/usr.bin/dsynth/html.c (revision c9c5aa9e)
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 			"    ,\"meta\":%d\n"
325 			"    ,\"elapsed\":\"%s\"\n"
326 			"    ,\"pkghour\":%d\n"
327 			"    ,\"impulse\":%d\n"
328 			"    ,\"swapinfo\":\"%s\"\n"
329 			"    ,\"load\":\"%s\"\n"
330 			"  }\n",
331 			Profile,
332 			KickOff_Buf,
333 			HistNum,		/* kfiles */
334 			info->active,		/* active */
335 
336 			info->total,		/* queued */
337 			info->successful,	/* built */
338 			info->failed,		/* failed */
339 			info->ignored,		/* ignored */
340 			info->skipped,		/* skipped */
341 			info->remaining,	/* remaining */
342 			info->meta,		/* meta-nodes */
343 			elapsed_buf,		/* elapsed */
344 			info->pkgrate,		/* pkghour */
345 			info->pkgimpulse,	/* impulse */
346 			swap_buf,		/* swapinfo */
347 			load_buf		/* load */
348 		);
349 		fprintf(fp,
350 			"  ,\"builders\":[\n"
351 		);
352 		for (i = 0; i < MaxWorkers; ++i) {
353 			if (HtmlSlots[i]) {
354 				if (i)
355 					fprintf(fp, ",");
356 				fwrite(HtmlSlots[i], 1,
357 				       strlen(HtmlSlots[i]), fp);
358 			} else {
359 				fprintf(fp,
360 					"   %s{\n"
361 					"     \"ID\":\"%02d\"\n"
362 					"     ,\"elapsed\":\"Shutdown\"\n"
363 					"     ,\"phase\":\"\"\n"
364 					"     ,\"origin\":\"\"\n"
365 					"     ,\"lines\":\"\"\n"
366 					"    }\n",
367 					(i ? "," : ""),
368 					i
369 				);
370 			}
371 		}
372 		fprintf(fp,
373 			"  ]\n"
374 			"}\n");
375 		fflush(fp);
376 		fclose(fp);
377 	}
378 	rename(path, dst);
379 	free(path);
380 	free(dst);
381 }
382 
383 static void
384 HtmlUpdateLogs(void)
385 {
386 }
387 
388 static void
389 HtmlUpdateCompletion(worker_t *work, int dlogid, pkg_t *pkg,
390 		     const char *reason, const char *skipbuf)
391 {
392 	FILE *fp;
393 	char *path;
394 	char elapsed_buf[64];
395 	struct stat st;
396 	time_t t;
397 	int s, m, h;
398 	int slot;
399 	const char *result;
400 	char *mreason;
401 
402 	if (skipbuf[0] == 0)
403 		skipbuf = "0";
404 	else if (skipbuf[0] == ' ')
405 		++skipbuf;
406 
407 	mreason = NULL;
408 	if (work) {
409 		t = time(NULL) - work->start_time;
410 		s = t % 60;
411 		m = t / 60 % 60;
412 		h = t / 60 / 60;
413 		SNPRINTF(elapsed_buf, "%02d:%02d:%02d", h, m, s);
414 		slot = work->index;
415 	} else {
416 		slot = -1;
417 		elapsed_buf[0] = 0;
418 	}
419 
420 	switch(dlogid) {
421 	case DLOG_SUCC:
422 		if (pkg->flags & PKGF_DUMMY)
423 			result = "meta";
424 		else
425 			result = "built";
426 		break;
427 	case DLOG_FAIL:
428 		result = "failed";
429 		if (work) {
430 			asprintf(&mreason, "%s:%s",
431 				 getphasestr(work->phase),
432 				 reason);
433 		} else {
434 			asprintf(&mreason, "unknown:%s", reason);
435 		}
436 		reason = mreason;
437 		break;
438 	case DLOG_IGN:
439 		result = "ignored";
440 		asprintf(&mreason, "%s:|:%s", reason, skipbuf);
441 		reason = mreason;
442 		break;
443 	case DLOG_SKIP:
444 		result = "skipped";
445 		break;
446 	default:
447 		result = "Unknown";
448 		break;
449 	}
450 
451 	t = time(NULL) - HtmlStart;
452 	s = t % 60;
453 	m = t / 60 % 60;
454 	h = t / 60 / 60;
455 
456 	/*
457 	 * Cycle history file as appropriate, includes initial file handling.
458 	 */
459 	if (HistNum == 0)
460 		HistNum = 1;
461 	asprintf(&path, "%s/%02d_history.json", ReportPath, HistNum);
462 	if (stat(path, &st) < 0) {
463 		fp = fopen(path, "we");
464 	} else if (st.st_size > 50000) {
465 		++HistNum;
466 		free(path);
467 		asprintf(&path, "%s/%02d_history.json", ReportPath, HistNum);
468 		fp = fopen(path, "we");
469 	} else {
470 		fp = fopen(path, "r+e");
471 		fseek(fp, 0, SEEK_END);
472 	}
473 
474 	if (fp) {
475 		if (ftell(fp) == 0) {
476 			fprintf(fp, "[\n");
477 		} else {
478 			fseek(fp, -2, SEEK_END);
479 		}
480 		fprintf(fp,
481 			"  %s{\n"
482 			"   \"entry\":%d\n"
483 			"   ,\"elapsed\":\"%02d:%02d:%02d\"\n"
484 			"   ,\"ID\":\"%02d\"\n"
485 			"   ,\"result\":\"%s\"\n"
486 			"   ,\"origin\":\"%s\"\n"
487 			"   ,\"info\":\"%s\"\n"
488 			"   ,\"duration\":\"%s\"\n"
489 			"  }\n"
490 			"]\n",
491 			((ftell(fp) > 10) ? "," : ""),
492 			EntryNum,
493 			h, m, s,
494 			slot,
495 			result,
496 			pkg->portdir,
497 			dequote(reason),
498 			elapsed_buf
499 		);
500 		++EntryNum;
501 		fclose(fp);
502 
503 	}
504 	free(path);
505 	if (mreason)
506 		free(mreason);
507 }
508 
509 static void
510 HtmlSync(void)
511 {
512 }
513 
514 runstats_t HtmlRunStats = {
515 	.init = HtmlInit,
516 	.done = HtmlDone,
517 	.reset = HtmlReset,
518 	.update = HtmlUpdate,
519 	.updateTop = HtmlUpdateTop,
520 	.updateLogs = HtmlUpdateLogs,
521 	.updateCompletion = HtmlUpdateCompletion,
522 	.sync = HtmlSync
523 };
524