import * as plugins from './plugins.js'; import { EventEmitter } from 'events'; import { Logger, ProcessError, handleError } from './utils.errorhandler.js'; export interface IProcessWrapperOptions { command: string; args?: string[]; cwd: string; env?: NodeJS.ProcessEnv; name: string; logBuffer?: number; // Number of log lines to keep in memory (default: 100) } export interface IProcessLog { timestamp: Date; type: 'stdout' | 'stderr' | 'system'; message: string; } export class ProcessWrapper extends EventEmitter { private process: plugins.childProcess.ChildProcess | null = null; private options: IProcessWrapperOptions; private logs: IProcessLog[] = []; private logBufferSize: number; private startTime: Date | null = null; private logger: Logger; constructor(options: IProcessWrapperOptions) { super(); this.options = options; this.logBufferSize = options.logBuffer || 100; this.logger = new Logger(`ProcessWrapper:${options.name}`); } /** * Start the wrapped process */ public start(): void { this.addSystemLog('Starting process...'); try { this.logger.debug(`Starting process: ${this.options.command}`); if (this.options.args && this.options.args.length > 0) { this.process = plugins.childProcess.spawn(this.options.command, this.options.args, { cwd: this.options.cwd, env: this.options.env || process.env, stdio: ['ignore', 'pipe', 'pipe'], // We need to pipe stdout and stderr }); } else { // Use shell mode to allow a full command string this.process = plugins.childProcess.spawn(this.options.command, { cwd: this.options.cwd, env: this.options.env || process.env, stdio: ['ignore', 'pipe', 'pipe'], // We need to pipe stdout and stderr shell: true, }); } this.startTime = new Date(); // Handle process exit this.process.on('exit', (code, signal) => { const exitMessage = `Process exited with code ${code}, signal ${signal}`; this.logger.info(exitMessage); this.addSystemLog(exitMessage); this.emit('exit', code, signal); }); // Handle errors this.process.on('error', (error) => { const processError = new ProcessError( error.message, 'ERR_PROCESS_EXECUTION', { command: this.options.command, pid: this.process?.pid } ); this.logger.error(processError); this.addSystemLog(`Process error: ${processError.toString()}`); this.emit('error', processError); }); // Capture stdout if (this.process.stdout) { this.process.stdout.on('data', (data) => { const lines = data.toString().split('\n'); for (const line of lines) { if (line.trim()) { this.addLog('stdout', line); } } }); } // Capture stderr if (this.process.stderr) { this.process.stderr.on('data', (data) => { const lines = data.toString().split('\n'); for (const line of lines) { if (line.trim()) { this.addLog('stderr', line); } } }); } this.addSystemLog(`Process started with PID ${this.process.pid}`); this.logger.info(`Process started with PID ${this.process.pid}`); this.emit('start', this.process.pid); } catch (error: Error | unknown) { const processError = error instanceof ProcessError ? error : new ProcessError( error instanceof Error ? error.message : String(error), 'ERR_PROCESS_START_FAILED', { command: this.options.command } ); this.logger.error(processError); this.addSystemLog(`Failed to start process: ${processError.toString()}`); this.emit('error', processError); throw processError; } } /** * Stop the wrapped process */ public stop(): void { if (!this.process) { this.logger.debug('Stop called but no process is running'); this.addSystemLog('No process running'); return; } this.logger.info('Stopping process...'); this.addSystemLog('Stopping process...'); // First try SIGTERM for graceful shutdown if (this.process.pid) { try { this.logger.debug(`Sending SIGTERM to process ${this.process.pid}`); process.kill(this.process.pid, 'SIGTERM'); // Give it 5 seconds to shut down gracefully setTimeout((): void => { if (this.process && this.process.pid) { this.logger.warn(`Process ${this.process.pid} did not exit gracefully, force killing...`); this.addSystemLog('Process did not exit gracefully, force killing...'); try { process.kill(this.process.pid, 'SIGKILL'); } catch (error: Error | unknown) { // Process might have exited between checks this.logger.debug(`Failed to send SIGKILL, process probably already exited: ${ error instanceof Error ? error.message : String(error) }`); } } }, 5000); } catch (error: Error | unknown) { const processError = new ProcessError( error instanceof Error ? error.message : String(error), 'ERR_PROCESS_STOP_FAILED', { pid: this.process.pid } ); this.logger.error(processError); this.addSystemLog(`Error stopping process: ${processError.toString()}`); } } } /** * Get the process ID if running */ public getPid(): number | null { return this.process?.pid || null; } /** * Get the current logs */ public getLogs(limit: number = this.logBufferSize): IProcessLog[] { // Return the most recent logs up to the limit return this.logs.slice(-limit); } /** * Get uptime in milliseconds */ public getUptime(): number { if (!this.startTime) return 0; return Date.now() - this.startTime.getTime(); } /** * Check if the process is currently running */ public isRunning(): boolean { return this.process !== null && typeof this.process.exitCode !== 'number'; } /** * Add a log entry from stdout or stderr */ private addLog(type: 'stdout' | 'stderr', message: string): void { const log: IProcessLog = { timestamp: new Date(), type, message, }; this.logs.push(log); // Trim logs if they exceed buffer size if (this.logs.length > this.logBufferSize) { this.logs = this.logs.slice(-this.logBufferSize); } // Emit log event for potential handlers this.emit('log', log); } /** * Add a system log entry (not from the process itself) */ private addSystemLog(message: string): void { const log: IProcessLog = { timestamp: new Date(), type: 'system', message, }; this.logs.push(log); // Trim logs if they exceed buffer size if (this.logs.length > this.logBufferSize) { this.logs = this.logs.slice(-this.logBufferSize); } // Emit log event for potential handlers this.emit('log', log); } }