aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-client
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-client')
-rw-r--r--packages/meshbay-client/package.json4
-rw-r--r--packages/meshbay-client/src/main.js92
2 files changed, 87 insertions, 9 deletions
diff --git a/packages/meshbay-client/package.json b/packages/meshbay-client/package.json
index 6d8e711..28e0f3c 100644
--- a/packages/meshbay-client/package.json
+++ b/packages/meshbay-client/package.json
@@ -49,6 +49,10 @@
{
"from": "../../packaging/win/service-mode.ps1",
"to": "service-mode.ps1"
+ },
+ {
+ "from": "../../packaging/win/ensure-node-path.ps1",
+ "to": "ensure-node-path.ps1"
}
]
},
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();
}
/**