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