UNPKG

@ordojs/cli

Version:

Command-line interface for OrdoJS framework

191 lines 6.46 kB
/** * @fileoverview OrdoJS CLI - Process Manager * * Handles process lifecycle management, cleanup, and zombie prevention. */ import { ChildProcess, spawn } from 'child_process'; import { logger } from '../utils/index.js'; /** * ProcessManager class for managing child processes */ export class ProcessManager { processes; isShuttingDown; exitHandlerRegistered; /** * Create a new ProcessManager instance */ constructor() { this.processes = new Map(); this.isShuttingDown = false; this.exitHandlerRegistered = false; this.registerExitHandlers(); } /** * Register handlers for process exit events */ registerExitHandlers() { if (this.exitHandlerRegistered) { return; } // Handle normal exit process.on('exit', () => { this.cleanup(); }); // Handle Ctrl+C and other signals process.on('SIGINT', () => { this.shutdown('SIGINT received'); }); process.on('SIGTERM', () => { this.shutdown('SIGTERM received'); }); // Handle uncaught exceptions process.on('uncaughtException', (error) => { logger.error(`Uncaught exception: ${error.message}`); this.shutdown('Uncaught exception'); }); this.exitHandlerRegistered = true; } /** * Start a new child process * * @param id - Unique identifier for the process * @param command - Command to execute * @param args - Command arguments * @param options - Spawn options * @returns The created child process */ startProcess(id, command, args = [], options = {}) { if (this.isShuttingDown) { throw new Error('Cannot start new process during shutdown'); } logger.debug(`Starting process ${id}: ${command} ${args.join(' ')}`); const childProcess = spawn(command, args, { stdio: 'pipe', ...options }); // Store the process this.processes.set(id, childProcess); // Set up event handlers childProcess.on('error', (error) => { logger.error(`Process ${id} error: ${error.message}`); }); childProcess.on('exit', (code, signal) => { logger.debug(`Process ${id} exited with code ${code} and signal ${signal}`); this.processes.delete(id); }); // Handle stdout and stderr if (childProcess.stdout) { childProcess.stdout.on('data', (data) => { logger.debug(`[${id}] ${data.toString().trim()}`); }); } if (childProcess.stderr) { childProcess.stderr.on('data', (data) => { logger.error(`[${id}] ${data.toString().trim()}`); }); } return childProcess; } /** * Stop a specific process * * @param id - Process identifier * @param signal - Signal to send (default: SIGTERM) * @returns Promise that resolves when the process has exited */ async stopProcess(id, signal = 'SIGTERM') { const childProcess = this.processes.get(id); if (!childProcess) { logger.debug(`Process ${id} not found or already stopped`); return; } return new Promise((resolve) => { // Set up exit handler childProcess.once('exit', () => { this.processes.delete(id); logger.debug(`Process ${id} stopped`); resolve(); }); // Try to kill the process gracefully if (childProcess.kill(signal)) { logger.debug(`Sent ${signal} to process ${id}`); } else { logger.warn(`Failed to send ${signal} to process ${id}, may already be exiting`); this.processes.delete(id); resolve(); } // Set a timeout to force kill if it doesn't exit setTimeout(() => { if (this.processes.has(id)) { logger.warn(`Process ${id} did not exit after ${signal}, forcing SIGKILL`); childProcess.kill('SIGKILL'); } }, 5000); }); } /** * Check if a process is running * * @param id - Process identifier * @returns True if the process is running */ isProcessRunning(id) { return this.processes.has(id); } /** * Get all running processes * * @returns Map of process IDs to child processes */ getRunningProcesses() { return new Map(this.processes); } /** * Clean up all managed processes */ async cleanup() { if (this.processes.size === 0) { return; } logger.debug(`Cleaning up ${this.processes.size} processes`); // Create an array of promises for stopping each process const stopPromises = Array.from(this.processes.keys()).map(id => this.stopProcess(id).catch(error => { logger.error(`Error stopping process ${id}: ${error instanceof Error ? error.message : String(error)}`); })); // Wait for all processes to stop await Promise.all(stopPromises); // Double-check that all processes are gone if (this.processes.size > 0) { logger.warn(`${this.processes.size} processes could not be stopped gracefully, forcing termination`); // Force kill any remaining processes for (const [id, childProcess] of this.processes.entries()) { childProcess.kill('SIGKILL'); this.processes.delete(id); } } } /** * Shutdown the process manager and all managed processes * * @param reason - Reason for shutdown */ async shutdown(reason) { if (this.isShuttingDown) { return; } this.isShuttingDown = true; logger.info(`Shutting down: ${reason}`); try { await this.cleanup(); logger.success('All processes terminated successfully'); } catch (error) { logger.error(`Error during shutdown: ${error instanceof Error ? error.message : String(error)}`); } // Exit the process process.exit(0); } } //# sourceMappingURL=process-manager.js.map