diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-07 22:17:27 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-07 22:22:51 +0200 |
| commit | b06338abf7a5be58151828acb801286c05a3d62a (patch) | |
| tree | 32ba77301f9f8f798f6aba953ce02fd9b5a57235 /packages/meshbay-node | |
| parent | e0dde3466fcb06570cc41b1c379707b2807040ad (diff) | |
| download | meshbay-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/meshbay-node')
| -rw-r--r-- | packages/meshbay-node/tests/test_e2e_windows_app.py | 40 | ||||
| -rw-r--r-- | packages/meshbay-node/tests/test_packaging_win.py | 22 |
2 files changed, 56 insertions, 6 deletions
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 |