From e89a57bb97b5a0d624e8d490b6b8aa38ba140817 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sat, 5 Sep 2026 13:06:55 +0200 Subject: fix(win): graceful shutdown, one startup-mode control, and a stray-\r bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows-only changes, all found by actually running the previous session's work rather than by review alone: - CTRL_CLOSE_EVENT/LOGOFF/SHUTDOWN handler (platform.py, ctypes SetConsoleCtrlHandler) so closing a console window, signing off, or a system shutdown runs the daemon's real _shutdown() instead of Windows just ending the process — closing WebRTC sessions and any in-flight ffmpeg transcode instead of orphaning it. `taskkill /F` itself stays uncatchable (like SIGKILL), so autostart_run() now spawns with CREATE_NEW_PROCESS_GROUP instead of DETACHED_PROCESS and autostart_end() tries CTRL_BREAK_EVENT against the recorded pid first, falling back to the hard kill only if that doesn't stop it in time. - Replaced the Node page's two independent autostart/service-mode toggles with one "start automatically" select (off / at sign-in / as a background service). The old pair let both be active at once — starting the daemon twice, at boot and at sign-in — and their layout broke wrapping inside .node-service's flex row. The new control always removes whichever mechanism is active before installing the target; platform.py's service_install() does the same on the CLI side. The "background service" option disables itself (with a hint pointing at the CLI) when running unpackaged, since service-mode.ps1/service.ps1/firewall.ps1 all assume an installed build's layout — verified live rather than assumed by actually running those scripts unelevated. - findNodeBinary() no longer bakes a stray \r into resolved paths. Found by rebooting after enabling per-user autostart: where.exe listed two matches, and stdout.trim().split('\n')[0] only strips the whole string's ends, leaving line one's own trailing \r attached — which landed inside the Startup .vbs's quoted path and broke it with "Unterminated string constant" at boot. Fixed by splitting on \r?\n and trimming every line. - Dependency audit for the Windows installer (docs/WINDOWS-PORT.md): no VC++ Redistributable needed, confirmed by inspecting the built node-runtime's actual import table rather than assuming. New docs/windows-build.md: a concise clone-to-installer build guide. Co-Authored-By: Claude Sonnet 5 --- .../src/meshbay_hub/static/node-page.js | 111 +++++++++++++-------- 1 file changed, 67 insertions(+), 44 deletions(-) (limited to 'packages/meshbay-hub/src/meshbay_hub/static/node-page.js') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/node-page.js b/packages/meshbay-hub/src/meshbay_hub/static/node-page.js index 2a0b0d7..ec73128 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/node-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/node-page.js @@ -47,21 +47,35 @@ function NodeServicePanel({ onChanged }) { } }, [refresh, onChanged]); - const toggleAutostart = useCallback(() => { - act('autostart', () => (info && info.autostart - ? platform.node.autostart.remove() - : platform.node.autostart.install())); - }, [act, info]); + // "off" / "signin" / "service" -- derived from the status payload, no new + // backend field needed: mode/autostart already distinguish all three. + const startupMode = (i) => { + if (!i) return 'off'; + if (i.mode === 'service') return i.mode; + return i.autostart ? 'signin' : 'off'; + }; // Switching mode itself — the installer's own choice is effectively one-shot // (it skips the question once the firewall rules exist for any reason, and // per-user mode sets those up on its own with no Scheduled Task), so this is // the only way back in if service mode was declined, or out if it is no // longer wanted. One elevation, task + firewall together, same script. - const toggleServiceMode = useCallback(() => { - act('serviceMode', () => (info && info.mode === 'service' - ? platform.node.serviceMode.remove() - : platform.node.serviceMode.install())); + // + // The two mechanisms are mutually exclusive by construction here: never + // both installed at once, which would start the daemon twice (once at + // boot via the Scheduled Task, again at sign-in via the Startup .vbs). + // Always remove whichever one is currently active before installing the + // target, so every transition -- not just the two that used to be + // separate toggles -- keeps that invariant. + const changeStartupMode = useCallback((target) => { + const current = startupMode(info); + if (target === current) return; + act('startupMode', async () => { + if (current === 'service') await platform.node.serviceMode.remove(); + else if (current === 'signin') await platform.node.autostart.remove(); + if (target === 'service') await platform.node.serviceMode.install(); + else if (target === 'signin') await platform.node.autostart.install(); + }); }, [act, info]); if (!platform.node.service.available) return null; @@ -79,44 +93,53 @@ function NodeServicePanel({ onChanged }) { const label = info.installed ? t('node.service_state_' + stateKey) : t('node.service_not_installed'); + // Own row, below the status/actions card rather than a further item + // crammed into its flex-wrap line -- that (plus two independent toggles + // for what is really one choice) is what made this a mess before. + const showStartupRow = (platform.node.autostart.available + || platform.node.serviceMode.available) && typeof info.mode === 'string'; + return html` -
-
- - ${label} -
- ${err && html`
${err}
`} -
- - ${info.installed && html` - - +
+
+
+ + ${label} +
+ ${err && html`
${err}
`} +
+ + ${info.installed && html` + + + `} +
+ ${info.mode === 'service' && html` +

${t('node.service_mode_hint')}

`}
- ${info.mode === 'service' && html` -

${t('node.service_mode_hint')}

- `} - ${platform.node.autostart.available && typeof info.autostart === 'boolean' && html` - - `} - ${platform.node.serviceMode.available && typeof info.mode === 'string' && html` - + ${showStartupRow && html` +
+ ${t('node.startup_mode_label')} + +
+ ${busy === 'startupMode' && html` +

${t('node.startup_mode_updating')}

`} + ${!info.canElevate && html` +

${t('node.startup_mode_service_unavailable_hint')}

`} `}
`; } -- cgit v1.2.3