Project homepage Mailing List  Warmcat.com  API Docs  Github Mirror 
    npro  
 Modern all-safe Rust Network Protocol library supporting h1, h2, h3, ws, wt sans-IO and with socket IO + tls
git clone https://npro.rs/repo/npro
 
root / scripts / builder-conf
Author[]Andy Green <andy@warmcat.com> 2026-04-04 05:09 UTC
Committer[]Andy Green <andy@warmcat.com> 2026-04-04 08:29 UTC
Tree2cf3039b805895bf8bf1e325763342079061a588   Raw Patch
 
log-segmentation
log-segmentation
diff --git a/assets/sai.css b/assets/sai.css index 5188fd4..bfd5711 100644 --- a/assets/sai.css +++ b/assets/sai.css @@ -1177,3 +1177,43 @@ body.overlay-active { margin-left: 10px; margin-top: 4px; } + +.log-segment-wrapper { + margin-top: 4px; +} +.log-segment-body .log-segment-wrapper { + margin-left: 2em; +} +.log-segment-header { + background-color: #f8f9fa; + cursor: pointer; + padding: 1px 2px; + border: 1px solid #ddd; + font-weight: bold; + font-family: 'OpenSans', sans-serif; + font-size: 11px; + margin-top: 1px; + margin-bottom: 1px; + user-select: none; + -webkit-user-select: none; + border-radius: 2px; +} +.log-segment-header.has-error { + background-color: #ffdddd; +} +.log-segment-header.has-warning { + background-color: #ffedcc; +} +.log-segment-header:hover { + background-color: #e9ecef; +} +.log-segment-body { + border-left: 1px solid #ddd; + padding-left: 2px; + margin-left: 2px; +} +.fold-icon { + display: inline-block; + width: 10px; + text-align: center; +} diff --git a/assets/sai.js b/assets/sai.js index f0430d6..65c30e0 100644 --- a/assets/sai.js +++ b/assets/sai.js @@ -399,6 +399,9 @@ var logs = "", redpend = 0, gitohashi_integ = 0, authd = 0, exptimer, auth_user ongoing_task_activities = {}, last_log_timestamp = 0, spreadsheet_data_cache = {}, loadreport_data_cache = {}, fadingTasks = new Map(); +var segment_stack = []; +var seg_counter = 0; + /* Global caches for reconcilation */ var pcon_topology = {}; var pcon_energy_cache = {}; @@ -561,6 +564,119 @@ function humanize(s) return s; } +function flush_segments() { + var target_id = segment_stack.length > 0 ? segment_stack[segment_stack.length - 1].id : "root"; + var c_idx = segment_stack.length > 0 ? (segment_stack[segment_stack.length - 1].chunk_index || 0) : 0; + + var logs_dom = target_id === "root" ? "logs" : ("dlogs-" + target_id + "-" + c_idx); + var lines_dom = target_id === "root" ? "dlogsn" : ("dlogsn-" + target_id + "-" + c_idx); + var times_dom = target_id === "root" ? "dlogst" : ("dlogst-" + target_id + "-" + c_idx); + + if (document.getElementById(logs_dom) && logs_pending) { + document.getElementById(logs_dom).insertAdjacentHTML('beforeend', logs_pending); + } + if (document.getElementById(lines_dom) && lines_pending) { + document.getElementById(lines_dom).insertAdjacentHTML('beforeend', lines_pending); + } + if (document.getElementById(times_dom) && times_pending) { + document.getElementById(times_dom).insertAdjacentHTML('beforeend', times_pending); + } + + logs_pending = lines_pending = times_pending = ""; + + if (target_id !== "root") { + var seg = segment_stack[segment_stack.length - 1]; + var ehdr = document.getElementById("hdr-seg-" + target_id); + if (ehdr) { + ehdr.querySelector('.seg-lines').innerText = seg.lines_count; + ehdr.querySelector('.seg-errors').innerText = seg.error_count; + + if (seg.error_count > 0) ehdr.classList.add("has-error"); + else if (seg.warning_count > 0) ehdr.classList.add("has-warning"); + } + } +} + +function append_chunk_table(id, chunk_index, target_dom) { + var html = ` + <table><tr> + <td class="atop"><div class="dlogsn" id="dlogsn-${id}-${chunk_index}"></div></td> + <td class="atop"><div class="dlogst" id="dlogst-${id}-${chunk_index}"></div></td> + <td class="atop"><div class="dlogs"><span class="nowrap" id="dlogs-${id}-${chunk_index}"></span></div></td> + </tr></table> + `; + if (target_dom) target_dom.insertAdjacentHTML('beforeend', html); +} + +function push_segment(title, default_folded) { + flush_segments(); + seg_counter++; + var id = seg_counter; + + var seg = { id: id, title: title, lines_count: 0, error_count: 0, warning_count: 0, folded: default_folded, chunk_index: 0 }; + + var parent_id = segment_stack.length > 0 ? segment_stack[segment_stack.length - 1].id : "root"; + // Append to root's dlogs container OR the parent segment's BODY container + var parent_logs_dom = parent_id === "root" ? "dlogs" : ("seg-" + parent_id); + var parent_dom = document.getElementById(parent_logs_dom); + + if (parent_dom) { + var icon = default_folded ? "▶" : "▼"; + var hideClass = default_folded ? " hide" : ""; + + var html = `<div class="log-segment-wrapper"> + <div class="log-segment-header" id="hdr-seg-${id}"> + <span class="fold-icon">${icon}</span><span class="seg-title">${title}</span> + <span class="seg-stats">(<span class="seg-lines">0</span> lines, <span class="seg-errors">0</span> errors)</span> + </div> + <div class="log-segment-body${hideClass}" id="seg-${id}"> + </div> + </div>`; + + // If root, we only append once, but wait, root is just flat. + parent_dom.insertAdjacentHTML('beforeend', html); + + var seg_dom = document.getElementById("seg-" + id); + append_chunk_table(id, 0, seg_dom); + + if (parent_id === "root") { + if (document.getElementById("dlogsn")) document.getElementById("dlogsn").insertAdjacentHTML('beforeend', '<br><br>'); + if (document.getElementById("dlogst")) document.getElementById("dlogst").insertAdjacentHTML('beforeend', '<br><br>'); + } + } + segment_stack.push(seg); +} + +function pop_segment() { + if (segment_stack.length > 0) { + flush_segments(); + segment_stack.pop(); + + // When we return to parent, we need a new table below the children we just popped + if (segment_stack.length > 0) { + var p = segment_stack[segment_stack.length - 1]; + p.chunk_index++; + var seg_dom = document.getElementById("seg-" + p.id); + append_chunk_table(p.id, p.chunk_index, seg_dom); + } + } +} + +function toggleSegment(id) { + var body = document.getElementById("seg-" + id); + var hdr = document.getElementById("hdr-seg-" + id); + if (body && hdr) { + var icon = hdr.querySelector('.fold-icon'); + if (body.classList.contains("hide")) { + body.classList.remove("hide"); + if (icon) icon.innerText = "▼"; + } else { + body.classList.add("hide"); + if (icon) icon.innerText = "▶"; + } + } +} + function ansiToHtml(text, state) { const classMap = { '1': 'ansi-bold', '4': 'ansi-underline', @@ -1671,9 +1787,13 @@ function ws_open_sai() // s1 = s1.split("?")[0]; console.log(s1); sai = new WebSocket(s1, "com-warmcat-sai"); - try { + sai.onopen = function() { + if (typeof window.overlayTimeout !== 'undefined' && window.overlayTimeout) { + clearTimeout(window.overlayTimeout); + window.overlayTimeout = null; + } var overlay = document.querySelector(".overlay"); if (overlay) { overlay.parentNode.removeChild(overlay); @@ -2213,70 +2333,147 @@ function ws_open_sai() break; case "com-warmcat-sai-logs": + var s1; try { - var s1 = decodeURIComponent(escape(atob(jso.log))), - ansiResult = ansiToHtml(s1, logAnsiState), - s = ansiResult.html, li, - en = "", yo, dh, ce, tn = ""; - logAnsiState = ansiResult.newState; + s1 = decodeURIComponent(escape(atob(jso.log))); } catch (e) { break; } - if (!tfirst) - tfirst = jso.timestamp; - + if (!tfirst) tfirst = jso.timestamp; last_log_timestamp = jso.timestamp; - li = (s1.match(/\n/g)||[]).length; + var lines_started = 0; + var lines_arr = s1.split('\n'); + for (var idx = 0; idx < lines_arr.length; ++idx) { + if (idx === lines_arr.length - 1 && lines_arr[idx] === '') continue; + + var text_line = lines_arr[idx]; + var has_nl = (idx < lines_arr.length - 1) ? '\n' : ''; + var line_str = text_line + has_nl; + + var ansiResult = ansiToHtml(line_str, logAnsiState); + var s = ansiResult.html; + logAnsiState = ansiResult.newState; + + var li = has_nl ? 1 : 0; + var en = "", tn = ""; + if (cont && !cont[jso.channel] && jso.len) + tn = ((jso.timestamp - tfirst) / 1000000).toFixed(4); + + var temp_li = li; + var temp_lli = lli; + while (temp_li > 0) { + en += "<a id=\"#sn" + temp_lli + "\" href=\"#sn" + temp_lli + "\">" + temp_lli + "</a><br>"; + tn += "<br>"; + temp_lli++; + temp_li--; + } + + var s_logs = ""; + switch (jso.channel) { + case 1: s_logs = s; break; + case 2: s_logs = "<span class=\"stderr\">" + s + "</span>"; break; + case 3: s_logs = "<span class=\"saibuild\">\u{25a0} " + s + "</span>"; break; + case 4: s_logs = "<span class=\"tty0\">" + s + "</span>"; break; + default: s_logs = "<span class=\"tty1\">" + s + "</span>"; break; + } - switch (jso.channel) { - case 1: - logs += s; logs_pending += s; - break; - case 2: - logs += "<span class=\"stderr\">" + s + - "</span>"; - logs_pending += "<span class=\"stderr\">" + s + - "</span>"; - break; - case 3: - logs += "<span class=\"saibuild\">\u{25a0} " + s + - "</span>"; - logs_pending += "<span class=\"saibuild\">\u{25a0} " + s + - "</span>"; - break; - case 4: - logs += "<span class=\"tty0\">" + s + - "</span>"; - logs_pending += "<span class=\"tty0\">" + s + - "</span>"; - break; - default: - logs += "<span class=\"tty1\">" + s + - "</span>"; - logs_pending += "<span class=\"tty1\">" + s + - "</span>"; + var skip_push = false; + var skip_render = false; + var match_fail = jso.channel === 1 ? text_line.match(/Test\s+#(\d+):\s+.*(Failed|\*\*\*)/i) : null; + var is_fail = match_fail || (jso.channel === 1 && /test failed/i.test(text_line)); + + if (jso.channel === 1) { + if (window.held_start_line) { + if (is_fail) { + while (segment_stack.length > 1) pop_segment(); + push_segment(text_line, true); + skip_push = true; + } + + logs += window.held_start_line.s_logs; logs_pending += window.held_start_line.s_logs; + if (window.held_start_line.li) { + lines += window.held_start_line.en; lines_pending += window.held_start_line.en; + times += window.held_start_line.tn; times_pending += window.held_start_line.tn; + } + window.held_start_line = null; + } + + if (/^\s*Start\s+\d+:/i.test(text_line)) { + window.held_start_line = { text: text_line, s_logs: s_logs, en: en, tn: tn, li: li }; + skip_render = true; + } + } + if (jso.channel === 3) { + if (/^>saib>\s+Starting task step/.test(text_line)) { + while (segment_stack.length > 0) pop_segment(); + push_segment(text_line, true); + } else if (/^>saib>\s+Step \d+:/.test(text_line) && segment_stack.length > 0) { + var pseg = segment_stack[0]; + var phdr = document.getElementById("hdr-seg-" + pseg.id); + if (phdr) phdr.querySelector('.seg-title').innerText = text_line; + } else { + while (segment_stack.length > 1) pop_segment(); + } + } + + if (jso.channel === 1) { + if (skip_push) { + // Fold logic successfully handled during lookahead execution + } else if (match_fail || /test failed/i.test(text_line)) { + while (segment_stack.length > 1) pop_segment(); + push_segment(text_line, true); + } else if (/^\d+% tests passed/i.test(text_line) || /Total Test time/i.test(text_line) || /The following tests FAILED:/i.test(text_line) || /Errors while running CTest/i.test(text_line)) { + while (segment_stack.length > 1) pop_segment(); + } else if (/^\d+\/\d+\s+Test\s+#\d+:/i.test(text_line)) { + while (segment_stack.length > 1) pop_segment(); + } + } + + var text_lower = text_line.toLowerCase(); + if (text_lower.includes("error:") || text_lower.includes("fatal:") || /error\s\d+:/.test(text_lower)) { + for (var si = 0; si < segment_stack.length; si++) { + var sobj = segment_stack[si]; + sobj.error_count++; + if (si < segment_stack.length - 1) { + var sbody = document.getElementById("seg-" + sobj.id); + var shdr = document.getElementById("hdr-seg-" + sobj.id); + if (sbody && shdr && sbody.classList.contains("hide")) { + sbody.classList.remove("hide"); + var sicon = shdr.querySelector('.fold-icon'); + if (sicon) sicon.innerText = "▼"; + } + } + } + } else if (text_lower.includes("warning:")) { + for (var si = 0; si < segment_stack.length; si++) segment_stack[si].warning_count++; + } - } + if (segment_stack.length > 0) { + segment_stack[segment_stack.length - 1].lines_count += (has_nl ? 1 : 0); + } - if (cont && !cont[jso.channel] && jso.len) - tn = ((jso.timestamp - tfirst) / 1000000).toFixed(4); + if (!skip_render) { + if (s_logs) { + logs += s_logs; logs_pending += s_logs; + } + if (li) { + lines += en; lines_pending += en; + times += tn; times_pending += tn; + } + } - if (cont) - cont[jso.channel] = (li == 0); + if (cont) + cont[jso.channel] = (li === 0); - while (li--) { - en += "<a id=\"#sn" + lli + - "\" href=\"#sn" + lli + "\">" + - lli + "</a><br>"; - tn += "<br>" - lli++; - } + while (li > 0) { + lli++; + li--; + } - lines += en; lines_pending += en; - times += tn; times_pending += tn; + } if (!redpend) { redpend = 1; @@ -2288,22 +2485,7 @@ function ws_open_sai() rightPane.clientHeight <= rightPane.scrollTop + 1; - if (document.getElementById("logs")) { - if (logs_pending) { - document.getElementById("logs").insertAdjacentHTML('beforeend', logs_pending); - logs_pending = ""; - } - - if (document.getElementById("dlogsn") && lines_pending) { - document.getElementById("dlogsn").insertAdjacentHTML('beforeend', lines_pending); - lines_pending = ""; - } - - if (document.getElementById("dlogst") && times_pending) { - document.getElementById("dlogst").insertAdjacentHTML('beforeend', times_pending); - times_pending = ""; - } - } + flush_segments(); if (locked && rightPane) rightPane.scrollTop = @@ -2320,13 +2502,20 @@ function ws_open_sai() }; sai.onclose = function(ev){ - var overlay = document.createElement("div"); - overlay.className = "overlay"; - document.body.appendChild(overlay); - document.body.classList.add("overlay-active"); - console.log("WebSocket closed. Code:", ev.code, "Reason:", ev.reason); - myVar = setTimeout(ws_open_sai, 4000); + + if (typeof overlayTimeout !== 'undefined' && overlayTimeout) clearTimeout(overlayTimeout); + + window.overlayTimeout = setTimeout(function() { + if (!document.querySelector(".overlay")) { + var overlay = document.createElement("div"); + overlay.className = "overlay"; + document.body.appendChild(overlay); + document.body.classList.add("overlay-active"); + } + }, 3000); + + myVar = setTimeout(ws_open_sai, 1000); }; } catch(exception) { alert("<p>Error" + exception); @@ -2337,6 +2526,14 @@ function ws_open_sai() window.addEventListener("load", function() { + document.addEventListener('click', function(e) { + var hdr = e.target.closest('.log-segment-header'); + if (hdr) { + var id = hdr.id.substring(8); + toggleSegment(id); + } + }); + const savedFlex = localStorage.getItem('sai-left-pane-flex'); if (savedFlex) { const leftPane = document.querySelector('.left-pane'); diff --git a/src/builder/b-nspawn.c b/src/builder/b-nspawn.c index b168fa1..7a7f42a 100644 --- a/src/builder/b-nspawn.c +++ b/src/builder/b-nspawn.c @@ -525,6 +525,9 @@ saib_spawn_script(struct sai_nspawn *ns) lws_strncpy(one_step, ns->task->script, sizeof(one_step)); + if (saib_log_chunk_create(op->ns, one_step, strlen(one_step), 3)) + return -1; + #if defined(WIN32) if (_sopen_s(&fd, ns->script_path, _O_CREAT | _O_TRUNC | _O_WRONLY, _SH_DENYNO, _S_IWRITE)) diff --git a/src/web/w-comms.c b/src/web/w-comms.c index f8da1a6..3bdf56c 100644 --- a/src/web/w-comms.c +++ b/src/web/w-comms.c @@ -565,7 +565,8 @@ http_resp: break; { - char som, eom, rb[1200]; + char som, eom, rb[1200 + LWS_PRE]; + uint8_t *prb = (uint8_t *)rb + LWS_PRE; int used, final = 1; size_t fsl = lws_buflist_next_segment_len(&pss->raw_tx, NULL); @@ -583,7 +584,7 @@ http_resp: * fragment_use sets 'som' to true if we are at * the segment start. */ - used = lws_buflist_fragment_use(&pss->raw_tx, (uint8_t *)rb, sizeof(rb), &som, &eom); + used = lws_buflist_fragment_use(&pss->raw_tx, prb, 1200, &som, &eom); if (!used) return 0; @@ -595,7 +596,7 @@ http_resp: if (used < (int)fsl || (pss->segment_flags & LWS_WRITE_NO_FIN)) final = 0; - if (lws_write(pss->wsi, (uint8_t *)rb + ((size_t)som * sizeof(int)), + if (lws_write(pss->wsi, prb + ((size_t)som * sizeof(int)), (size_t)used - ((size_t)som * sizeof(int)), (lws_ws_sending_multifragment(pss->wsi) ? LWS_WRITE_CONTINUATION : LWS_WRITE_TEXT) |
Page fetched 0s ago, creation time: 20ms (vhost etag hits: 0%, cache hits: 0%)