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
@@ -38,68 +38,86 @@ export const createServerStartupRuntime = (dependencies) => {
server.once('error', onError);
const onListening = async () => {
server.off('error', onError);
const addressInfo = server.address();
activePort = typeof addressInfo === 'object' && addressInfo ? addressInfo.port : port;
try {
process.send?.({ type: 'openchamber:ready', port: activePort });
} catch {
// ignore
}
const addressInfo = server.address();
activePort = typeof addressInfo === 'object' && addressInfo ? addressInfo.port : port;
const displayHost = (bindHost === '0.0.0.0' || bindHost === '::' || bindHost === '[::]')
? 'localhost'
: (bindHost.includes(':') ? `[${bindHost}]` : bindHost);
console.log(`OpenChamber server listening on ${bindHost}:${activePort}`);
console.log(`Health check: http://${displayHost}:${activePort}/health`);
console.log(`Web interface: http://${displayHost}:${activePort}`);
if (startupTunnelRequest) {
const startupModeLabel = startupTunnelRequest.mode === TUNNEL_MODE_QUICK
? 'Quick Tunnel'
: (startupTunnelRequest.mode === TUNNEL_MODE_MANAGED_LOCAL
? 'Managed Local Tunnel'
: (startupTunnelRequest.mode === TUNNEL_MODE_MANAGED_REMOTE ? 'Managed Remote Tunnel' : 'Tunnel'));
console.log(`\nInitializing ${startupModeLabel} for provider '${startupTunnelRequest.provider}'...`);
try {
const { publicUrl, mode } = await startTunnelWithNormalizedRequest({
provider: startupTunnelRequest.provider,
mode: startupTunnelRequest.mode,
intent: startupTunnelRequest.intent,
hostname: startupTunnelRequest.hostname,
token: startupTunnelRequest.token,
configPath: startupTunnelRequest.configPath,
selectedPresetId: '',
selectedPresetName: '',
});
if (publicUrl) {
tunnelAuthController.setActiveTunnel({
tunnelId: crypto.randomUUID(),
publicUrl,
mode,
});
const settings = await readSettingsFromDiskMigrated();
const bootstrapTtlMs = settings?.tunnelBootstrapTtlMs === null
? null
: normalizeTunnelBootstrapTtlMs(settings?.tunnelBootstrapTtlMs);
const bootstrapToken = tunnelAuthController.issueBootstrapToken({ ttlMs: bootstrapTtlMs });
const connectUrl = `${publicUrl.replace(/\/$/, '')}/connect?t=${encodeURIComponent(bootstrapToken.token)}`;
if (onTunnelReady) {
onTunnelReady(publicUrl, connectUrl);
} else {
console.log(`\n🌐 Tunnel URL: ${connectUrl}`);
console.log('🔑 One-time connect link (expires after first use)\n');
}
} else if (onTunnelReady) {
onTunnelReady(publicUrl, null);
if (typeof process.send === 'function') {
if (!process.connected) {
throw new Error('OpenChamber startup IPC channel disconnected before ready notification');
}
} catch (error) {
console.error(`Failed to start tunnel: ${error.message}`);
console.log('Continuing without tunnel...');
}
}
resolve();
await new Promise((resolveReadyNotification, rejectReadyNotification) => {
try {
process.send({ type: 'openchamber:ready', port: activePort }, (error) => {
if (error) {
rejectReadyNotification(error);
return;
}
resolveReadyNotification();
});
} catch (error) {
rejectReadyNotification(error);
}
});
}
const displayHost = (bindHost === '0.0.0.0' || bindHost === '::' || bindHost === '[::]')
? 'localhost'
: (bindHost.includes(':') ? `[${bindHost}]` : bindHost);
console.log(`OpenChamber server listening on ${bindHost}:${activePort}`);
console.log(`Health check: http://${displayHost}:${activePort}/health`);
console.log(`Web interface: http://${displayHost}:${activePort}`);
if (startupTunnelRequest) {
const startupModeLabel = startupTunnelRequest.mode === TUNNEL_MODE_QUICK
? 'Quick Tunnel'
: (startupTunnelRequest.mode === TUNNEL_MODE_MANAGED_LOCAL
? 'Managed Local Tunnel'
: (startupTunnelRequest.mode === TUNNEL_MODE_MANAGED_REMOTE ? 'Managed Remote Tunnel' : 'Tunnel'));
console.log(`\nInitializing ${startupModeLabel} for provider '${startupTunnelRequest.provider}'...`);
try {
const { publicUrl, mode } = await startTunnelWithNormalizedRequest({
provider: startupTunnelRequest.provider,
mode: startupTunnelRequest.mode,
intent: startupTunnelRequest.intent,
hostname: startupTunnelRequest.hostname,
token: startupTunnelRequest.token,
configPath: startupTunnelRequest.configPath,
selectedPresetId: '',
selectedPresetName: '',
});
if (publicUrl) {
tunnelAuthController.setActiveTunnel({
tunnelId: crypto.randomUUID(),
publicUrl,
mode,
});
const settings = await readSettingsFromDiskMigrated();
const bootstrapTtlMs = settings?.tunnelBootstrapTtlMs === null
? null
: normalizeTunnelBootstrapTtlMs(settings?.tunnelBootstrapTtlMs);
const bootstrapToken = tunnelAuthController.issueBootstrapToken({ ttlMs: bootstrapTtlMs });
const connectUrl = `${publicUrl.replace(/\/$/, '')}/connect?t=${encodeURIComponent(bootstrapToken.token)}`;
if (onTunnelReady) {
onTunnelReady(publicUrl, connectUrl);
} else {
console.log(`\n🌐 Tunnel URL: ${connectUrl}`);
console.log('🔑 One-time connect link (expires after first use)\n');
}
} else if (onTunnelReady) {
onTunnelReady(publicUrl, null);
}
} catch (error) {
console.error(`Failed to start tunnel: ${error.message}`);
console.log('Continuing without tunnel...');
}
}
resolve();
} catch (error) {
reject(error);
}
};
server.listen(port, bindHost, onListening);