aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-07 22:17:27 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-07 22:22:51 +0200
commitb06338abf7a5be58151828acb801286c05a3d62a (patch)
tree32ba77301f9f8f798f6aba953ce02fd9b5a57235 /packages
parente0dde3466fcb06570cc41b1c379707b2807040ad (diff)
downloadmeshbay-b06338abf7a5be58151828acb801286c05a3d62a.tar.gz
fix(client): stop the node at Quit in "only while open", whoever started it
Switching from "at sign-in" to "only while MeshBay is open" left the node the sign-in launcher had started running after Quit: only a node this process had started was stopped. In that mode the app owns the node, so Quit stops the one that is there. The start with the app and the sign-in's own start (ensureNode) also both ran `autostart start` at launch -- three meshbay-node.exe were seen racing for the port. The sign-in's start and node:start now wait for the launch's. The end-to-end test covers the mode: Quit leaves no node, opening the app starts one. It launches the app with the environment it was imported with: the suite's conftest points HOME, USERPROFILE, LOCALAPPDATA and APPDATA at a throwaway directory per test, and the app started under that crashed at once (0x80000003), which first looked like a crash of the app itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages')
-rw-r--r--packages/meshbay-client/src/main.js31
-rw-r--r--packages/meshbay-node/tests/test_e2e_windows_app.py40
-rw-r--r--packages/meshbay-node/tests/test_packaging_win.py22
3 files changed, 81 insertions, 12 deletions
diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js
index 8e987d3..f01b8f2 100644
--- a/packages/meshbay-client/src/main.js
+++ b/packages/meshbay-client/src/main.js
@@ -558,9 +558,9 @@ let trayTimer = null;
// there already do what hiding to an indicator does elsewhere.
const trayOS = () => process.platform === 'linux' || process.platform === 'win32';
let nodeService = null; // assigned by registerBridge()
-// Whether this app started the node that runs now, outside service mode: in
-// the installer's "only while MeshBay is open" mode that is the node it stops
-// when it quits.
+// Whether this app started the node that runs now, outside service mode. In
+// "only while MeshBay is open" the node is stopped at Quit whoever started it;
+// this only saves asking whether one is there.
let nodeStartedByApp = false;
let nodeWithApp = null; // assigned by registerBridge()
@@ -1537,6 +1537,11 @@ function registerBridge() {
// each mode of a real install. A node not yet set up (no account in
// node.toml) is left alone -- node:start provisions and starts it.
async function winStartNodeWithApp() {
+ if (!nodeWithAppStart) nodeWithAppStart = winStartNodeWithAppOnce();
+ return nodeWithAppStart;
+ }
+
+ async function winStartNodeWithAppOnce() {
if (process.platform !== 'win32' || !hasBundledNode() || !nodeProvisioned()) return;
if ((await winStartupMode()) !== 'open' || await probeNode()) return;
try {
@@ -1546,15 +1551,27 @@ function registerBridge() {
console.error('[node] start with the app:', err.message);
}
}
+ // The start at launch, while it runs: the sign-in's own start (ensureNode)
+ // and node:start wait for it. Both used to run `autostart start` at once,
+ // and three meshbay-node.exe were seen racing for the port.
+ let nodeWithAppStart = null;
nodeWithApp = { start: winStartNodeWithApp };
+ // In "only while MeshBay is open" the app owns the node, whoever started it:
+ // one left by the sign-in launcher before the mode was switched, or started
+ // from a terminal, ran on after Quit because only a node this process had
+ // started was stopped (found switching modes on a real install).
let nodeStopAtQuitDone = false;
app.on('before-quit', (event) => {
- if (process.platform !== 'win32' || nodeStopAtQuitDone || !nodeStartedByApp) return;
+ if (process.platform !== 'win32' || nodeStopAtQuitDone || !hasBundledNode()) return;
event.preventDefault(); // before-quit waits for no promise
nodeStopAtQuitDone = true;
- winStartupMode()
- .then((mode) => (mode === 'open' ? killNodeProcesses() : null))
+ (async () => {
+ if ((await winStartupMode()) !== 'open') return;
+ if (nodeStartedByApp || await probeNode() || (await winNodePids()).length) {
+ await killNodeProcesses();
+ }
+ })()
.catch((err) => console.error('[node] stop at quit:', err.message))
.finally(() => app.quit());
});
@@ -2033,6 +2050,7 @@ Its log: ${nodeLogHint()}`);
async function ensureNode(opts) {
if (process.platform !== 'win32' || !hasBundledNode()) return { state: 'unsupported' };
+ if (nodeWithAppStart) await nodeWithAppStart.catch(() => {});
const hubUrl = String(config.hubBase || '').replace(/\/+$/, '');
const username = String((opts && opts.username) || '');
const token = opts && opts.token;
@@ -2083,6 +2101,7 @@ Its log: ${nodeLogHint()}`);
handle('node:start', async (_e, opts) => {
// One at a time: a start racing the sign-in's own would stop the node the
// other had just started.
+ if (nodeWithAppStart) await nodeWithAppStart.catch(() => {});
if (nodeEnsuring) await nodeEnsuring.catch(() => {});
const already = await probeNode();
if (already && already.status === 'running') {
diff --git a/packages/meshbay-node/tests/test_e2e_windows_app.py b/packages/meshbay-node/tests/test_e2e_windows_app.py
index 86e6a62..da1d251 100644
--- a/packages/meshbay-node/tests/test_e2e_windows_app.py
+++ b/packages/meshbay-node/tests/test_e2e_windows_app.py
@@ -46,6 +46,12 @@ pytestmark = [
pytest.mark.timeout(900),
]
+# The environment as it is now, at import: the suite's conftest points HOME,
+# USERPROFILE, LOCALAPPDATA and APPDATA at a throwaway directory for every test,
+# and an application started under that crashes at once (0x80000003). The
+# module-scoped fixture launched before it applied; the relaunch in the test
+# body did not, which looked like a crash of the application.
+REAL_ENV = dict(os.environ)
LOCALAPPDATA = Path(os.environ.get("LOCALAPPDATA", ""))
APPDATA = Path(os.environ.get("APPDATA", ""))
INSTALL = Path(os.environ.get("MESHBAY_E2E_INSTALL", LOCALAPPDATA / "Programs" / "MeshBay"))
@@ -62,8 +68,11 @@ def _free_port() -> int:
def _launch(argv: list[str]) -> None:
- """Start a process detached: DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP."""
- subprocess.Popen(argv, creationflags=0x00000008 | 0x00000200, close_fds=True)
+ """Start the application detached, in the real profile."""
+ log = Path(REAL_ENV.get("TEMP", ".")) / "mb-e2e-app.log" # its console, for a failure
+ subprocess.Popen(argv, creationflags=0x00000008 | 0x00000200, close_fds=True,
+ stdout=log.open("a"), stderr=subprocess.STDOUT,
+ env={**REAL_ENV, "ELECTRON_ENABLE_LOGGING": "1"})
def _node_processes() -> dict[int, int]:
@@ -150,8 +159,16 @@ class Page:
_wait(f"a control named {label!r}",
lambda: _try(lambda: self.eval(f"(clickText({json.dumps(label)}), 1)")), timeout)
+ def quit(self) -> None:
+ """Quit the application the way its own Quit does (before-quit runs)."""
+ version = json.loads(_http_get(f"http://127.0.0.1:{CDP_PORT}/json/version"))
+ from websockets.sync.client import connect
+ with connect(version["webSocketDebuggerUrl"]) as browser:
+ browser.send(json.dumps({"id": 1, "method": "Browser.close"}))
+ _try(lambda: browser.recv(timeout=5))
+
def close(self):
- self._conn.__exit__(None, None, None)
+ _try(lambda: self._conn.__exit__(None, None, None))
def _http_get(url: str, token: str | None = None) -> str:
@@ -301,3 +318,20 @@ def test_a_fresh_account_gets_its_node_set_up_and_can_stop_it(app, hub):
s.get("status") == "running" and s.get("pid") != first.get("pid") and s, 120)
assert len(_node_pids()) == 1, _node_pids()
assert second["pk_node_ed25519"] == first["pk_node_ed25519"]
+
+ if MODE == "open":
+ # Only while MeshBay is open: Quit stops the node, opening starts it.
+ page.quit()
+ _wait("no node after Quit", lambda: not _node_pids(), 60)
+ # A second instance started while the first is still exiting defers
+ # to it (single-instance lock) and quits.
+ _wait("the application gone", lambda: b"MeshBay.exe" not in subprocess.run(
+ ["tasklist", "/FI", "IMAGENAME eq MeshBay.exe", "/NH"],
+ capture_output=True).stdout, 60)
+ _launch([str(INSTALL / "MeshBay.exe"), f"--remote-debugging-port={CDP_PORT}"])
+ again = _wait("the application window again", lambda: _try(Page), 60)
+ again.close()
+ _wait("a node started with the app", lambda: (s := _node_status()) and
+ s.get("status") == "running", 120)
+ # The CLI that started it is meshbay-node.exe too, for a moment.
+ _wait("a single meshbay-node.exe", lambda: len(_node_pids()) == 1, 20)
diff --git a/packages/meshbay-node/tests/test_packaging_win.py b/packages/meshbay-node/tests/test_packaging_win.py
index 1fe6be8..b5f6191 100644
--- a/packages/meshbay-node/tests/test_packaging_win.py
+++ b/packages/meshbay-node/tests/test_packaging_win.py
@@ -1625,11 +1625,27 @@ def test_a_declined_prompt_leaves_the_node_as_it_was():
def test_only_while_open_starts_with_the_app_and_stops_at_quit():
main_js = MAIN_JS.read_text(encoding="utf-8")
- start = _fn_body(main_js, "async function winStartNodeWithApp()")
+ start = _fn_body(main_js, "async function winStartNodeWithAppOnce()")
assert "winStartupMode()) !== 'open'" in start and "nodeProvisioned()" in start
assert "winNodeStartVia(['autostart', 'start'])" in start
ready = main_js.split("app.whenReady().then(", 1)[1]
assert ready.index("createWindow();") < ready.index("nodeWithApp.start()")
quit_ = main_js.split("app.on('before-quit', (event) => {", 1)[1].split("\n });", 1)[0]
- assert "!nodeStartedByApp" in quit_ and "event.preventDefault()" in quit_
- assert "mode === 'open' ? killNodeProcesses()" in quit_
+ assert "event.preventDefault()" in quit_
+ assert "(await winStartupMode()) !== 'open'" in quit_ and "killNodeProcesses()" in quit_
+ # Whoever started it: a node left by the sign-in launcher before the mode
+ # was switched ran on after Quit (found on a real install, 2026-10-08).
+ assert "!nodeStartedByApp" not in quit_
+ assert "await probeNode() || (await winNodePids()).length" in quit_
+
+
+def test_one_start_at_a_time_at_launch():
+ """The start with the app and the sign-in's own both ran `autostart start`
+ at once: three meshbay-node.exe were seen racing for the port."""
+ main_js = MAIN_JS.read_text(encoding="utf-8")
+ once = _fn_body(main_js, "async function winStartNodeWithApp()")
+ assert "if (!nodeWithAppStart) nodeWithAppStart = winStartNodeWithAppOnce();" in once
+ ensure = _fn_body(main_js, "async function ensureNode(opts)")
+ assert ensure.index("await nodeWithAppStart") < ensure.index("provisionNode(")
+ start = main_js.split("handle('node:start'", 1)[1][:600]
+ assert "await nodeWithAppStart" in start