aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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