2026-03-31 18:47:00 +03:00
|
|
|
export const createGracefulShutdownRuntime = (dependencies) => {
|
|
|
|
|
const {
|
|
|
|
|
process,
|
|
|
|
|
shutdownTimeoutMs,
|
|
|
|
|
getExitOnShutdown,
|
|
|
|
|
getIsShuttingDown,
|
|
|
|
|
setIsShuttingDown,
|
|
|
|
|
syncToHmrState,
|
|
|
|
|
openCodeWatcherRuntime,
|
|
|
|
|
sessionRuntime,
|
2026-04-16 15:55:08 +03:00
|
|
|
scheduledTasksRuntime,
|
2026-03-31 18:47:00 +03:00
|
|
|
getHealthCheckInterval,
|
|
|
|
|
clearHealthCheckInterval,
|
|
|
|
|
getTerminalRuntime,
|
|
|
|
|
setTerminalRuntime,
|
2026-04-17 16:07:26 +08:00
|
|
|
getMessageStreamRuntime,
|
|
|
|
|
setMessageStreamRuntime,
|
2026-03-31 18:47:00 +03:00
|
|
|
shouldSkipOpenCodeStop,
|
|
|
|
|
getOpenCodePort,
|
|
|
|
|
getOpenCodeProcess,
|
|
|
|
|
setOpenCodeProcess,
|
|
|
|
|
killProcessOnPort,
|
2026-04-12 07:29:09 +12:00
|
|
|
waitForPortRelease,
|
2026-03-31 18:47:00 +03:00
|
|
|
getServer,
|
|
|
|
|
getUiAuthController,
|
|
|
|
|
setUiAuthController,
|
|
|
|
|
getActiveTunnelController,
|
|
|
|
|
setActiveTunnelController,
|
|
|
|
|
tunnelAuthController,
|
|
|
|
|
} = dependencies;
|
|
|
|
|
|
2026-06-03 14:51:10 +03:00
|
|
|
let shutdownPromise = null;
|
|
|
|
|
|
|
|
|
|
const runShutdown = async (options = {}) => {
|
2026-03-31 18:47:00 +03:00
|
|
|
if (getIsShuttingDown()) return;
|
|
|
|
|
|
|
|
|
|
setIsShuttingDown(true);
|
|
|
|
|
syncToHmrState();
|
|
|
|
|
console.log('Starting graceful shutdown...');
|
|
|
|
|
const exitProcess = typeof options.exitProcess === 'boolean' ? options.exitProcess : getExitOnShutdown();
|
|
|
|
|
|
|
|
|
|
openCodeWatcherRuntime.stop();
|
|
|
|
|
sessionRuntime.dispose();
|
2026-04-16 15:55:08 +03:00
|
|
|
scheduledTasksRuntime?.stop?.();
|
2026-03-31 18:47:00 +03:00
|
|
|
|
|
|
|
|
const healthCheckInterval = getHealthCheckInterval();
|
|
|
|
|
if (healthCheckInterval) {
|
|
|
|
|
clearHealthCheckInterval(healthCheckInterval);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const terminalRuntime = getTerminalRuntime();
|
|
|
|
|
if (terminalRuntime) {
|
|
|
|
|
try {
|
|
|
|
|
await terminalRuntime.shutdown();
|
|
|
|
|
} catch {
|
|
|
|
|
} finally {
|
|
|
|
|
setTerminalRuntime(null);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 16:07:26 +08:00
|
|
|
const messageStreamRuntime = getMessageStreamRuntime();
|
|
|
|
|
if (messageStreamRuntime) {
|
|
|
|
|
try {
|
|
|
|
|
await messageStreamRuntime.close();
|
|
|
|
|
} catch {
|
|
|
|
|
} finally {
|
|
|
|
|
setMessageStreamRuntime(null);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
if (!shouldSkipOpenCodeStop()) {
|
|
|
|
|
const portToKill = getOpenCodePort();
|
|
|
|
|
const openCodeProcess = getOpenCodeProcess();
|
|
|
|
|
|
|
|
|
|
if (openCodeProcess) {
|
|
|
|
|
console.log('Stopping OpenCode process...');
|
|
|
|
|
try {
|
2026-04-12 07:29:09 +12:00
|
|
|
await openCodeProcess.close();
|
2026-03-31 18:47:00 +03:00
|
|
|
} catch (error) {
|
|
|
|
|
console.warn('Error closing OpenCode process:', error);
|
|
|
|
|
}
|
|
|
|
|
setOpenCodeProcess(null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
killProcessOnPort(portToKill);
|
2026-04-12 07:29:09 +12:00
|
|
|
if (!(await waitForPortRelease(portToKill, 5000))) {
|
|
|
|
|
console.warn(`Timed out waiting for OpenCode port ${portToKill} to be released during shutdown`);
|
|
|
|
|
}
|
2026-03-31 18:47:00 +03:00
|
|
|
} else {
|
|
|
|
|
console.log('Skipping OpenCode shutdown (external server)');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const server = getServer();
|
|
|
|
|
if (server) {
|
2026-05-12 04:00:45 -04:00
|
|
|
let closeTimeout = null;
|
|
|
|
|
try {
|
|
|
|
|
await Promise.race([
|
|
|
|
|
new Promise((resolve) => {
|
|
|
|
|
server.close(() => {
|
|
|
|
|
console.log('HTTP server closed');
|
|
|
|
|
resolve();
|
|
|
|
|
});
|
|
|
|
|
}),
|
|
|
|
|
new Promise((resolve) => {
|
|
|
|
|
closeTimeout = setTimeout(() => {
|
|
|
|
|
console.warn('Server close timeout reached, forcing shutdown');
|
|
|
|
|
resolve();
|
|
|
|
|
}, shutdownTimeoutMs);
|
|
|
|
|
}),
|
|
|
|
|
]);
|
|
|
|
|
} finally {
|
|
|
|
|
if (closeTimeout) {
|
|
|
|
|
clearTimeout(closeTimeout);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 18:47:00 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const uiAuthController = getUiAuthController();
|
|
|
|
|
if (uiAuthController) {
|
|
|
|
|
uiAuthController.dispose();
|
|
|
|
|
setUiAuthController(null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const activeTunnelController = getActiveTunnelController();
|
|
|
|
|
if (activeTunnelController) {
|
|
|
|
|
console.log('Stopping active tunnel...');
|
|
|
|
|
activeTunnelController.stop();
|
|
|
|
|
setActiveTunnelController(null);
|
|
|
|
|
tunnelAuthController.clearActiveTunnel();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('Graceful shutdown complete');
|
|
|
|
|
if (exitProcess) {
|
|
|
|
|
process.exit(0);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-03 14:51:10 +03:00
|
|
|
const gracefulShutdown = (options = {}) => {
|
|
|
|
|
if (shutdownPromise) return shutdownPromise;
|
|
|
|
|
shutdownPromise = runShutdown(options);
|
|
|
|
|
return shutdownPromise;
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
return {
|
|
|
|
|
gracefulShutdown,
|
|
|
|
|
};
|
|
|
|
|
};
|