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