diff options
Diffstat (limited to 'packages/meshbay-client/src/main.js')
| -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(); } /** |