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