diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-12 15:48:03 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-12 15:48:22 +0200 |
| commit | 9cc2909cb4a360c81b471ceab1d9578a7655a88e (patch) | |
| tree | da6da7ff43eed10382e72247ef7c84b142a42ca5 /packages/meshbay-client/src | |
| parent | d8885c8df17c60927cb8d1f77ce1745814c6d3b4 (diff) | |
| download | meshbay-9cc2909cb4a360c81b471ceab1d9578a7655a88e.tar.gz | |
fix(packaging): three MSIX first-run regressions found by a real sideload
A second-machine sideload of the MSIX target surfaced three things the
earlier verification round (which only proved the package installs and
runs) had missed:
1. meshbay-node missing from PATH. installer.nsh's customInstall adds
node-runtime\ to HKCU\Environment at install time -- an unelevated
per-user write, never blocked by MSIX's no-elevation rule, only by the
more basic fact that an AppX/MSIX install runs no custom code at all.
packaging/win/ensure-node-path.ps1 (idempotent, no admin verb) plus
main.js's winEnsureNodeOnPath() do it from the app itself instead, once
per launch, shipped to Full and MSIX (not Light, nothing to add there).
Verified live via the Node inspector protocol: the entry was in
HKCU\Environment\Path after a launch, absent before.
2. A daemon that crashes on startup failed silently. spawnNodeDetached()
used stdio: 'ignore', so a real crash reproduced live (a second instance
colliding with the first on 127.0.0.1:18000) left waitForNode()'s
generic 60s timeout as the only failure ever shown. spawnNodeDetachedWatched()
pipes stdio and watches ~2.5s, rejecting immediately with the daemon's
own stderr on an early exit; a survivor has its streams released and
runs fully detached exactly as before. First version bounded the
captured text by line count and a live test showed that cut the actual
OSError line -- two uvicorn/asyncio tracebacks followed it in the real
capture -- so it is bounded by characters instead.
3. No hint that a startup-mode choice exists. The install-time radio page
was the only place this was ever offered, and nothing replaces it now
that no install-time page can exist at all. SetupWelcome (the existing
first-run banner) grew a conditional hint, shown only while a bundled
node is present and neither autostart nor service mode is configured
yet. Considered and rejected: linking straight to the Node page -- its
route is gated on a linked hub node key, false on the exact fresh-install
screen this hint targets, so the link would have been dead on arrival.
New key setup.node_startup_hint, added to all ten locale catalogues.
test_packaging_win.py gained six tests pinning all three (69 total).
Full plan and verification detail: C:\Users\admin\devel\msix-installer.md
section 13 (out of repo).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-client/src')
| -rw-r--r-- | packages/meshbay-client/src/main.js | 92 |
1 files changed, 83 insertions, 9 deletions
diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js index d7e3e7b..bebe144 100644 --- a/packages/meshbay-client/src/main.js +++ b/packages/meshbay-client/src/main.js @@ -1000,6 +1000,29 @@ function registerBridge() { return true; } + // build/installer.nsh's customInstall adds node-runtime\ to the per-user + // PATH at install time (HKCU\Environment, no elevation needed for that -- + // it never was the elevation that blocked it here). An AppX/MSIX install + // has no install-time hook at all, so `meshbay-node` in a terminal simply + // never got added for that target -- a real regression found by actually + // running a sideloaded build, not a theoretical gap. Idempotent (the script + // itself checks first) and harmless to call on every launch, NSIS Full + // included, where it is normally already a no-op. Fire-and-forget: a + // terminal convenience is not worth blocking startup or surfacing an error + // dialog over. + function winEnsureNodeOnPath() { + if (process.platform !== 'win32' || !hasBundledNode()) return; + const script = path.join(process.resourcesPath, 'ensure-node-path.ps1'); + if (!fs.existsSync(script)) return; // dev run, or an older build without it + const nodeDir = path.join(process.resourcesPath, 'node-runtime'); + execFile(MB_PWSH, + ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', script, '-NodeDir', nodeDir], + (err, stdout) => { + if (err) { console.error('[path] ensure-node-path.ps1 failed:', err.message); return; } + console.log('[path] node-runtime on PATH:', (stdout || '').trim()); + }); + } + function findNodeBinary() { if (process.platform === 'win32') { // A packaged Windows build carries the frozen daemon as an @@ -1166,18 +1189,67 @@ function registerBridge() { }); } + // A daemon that crashes immediately (a port already in use -- reproduced + // live: a second node instance found 18000 taken by the first -- a corrupt + // config, antivirus interference) used to fail silently: stdio was + // 'ignore', so its stderr was thrown away, and the only failure path left + // was the caller's waitForNode() timing out after a generic 60s ("did not + // start within 60s"). The real reason was sitting on stderr the whole time, + // just never read. This watches for a few seconds -- long enough for any + // startup crash, reproduced consistently well under one second -- and + // rejects with the daemon's own tail of stderr if it exits in that window. + // If it survives the window, stdio is released and it is left fully + // detached, same as before this existed. + const NODE_CRASH_WATCH_MS = 2500; + + function spawnNodeDetachedWatched(bin, args = []) { + return new Promise((resolve, reject) => { + const child = spawn(bin, args, { + detached: true, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, + }); + let stderr = ''; + let settled = false; + child.stderr.on('data', (d) => { stderr += d.toString(); }); + // spawn() failures (bad path, a stale PATH entry, antivirus + // interference) land on the ChildProcess as an 'error' event, + // asynchronously -- with no listener, Node rethrows it as an uncaught + // exception and takes the whole main process down with it. + child.on('error', (err) => { + if (settled) return; + settled = true; + reject(err); + }); + child.on('exit', (code, signal) => { + if (settled) return; + settled = true; + // By lines (last 8) at first cut the actual OSError -- a real crash + // captured live logged the bind failure, then two separate uvicorn/ + // asyncio tracebacks *after* it, which pushed it out of a short tail. + // Character-bounded instead: Python's own daemon rarely writes more + // than a couple of screens on a startup crash, so keeping the last + // stretch of raw text is far more likely to still include the one + // line that actually says what went wrong than guessing a line count. + let tail = stderr.trim(); + if (tail.length > 4000) tail = `…${tail.slice(-4000)}`; + reject(new Error( + `meshbay-node exited immediately (code ${code}${signal ? `, signal ${signal}` : ''})` + + (tail ? `:\n${tail}` : ''))); + }); + setTimeout(() => { + if (settled) return; + settled = true; + child.stdout.destroy(); + child.stderr.destroy(); + child.unref(); + resolve(); + }, NODE_CRASH_WATCH_MS); + }); + } + async function spawnNodeDetached() { const bin = await findNodeBinary(); if (!bin) throw new Error('meshbay-node not found on PATH'); - const child = spawn(bin, [], { detached: true, stdio: 'ignore', windowsHide: true }); - // spawn() failures (bad path, a stale PATH entry, antivirus interference) - // land on the ChildProcess as an 'error' event, asynchronously -- with no - // listener, Node rethrows it as an uncaught exception and takes the whole - // main process down with it. The caller's waitForNode() timeout already - // turns "never came up" into a clean message; this only has to keep that - // path reachable instead of crashing first. - child.on('error', (err) => console.error('[node] failed to start:', err.message)); - child.unref(); + await spawnNodeDetachedWatched(bin); } async function waitForNode(deadline) { @@ -1724,6 +1796,8 @@ function registerBridge() { await castChromecast.disconnect(); return true; }); + + winEnsureNodeOnPath(); } /** |