summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-14 23:51:36 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-14 23:51:36 +0200
commite12570f9d1aa2645e6bb223b1417fa0e81957b65 (patch)
treed3eda196ed925dc4fbedcb7671b38bd1f05f552d /packages/meshbay-node
parentcd2745cecff12e894e0dfa702bff6a90f0e8734e (diff)
downloadmeshbay-e12570f9d1aa2645e6bb223b1417fa0e81957b65.tar.gz
fix(win): a service-mode daemon can be replaced, and the Node page can link one
Two live-reproduced bugs in Windows node start/stop, found sideloading the 0.14.0 build: - node:start's crash-recovery step killed a service-mode daemon with taskkill/CTRL_BREAK, both of which fail with "Access is denied" against a process running under the Scheduled Task's own S4U logon session (a different session from the Electron app's). The daemon it was meant to replace just kept running, unreplaced, and schtasks /run on a task Windows still considered Running was then a silent no-op too. Route through winServiceTaskEnd() (schtasks /end) first, the way nodeServiceStop/ nodeServiceRestart already correctly do. service-mode.ps1 also now starts the task right after registering it -- Register-ScheduledTask's own AtStartup trigger does not run it immediately, so nothing was listening until the next reboot. - The Node page's Start button called node.start() with no arguments, so an unlinked node (a fresh install, or one whose hub-side link was lost) could never link on Start alone -- only create-group-page.js's own call passed {hubUrl, username, token}. Reproduced on a fresh non-service install signed in to the real hub: Start hung for ~105s and failed with "could not link", pointing at a "Link Node" control that lives on Settings, not the Node page (that message is fixed too). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node')
-rw-r--r--packages/meshbay-node/tests/test_packaging_win.py73
1 files changed, 73 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_packaging_win.py b/packages/meshbay-node/tests/test_packaging_win.py
index 6f7573b..877994d 100644
--- a/packages/meshbay-node/tests/test_packaging_win.py
+++ b/packages/meshbay-node/tests/test_packaging_win.py
@@ -1216,3 +1216,76 @@ def test_node_startup_hint_key_exists_in_all_ten_locales():
for name in ("en", "fr", "es", "pt-BR", "zh-CN", "ja", "de", "it", "nl", "pl"):
cat = (HUB_STATIC / "locales" / f"{name}.js").read_text(encoding="utf-8")
assert "'setup.node_startup_hint':" in cat, f"{name}.js is missing the key"
+
+
+def test_service_mode_ps1_starts_the_task_after_installing_it():
+ """
+ Register-ScheduledTask -Trigger AtStartup registers a boot trigger; it
+ does not run the task now. Neither the installer's "background service"
+ choice nor the Node page's later toggle followed up with `service.ps1
+ run` -- reproduced live 2026-09-14 on a fresh install: the task existed,
+ Task Scheduler agreed it was installed, and nothing was listening on
+ 18000 until either a reboot or a separate manual Start. Both callers
+ share this one elevated script, so the fix belongs here, once.
+
+ Must run only after a successful install, and never on `remove` -- a
+ script with no failure branch that always called `run` would restart an
+ already-running task pointlessly on every mode switch away from service.
+ """
+ src = (WIN / "service-mode.ps1").read_text(encoding="utf-8")
+ install_branch, remove_branch = src.split('$Action -eq "install"', 1)[1], None
+ assert '"run"' in install_branch or "'run'" in install_branch, (
+ "service-mode.ps1 registers the task but never starts it -- the "
+ "daemon stays down until the next reboot")
+ # The run step must be conditioned on install having actually succeeded,
+ # not fired unconditionally regardless of $Action.
+ guard_line = src[src.index('$Action -eq "install"') - 40:src.index('$Action -eq "install"') + 40]
+ assert "-and" in guard_line or "-not $failed" in install_branch, (
+ "the follow-up `run` must be gated on Action=install and success, "
+ "not run unconditionally on every invocation including remove")
+
+
+def test_node_start_ends_a_service_mode_daemon_via_task_scheduler_not_taskkill():
+ """
+ A service-mode daemon runs under the Scheduled Task's own S4U logon
+ session, a different one from the Electron app's. killNodeProcesses()
+ signals or taskkills by image name/pid from THIS session -- reproduced
+ live 2026-09-14: `taskkill /IM meshbay-node.exe /F` against the exact
+ live pid of an S4U-launched "MeshBay Node" task instance answered
+ "Access is denied", while `schtasks /end /tn "MeshBay Node"` against the
+ same pid, from the same unelevated shell, succeeded immediately -- Task
+ Scheduler holds the authority to stop what it started; this process does
+ not.
+
+ killNodeProcesses()'s own error is swallowed (fire-and-forget, `() =>
+ resolve()`), so the old bug was silent: node:start's "clear a
+ crash-looping one" step did nothing to a stuck service-mode instance,
+ and the winServiceTaskRun() that followed was then a no-op too (a task
+ Windows still considers Running does not get a second concurrent
+ instance under the default multiple-instances policy). The daemon a
+ broken Start was supposed to replace just kept running, unreplaced,
+ until a reboot. nodeServiceStop/nodeServiceRestart already route through
+ winServiceTaskEnd() first for exactly this reason -- node:start must too.
+ """
+ main_js = MAIN_JS.read_text(encoding="utf-8")
+ body = main_js.split("ipcMain.handle('node:start'", 1)[1]
+ body = body[:body.index("ipcMain.handle(")]
+ win_branch = body.split("process.platform === 'win32'", 1)[1]
+ win_branch = win_branch[:win_branch.index("process.platform !== 'linux'")]
+
+ svc_installed = win_branch.split("if (svc.installed) {", 1)[1]
+ svc_installed = svc_installed[:svc_installed.index("} else {")]
+ assert "winServiceTaskEnd" in svc_installed, (
+ "the service-mode branch of node:start never calls winServiceTaskEnd() "
+ "-- a stuck S4U-session daemon can't be reached by killNodeProcesses() "
+ "(Access is denied, confirmed live) so it never actually gets replaced")
+ assert svc_installed.index("winServiceTaskEnd") < svc_installed.index("winServiceTaskRun"), (
+ "winServiceTaskEnd() must run before winServiceTaskRun() -- ending "
+ "second would stop the fresh instance right after starting it")
+
+ svc_else = win_branch.split("} else {", 1)[1]
+ svc_else = svc_else[:svc_else.index("const p = await waitForNode")]
+ assert "killNodeProcesses" in svc_else, (
+ "the non-service branch (Startup mode / only-while-open) should still "
+ "use killNodeProcesses() -- that daemon runs in this same session, "
+ "where taskkill/CTRL_BREAK actually work")