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