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 / READMEs / README-sai-virt.md
Author[]Andy Green <andy@warmcat.com> 2026-04-12 08:37 UTC
Committer[]Andy Green <andy@warmcat.com> 2026-04-13 07:39 UTC
Tree1aeda99c5ad7f6257bfd3a2add6204dfb6b2707c   Raw Patch
 
builder: spawn: pty
builder: spawn: pty
diff --git a/assets/404.html b/assets/404.html index 3e5a14b..9d856c9 100644 --- a/assets/404.html +++ b/assets/404.html @@ -1,5 +1,5 @@ <meta charset="UTF-8"> -<html> +<html lang="en"> <body> <img src="libwebsockets.org-logo.svg"><br> <h1>404</h1> diff --git a/assets/failed-login.html b/assets/failed-login.html index 9ab065b..66bfe9c 100644 --- a/assets/failed-login.html +++ b/assets/failed-login.html @@ -1,3 +1,3 @@ -<html> +<html lang="en"> This is an example destination that will appear after a failed login </html> diff --git a/assets/gs/admin-login.html b/assets/gs/admin-login.html index 113df9c..fa36384 100644 --- a/assets/gs/admin-login.html +++ b/assets/gs/admin-login.html @@ -1,4 +1,4 @@ -<html> +<html lang="en"> This is an example destination that will appear after successful Admin login. This URL cannot be served if you're not logged in as admin. diff --git a/assets/gs/index.html b/assets/gs/index.html index d40904a..a0445a0 100644 --- a/assets/gs/index.html +++ b/assets/gs/index.html @@ -1,4 +1,4 @@ -<html> +<html lang="en"> <head> <meta charset="UTF-8"> <script src="/lws-common.js"></script> diff --git a/assets/gs/post-forgot-fail.html b/assets/gs/post-forgot-fail.html index ead3d13..ed575a6 100644 --- a/assets/gs/post-forgot-fail.html +++ b/assets/gs/post-forgot-fail.html @@ -1,4 +1,4 @@ -<html> +<html lang="en"> Sorry, something went wrong. Click <a href="../">here</a> to continue. diff --git a/assets/gs/post-forgot-ok.html b/assets/gs/post-forgot-ok.html index 3e8e9cf..bfec1e0 100644 --- a/assets/gs/post-forgot-ok.html +++ b/assets/gs/post-forgot-ok.html @@ -1,4 +1,4 @@ -<html> +<html lang="en"> This is a one-time password recovery login. Please click <a href="./">here</a> and click your username at the top to reset your password. diff --git a/assets/gs/post-verify-fail.html b/assets/gs/post-verify-fail.html index d1d89ca..d0e5b3c 100644 --- a/assets/gs/post-verify-fail.html +++ b/assets/gs/post-verify-fail.html @@ -1,4 +1,4 @@ -<html> +<html lang="en"> <head> <script src="lwsgs.js"></script> </head> diff --git a/assets/gs/post-verify-ok.html b/assets/gs/post-verify-ok.html index ae647fc..918b713 100644 --- a/assets/gs/post-verify-ok.html +++ b/assets/gs/post-verify-ok.html @@ -1,4 +1,4 @@ -<html> +<html lang="en"> <head> <script src="lwsgs.js"></script> </head> diff --git a/assets/gs/sent-forgot-fail.html b/assets/gs/sent-forgot-fail.html index ead3d13..ed575a6 100644 --- a/assets/gs/sent-forgot-fail.html +++ b/assets/gs/sent-forgot-fail.html @@ -1,4 +1,4 @@ -<html> +<html lang="en"> Sorry, something went wrong. Click <a href="../">here</a> to continue. diff --git a/assets/gs/successful-login.html b/assets/gs/successful-login.html index dfc25cf..39a47d5 100644 --- a/assets/gs/successful-login.html +++ b/assets/gs/successful-login.html @@ -1,4 +1,4 @@ -<html> +<html lang="en"> This is an example destination that will appear after successful non-Admin login </html> diff --git a/assets/post-register-ok.html b/assets/post-register-ok.html index f5d70b8..b1bddd5 100644 --- a/assets/post-register-ok.html +++ b/assets/post-register-ok.html @@ -1,4 +1,4 @@ -<html> +<html lang="en"> <body> <table> <tr> diff --git a/assets/sai.css b/assets/sai.css index 60109bf..189afb3 100644 --- a/assets/sai.css +++ b/assets/sai.css @@ -1287,3 +1287,7 @@ canvas.power-graph { background: #fff; box-sizing: border-box; } + +.pcon-header-on { + background-color: #79da80; +} diff --git a/assets/sai.js b/assets/sai.js index 2aa519d..dd78704 100644 --- a/assets/sai.js +++ b/assets/sai.js @@ -394,7 +394,7 @@ var lang_zhs = "{" + "\"%{pf}前创建, 创作时间: %{ct}ms \"" + "}}"; -var logs = "", redpend = 0, gitohashi_integ = 0, authd = 0, exptimer, auth_user = "", +var logs = "", redpend = 0, gitohashi_integ = 0, authd = 0, auth_is_admin = 0, exptimer, auth_user = "", logAnsiState = {}, logs_pending = "", lines_pending = "", times_pending = "", ongoing_task_activities = {}, last_log_timestamp = 0, spreadsheet_data_cache = {}, loadreport_data_cache = {}, fadingTasks = new Map(); @@ -426,7 +426,7 @@ function createPconDiv(pcon) { const header = document.createElement("div"); header.className = "pcon-header"; - let stateClass = pcon.on ? "pcon-on" : "pcon-off"; + let stateClass = (pcon.on === 1) ? "pcon-on" : "pcon-off"; let type = pcon.type ? `(${pcon.type})` : ""; header.innerHTML = `<span class="${stateClass}">&#x23FB;</span> <b>${hsanitize(pcon.name)}</b> <span class="pcon-type">${hsanitize(type)}</span>`; @@ -1429,11 +1429,13 @@ function createBuilderDiv(plat) { else { if (!plat.power_managed) platDiv.className += " power-unmanaged"; - else - if (plat.stay_on !== 0) + else { + let pcon = pcon_topology[plat.pcon]; + if (pcon && pcon.manual_on) platDiv.className += " power-stay"; else platDiv.className += " power-stay-dep"; + } } if (plat.powering_up) platDiv.className += " powering-up"; @@ -1462,7 +1464,8 @@ function createBuilderDiv(plat) { `<div class="res-bar"><div class="res-bar-inner res-bar-ram w-0"></div></div>` + `<div class="res-bar"><div class="res-bar-inner res-bar-disk w-0"></div></div>` + `</div>`; - innerHTML += `${hsanitize(plat.peer_ip)}` + " " + hsanitize(plat.stay_on); + if (authd && auth_is_admin && plat.peer_ip) + innerHTML += `<span class="plat-peer-ip" style="font-size: 0.8em; color: #555;">${hsanitize(plat.peer_ip)}</span>`; innerHTML += `</td></tr></tbody></table>`; platDiv.innerHTML = innerHTML; @@ -1481,34 +1484,6 @@ function createBuilderDiv(plat) { { label: `<b>LWS:</b> ${plat.lws_hash}` }, ]; - if (plat.power_managed && authd) { - if (plat.stay_on !== 0) { - menuItems.push({ - label: "Release Stay", - callback: () => { - const stayMsg = { - schema: "com.warmcat.sai.stay", - builder_name: plat.name.split('.')[0], - stay_on: 0 - }; - sai.send(JSON.stringify(stayMsg)); - } - }); - } else { - menuItems.push({ - label: "Stay On", - callback: () => { - const stayMsg = { - schema: "com.warmcat.sai.stay", - builder_name: plat.name.split('.')[0], - stay_on: 1 - }; - sai.send(JSON.stringify(stayMsg)); - } - }); - } - } - platDiv.addEventListener("contextmenu", function(event) { if (!authd) return; @@ -1619,7 +1594,27 @@ function createPconDiv(pcon) { const header = document.createElement("div"); header.className = "pcon-header"; - let stateClass = pcon.on ? "pcon-on" : "pcon-off"; + const myBuilders = last_builder_list.filter(b => b.pcon === pcon.name); + let anyConnected = myBuilders.some(b => b.online === 1); + let anyPoweringUp = myBuilders.some(b => b.powering_up === 1); + + let isActuallyOn = false; + let stateClass = "pcon-off"; + + if (pcon_energy_cache[pcon.name]) { + const d = pcon_energy_cache[pcon.name]; + let hasPower = d.voltage_v >= 70 && d.active_power_w > 0; + isActuallyOn = anyConnected || anyPoweringUp || hasPower; + stateClass = hasPower ? "pcon-on" : "pcon-off"; + } else { + isActuallyOn = anyConnected || (pcon.on === 1 && !anyPoweringUp); + stateClass = (pcon.on === 1) ? "pcon-on" : "pcon-off"; + } + + if (isActuallyOn) { + header.className += " pcon-header-on"; + } + let type = pcon.type ? `(${pcon.type})` : ""; header.innerHTML = `<span class="${stateClass}">&#x23FB;</span> <b>${hsanitize(pcon.name)}</b> <span class="pcon-type">${hsanitize(type)}</span>`; @@ -1649,7 +1644,7 @@ function createPconDiv(pcon) { ]; if (authd) { - if (pcon.on) { + if (isActuallyOn) { menuItems.push({ label: "Turn Off", callback: () => { @@ -2038,7 +2033,7 @@ function ws_open_sai() if (b.pcon && !pcon_topology[b.pcon]) { pcon_topology[b.pcon] = { name: b.pcon, - on: 1, /* Default to on so it shows green if unknown */ + on: 0, /* Default to off so it shows grey until we get real state */ type: "auto-discovered", depends_on: "", children: [] @@ -2090,13 +2085,30 @@ function ws_open_sai() } const d = item; -// stats.textContent = `${d.voltage_v}V ${d.active_power_w}W ${d.current_ma}mA today:${(d.energy_today_wh/1000).toFixed(3)}kWh`; + let hasPower = false; if (d.voltage_v < 70) stats.textContent = "unpowered"; else if (!d.active_power_w) stats.textContent = "OFF"; - else + else { stats.textContent = `${d.active_power_w}W`; + hasPower = true; + } + + let plugIcon = header.querySelector("span"); + if (plugIcon) { + plugIcon.className = hasPower ? "pcon-on" : "pcon-off"; + } + + const myBuilders = last_builder_list.filter(b => b.pcon === item.name); + let anyConnected = myBuilders.some(b => b.online === 1); + let anyPoweringUp = myBuilders.some(b => b.powering_up === 1); + + if (anyConnected || anyPoweringUp || hasPower) { + header.classList.add("pcon-header-on"); + } else { + header.classList.remove("pcon-header-on"); + } } }); @@ -2788,16 +2800,20 @@ window.addEventListener("load", function() { fetch('.lws-login-status') .then(function(res) { return res.json(); }) .then(function(data) { - if (data.logged_in && data.has_grant) + if (data.logged_in && data.has_grant) { authd = 1; + if (data.is_admin) + auth_is_admin = 1; + } }) .catch(function(err) { console.log('lws-login auth fetch failed: ', err); + }) + .finally(function() { + ws_open_sai(); + aging(); }); - ws_open_sai(); - aging(); - setInterval(function() { update_task_activities(); diff --git a/src/builder/b-nspawn.c b/src/builder/b-nspawn.c index 024cf4c..ad60019 100644 --- a/src/builder/b-nspawn.c +++ b/src/builder/b-nspawn.c @@ -137,8 +137,13 @@ callback_sai_stdwsi(struct lws *wsi, enum lws_callback_reasons reason, return -1; } - if (saib_log_chunk_create(op->ns, buf, len, lws_spawn_get_stdfd(wsi))) - return -1; + { + int ch = lws_spawn_get_stdfd(wsi); + if (ch == 0) + ch = 1; + if (saib_log_chunk_create(op->ns, buf, len, ch)) + return -1; + } return lws_ss_request_tx(op->ns->spm->ss) ? -1 : 0; @@ -612,6 +617,7 @@ saib_spawn_script(struct sai_nspawn *ns) info.max_log_lines = 10000; info.timeout_us = 30 * 60 * LWS_US_PER_SEC; info.reap_cb = sai_lsp_reap_cb; + info.pty_mode = 1; memset(&ns->res, 0, sizeof(ns->res)); info.res = &ns->res; #if defined(__linux__) diff --git a/src/common/include/private.h b/src/common/include/private.h index 92aee13..222cedc 100644 --- a/src/common/include/private.h +++ b/src/common/include/private.h @@ -531,9 +531,9 @@ typedef struct sai_plat { unsigned int avail_mem_kib; unsigned int avail_sto_kib; - char windows; - char power_managed; - char stay_on; + unsigned int windows; + unsigned int power_managed; + unsigned int stay_on; char busy; int index; /* used to create unique build dir path */ @@ -604,7 +604,7 @@ typedef struct sai_stay { lws_dll2_t list; char builder_name[64]; char pcon_name[64]; - char stay_on; /* 0 = release, 1 = set */ + unsigned int stay_on; /* 0 = release, 1 = set */ } sai_stay_t; typedef struct sai_controlled_builder { @@ -619,14 +619,15 @@ typedef struct sai_power_controller { char name[64]; char type[32]; char depends_on[64]; - char on; + unsigned int on; + unsigned int manual_on; } sai_power_controller_t; /* sai-power -> sai-server, tells it the builders it can manage */ typedef struct sai_power_managed_builder { lws_dll2_t list; char name[64]; - char stay_on; + unsigned int stay_on; } sai_power_managed_builder_t; typedef struct sai_power_managed_builders { @@ -639,7 +640,7 @@ typedef struct sai_power_managed_builders { typedef struct sai_stay_state_update { lws_dll2_t list; char builder_name[64]; - char stay_on; + unsigned int stay_on; } sai_stay_state_update_t; /* Builder -> sai-power registration */ @@ -699,7 +700,7 @@ extern const lws_struct_map_t lsm_power_managed_builder[2], lsm_power_managed_builders_list[2], lsm_schema_power_managed_builders[1], - lsm_power_controller[5], + lsm_power_controller[6], lsm_schema_json_map_task[], lsm_schema_sq3_map_task[], lsm_schema_sq3_map_event[], diff --git a/src/common/struct-metadata.c b/src/common/struct-metadata.c index 845c29c..85b6f9a 100644 --- a/src/common/struct-metadata.c +++ b/src/common/struct-metadata.c @@ -324,6 +324,7 @@ const lws_struct_map_t lsm_power_controller[] = { LSM_CARRAY(sai_power_controller_t, type, "type"), LSM_CARRAY(sai_power_controller_t, depends_on, "depends_on"), LSM_UNSIGNED(sai_power_controller_t, on, "on"), + LSM_UNSIGNED(sai_power_controller_t, manual_on, "manual_on"), LSM_LIST(sai_power_controller_t, controlled_builders_owner, sai_controlled_builder_t, list, NULL, lsm_controlled_builder, "controlled_builders"), diff --git a/src/power/p-http-api.c b/src/power/p-http-api.c index b1db6c8..45d70ee 100644 --- a/src/power/p-http-api.c +++ b/src/power/p-http-api.c @@ -140,7 +140,7 @@ saip_notify_server_stay_state(const char *builder_name, int stay_on) memset(&ssu, 0, sizeof(ssu)); lws_strncpy(ssu.builder_name, builder_name, sizeof(ssu.builder_name)); - ssu.stay_on = (char)stay_on; + ssu.stay_on = (uint8_t)stay_on; sai_ss_serialize_queue_helper(sps->ss, &m->bl_pwr_to_srv, lsm_schema_stay_state_update, diff --git a/src/power/p-ws-server.c b/src/power/p-ws-server.c index 6ad2e64..99f69e3 100644 --- a/src/power/p-ws-server.c +++ b/src/power/p-ws-server.c @@ -149,7 +149,8 @@ saip_queue_stay_info(saip_server_t *sps) if (pc1 && pc->name) { lws_strncpy(pc1->name, pc->name, sizeof(pc1->name)); - pc1->on = pc->on; + pc1->on = (unsigned int)pc->on; + pc1->manual_on = !!(pc->flags & SAIP_PCON_F_MANUAL_STAY); if (pc->depends_on) lws_strncpy(pc1->depends_on, pc->depends_on, sizeof(pc1->depends_on)); if (pc->type) diff --git a/src/server/s-central.c b/src/server/s-central.c index 802a062..122204b 100644 --- a/src/server/s-central.c +++ b/src/server/s-central.c @@ -161,6 +161,11 @@ sais_ensure_tables(struct vhd *vhd) /* lwsl_notice("%s: (stateless) %s\n", __func__, err); */ sqlite3_free(err); } + sqlite3_exec(vhd->server.pdb, "ALTER TABLE power_controllers ADD COLUMN manual_on integer;", NULL, NULL, &err); + if (err) { + /* lwsl_notice("%s: (stateless) %s\n", __func__, err); */ + sqlite3_free(err); + } sai_sqlite3_statement(vhd->server.pdb, "CREATE TABLE IF NOT EXISTS power_controllers (" @@ -168,7 +173,8 @@ sais_ensure_tables(struct vhd *vhd) " type varchar(32)," " url varchar(128)," " depends_on varchar(64)," - " state integer" + " state integer," + " manual_on integer" ");", "create pcon table"); sai_sqlite3_statement(vhd->server.pdb, diff --git a/src/server/s-power.c b/src/server/s-power.c index 0546b7e..dac5982 100644 --- a/src/server/s-power.c +++ b/src/server/s-power.c @@ -151,8 +151,8 @@ sais_power_rx(struct vhd *vhd, struct pss *pss, uint8_t *buf, /* Insert PCON */ lws_snprintf(q, sizeof(q), - "INSERT OR REPLACE INTO power_controllers (name, type, url, depends_on, state) VALUES ('%s', '%s', '', '%s', %d)", - pc->name, pc->type, pc->depends_on, pc->on); + "INSERT OR REPLACE INTO power_controllers (name, type, url, depends_on, state, manual_on) VALUES ('%s', '%s', '', '%s', %d, %d)", + pc->name, pc->type, pc->depends_on, pc->on, pc->manual_on); sai_sqlite3_statement(vhd->server.pdb, q, "insert pcon"); /* Insert Controlled Builders */ diff --git a/src/server/s-ws-web.c b/src/server/s-ws-web.c index 0584aa5..70377d9 100644 --- a/src/server/s-ws-web.c +++ b/src/server/s-ws-web.c @@ -192,7 +192,7 @@ sais_list_pcons(struct vhd *vhd) * complex inner/outer lws_struct_sq3 memory management edge cases that truncates rows. */ lws_snprintf(pcon_query, sizeof(pcon_query), - "SELECT name, type, depends_on, state FROM power_controllers ORDER BY name LIMIT 100"); + "SELECT name, type, depends_on, state, manual_on FROM power_controllers ORDER BY name LIMIT 100"); if (sqlite3_prepare_v2(vhd->server.pdb, pcon_query, -1, &pcon_stmt, NULL) == SQLITE_OK) { while (sqlite3_step(pcon_stmt) == SQLITE_ROW) { @@ -208,7 +208,8 @@ sais_list_pcons(struct vhd *vhd) const char *depends_on = (const char *)sqlite3_column_text(pcon_stmt, 2); if (depends_on) lws_strncpy(pc->depends_on, depends_on, sizeof(pc->depends_on)); - pc->on = (char)sqlite3_column_int(pcon_stmt, 3); + pc->on = (unsigned int)sqlite3_column_int(pcon_stmt, 3); + pc->manual_on = (unsigned int)sqlite3_column_int(pcon_stmt, 4); lws_dll2_add_tail(&pc->list, &pmb.power_controllers); } @@ -350,7 +351,8 @@ sais_list_builders(struct vhd *vhd) if ((!strncmp(sp->name, ps->host, host_len) && sp->name[host_len] == '.') || (pl > host_len && - !strncmp(sp->name + (pl - host_len), ps->host, host_len))) + !strncmp(sp->name + (pl - host_len), ps->host, host_len)) || + (sp->pcon && !strcmp(sp->pcon, ps->host))) { lwsl_notice("%s: %s vs %s, sp->online %d, pup %d, pdwn %d\n", __func__, sp->name, ps->host, sp->online, ps->powering_up, ps->powering_down); /*
Page fetched 0s ago, creation time: 8ms (vhost etag hits: 0%, cache hits: 0%)