/** * GracefulShutdown - Cleanup utilities for graceful exit * * Extracted from worker-service.ts to provide centralized shutdown coordination. * Handles: * - HTTP server closure (with Windows-specific delays) * - Session manager shutdown coordination * - Child process cleanup (Windows zombie port fix) */ import http from 'http'; import { logger } from '../../utils/logger.js'; import { getChildProcesses, forceKillProcess, waitForProcessesExit, removePidFile } from './ProcessManager.js'; export interface ShutdownableService { shutdownAll(): Promise; } export interface CloseableClient { close(): Promise; } export interface CloseableDatabase { close(): Promise; } /** * Stoppable service interface for Chroma server */ export interface StoppableServer { stop(): Promise; } /** * Configuration for graceful shutdown */ export interface GracefulShutdownConfig { server: http.Server | null; sessionManager: ShutdownableService; mcpClient?: CloseableClient; dbManager?: CloseableDatabase; chromaServer?: StoppableServer; } /** * Perform graceful shutdown of all services * * IMPORTANT: On Windows, we must kill all child processes before exiting * to prevent zombie ports. The socket handle can be inherited by children, * and if not properly closed, the port stays bound after process death. */ export async function performGracefulShutdown(config: GracefulShutdownConfig): Promise { logger.info('SYSTEM', 'Shutdown initiated'); // Clean up PID file on shutdown removePidFile(); // STEP 1: Enumerate all child processes BEFORE we start closing things const childPids = await getChildProcesses(process.pid); logger.info('SYSTEM', 'Found child processes', { count: childPids.length, pids: childPids }); // STEP 2: Close HTTP server first if (config.server) { await closeHttpServer(config.server); logger.info('SYSTEM', 'HTTP server closed'); } // STEP 3: Shutdown active sessions await config.sessionManager.shutdownAll(); // STEP 4: Close MCP client connection (signals child to exit gracefully) if (config.mcpClient) { await config.mcpClient.close(); logger.info('SYSTEM', 'MCP client closed'); } // STEP 5: Stop Chroma server (local mode only) if (config.chromaServer) { logger.info('SHUTDOWN', 'Stopping Chroma server...'); await config.chromaServer.stop(); logger.info('SHUTDOWN', 'Chroma server stopped'); } // STEP 6: Close database connection (includes ChromaSync cleanup) if (config.dbManager) { await config.dbManager.close(); } // STEP 7: Force kill any remaining child processes (Windows zombie port fix) if (childPids.length > 0) { logger.info('SYSTEM', 'Force killing remaining children'); for (const pid of childPids) { await forceKillProcess(pid); } // Wait for children to fully exit await waitForProcessesExit(childPids, 5000); } logger.info('SYSTEM', 'Worker shutdown complete'); } /** * Close HTTP server with Windows-specific delays * Windows needs extra time to release sockets properly */ async function closeHttpServer(server: http.Server): Promise { // Close all active connections server.closeAllConnections(); // Give Windows time to close connections before closing server (prevents zombie ports) if (process.platform === 'win32') { await new Promise(r => setTimeout(r, 500)); } // Close the server await new Promise((resolve, reject) => { server.close(err => err ? reject(err) : resolve()); }); // Extra delay on Windows to ensure port is fully released if (process.platform === 'win32') { await new Promise(r => setTimeout(r, 500)); logger.info('SYSTEM', 'Waited for Windows port cleanup'); } }