fix: make daemon startup ready handoff reliable

Wait longer for slow daemon startup
Fail cleanly when ready handoff does not complete
Avoid orphaned daemon processes after startup timeout
This commit is contained in:
Bohdan Triapitsyn
2026-05-14 15:00:25 +03:00
parent a12be061e3
commit 34fde831eb
2 changed files with 109 additions and 77 deletions
+19 -5
View File
@@ -29,6 +29,7 @@ const __dirname = path.dirname(__filename);
const DEFAULT_PORT = 3000; const DEFAULT_PORT = 3000;
const DEFAULT_TAIL_LINES = 200; const DEFAULT_TAIL_LINES = 200;
const DAEMON_READY_TIMEOUT_MS = 30000;
const LOG_ROTATE_MAX_BYTES = 10 * 1024 * 1024; const LOG_ROTATE_MAX_BYTES = 10 * 1024 * 1024;
const LOG_ROTATE_KEEP = 5; const LOG_ROTATE_KEEP = 5;
const TUNNEL_PROFILES_VERSION = 1; const TUNNEL_PROFILES_VERSION = 1;
@@ -3006,13 +3007,15 @@ const commands = {
child.unref(); child.unref();
serveSpin?.start(`Starting OpenChamber on port ${targetPort === 0 ? 'auto' : targetPort}...`); serveSpin?.start(`Starting OpenChamber on port ${targetPort === 0 ? 'auto' : targetPort}...`);
const resolvedPort = await new Promise((resolve) => { let resolvedPort;
try {
resolvedPort = await new Promise((resolve, reject) => {
let settled = false; let settled = false;
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
if (settled) return; if (settled) return;
settled = true; settled = true;
resolve(targetPort); reject(new Error(`OpenChamber daemon did not report ready within ${DAEMON_READY_TIMEOUT_MS / 1000}s`));
}, 5000); }, DAEMON_READY_TIMEOUT_MS);
child.on('message', (msg) => { child.on('message', (msg) => {
if (settled) return; if (settled) return;
@@ -3023,13 +3026,24 @@ const commands = {
} }
}); });
child.on('exit', () => { child.on('error', (error) => {
if (settled) return; if (settled) return;
settled = true; settled = true;
clearTimeout(timeout); clearTimeout(timeout);
resolve(targetPort); reject(error);
});
child.on('exit', (code, signal) => {
if (settled) return;
settled = true;
clearTimeout(timeout);
reject(new Error(`OpenChamber daemon exited before reporting ready${signal ? ` (${signal})` : ` (code ${code ?? 'unknown'})`}`));
}); });
}); });
} catch (error) {
await terminateProcessTree(child.pid, { gracefulTimeoutMs: 1500, forceTimeoutMs: 1500 });
throw error;
}
try { try {
if (typeof child.disconnect === 'function' && child.connected) { if (typeof child.disconnect === 'function' && child.connected) {
@@ -38,13 +38,28 @@ export const createServerStartupRuntime = (dependencies) => {
server.once('error', onError); server.once('error', onError);
const onListening = async () => { const onListening = async () => {
server.off('error', onError); server.off('error', onError);
try {
const addressInfo = server.address(); const addressInfo = server.address();
activePort = typeof addressInfo === 'object' && addressInfo ? addressInfo.port : port; activePort = typeof addressInfo === 'object' && addressInfo ? addressInfo.port : port;
if (typeof process.send === 'function') {
if (!process.connected) {
throw new Error('OpenChamber startup IPC channel disconnected before ready notification');
}
await new Promise((resolveReadyNotification, rejectReadyNotification) => {
try { try {
process.send?.({ type: 'openchamber:ready', port: activePort }); process.send({ type: 'openchamber:ready', port: activePort }, (error) => {
} catch { if (error) {
// ignore rejectReadyNotification(error);
return;
}
resolveReadyNotification();
});
} catch (error) {
rejectReadyNotification(error);
}
});
} }
const displayHost = (bindHost === '0.0.0.0' || bindHost === '::' || bindHost === '[::]') const displayHost = (bindHost === '0.0.0.0' || bindHost === '::' || bindHost === '[::]')
@@ -100,6 +115,9 @@ export const createServerStartupRuntime = (dependencies) => {
} }
resolve(); resolve();
} catch (error) {
reject(error);
}
}; };
server.listen(port, bindHost, onListening); server.listen(port, bindHost, onListening);