/** * 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 { stopSupervisor } from '../../supervisor/index.js'; export interface ShutdownableService { shutdownAll(): Promise; } export interface CloseableClient { close(): Promise; } export interface CloseableDatabase { close(): Promise; } /** * Stoppable service interface for ChromaMcpManager */ export interface StoppableService { stop(): Promise; } /** * Configuration for graceful shutdown */ export interface GracefulShutdownConfig { server: http.Server | null; sessionManager: ShutdownableService; mcpClient?: CloseableClient; dbManager?: CloseableDatabase; chromaMcpManager?: StoppableService; } /** * 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'); // STEP 1: Close HTTP server first if (config.server) { await closeHttpServer(config.server); logger.info('SYSTEM', 'HTTP server closed'); } // STEP 2: Shutdown active sessions await config.sessionManager.shutdownAll(); // STEP 3: Close MCP client connection (signals child to exit gracefully) if (config.mcpClient) { await config.mcpClient.close(); logger.info('SYSTEM', 'MCP client closed'); } // STEP 4: Stop Chroma MCP connection if (config.chromaMcpManager) { logger.info('SHUTDOWN', 'Stopping Chroma MCP connection...'); await config.chromaMcpManager.stop(); logger.info('SHUTDOWN', 'Chroma MCP connection stopped'); } // STEP 5: Close database connection (includes ChromaSync cleanup) if (config.dbManager) { await config.dbManager.close(); } // STEP 6: Supervisor handles tracked child termination, PID cleanup, and stale sockets. await stopSupervisor(); 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'); } }