import * as plugins from './plugins.js'; import { ProcessWrapper } from './classes.processwrapper.js'; export interface IMonitorConfig { name?: string; // Optional name to identify the instance projectDir: string; // Directory where the command will run command: string; // Full command to run (e.g., "npm run xyz") args?: string[]; // Optional: arguments for the command memoryLimitBytes: number; // Maximum allowed memory (in bytes) for the process group monitorIntervalMs?: number; // Interval (in ms) at which memory is checked (default: 5000) env?: NodeJS.ProcessEnv; // Optional: custom environment variables logBufferSize?: number; // Optional: number of log lines to keep (default: 100) } export class ProcessMonitor { private processWrapper: ProcessWrapper | null = null; private config: IMonitorConfig; private intervalId: NodeJS.Timeout | null = null; private stopped: boolean = true; // Initially stopped until start() is called private restartCount: number = 0; constructor(config: IMonitorConfig) { this.config = config; } public start(): void { // Reset the stopped flag so that new processes can spawn. this.stopped = false; this.log(`Starting process monitor.`); this.spawnProcess(); // Set the monitoring interval. const interval = this.config.monitorIntervalMs || 5000; this.intervalId = setInterval(() => { if (this.processWrapper && this.processWrapper.getPid()) { this.monitorProcessGroup(this.processWrapper.getPid()!, this.config.memoryLimitBytes); } }, interval); } private spawnProcess(): void { // Don't spawn if the monitor has been stopped. if (this.stopped) return; // Create a new process wrapper this.processWrapper = new ProcessWrapper({ name: this.config.name || 'unnamed-process', command: this.config.command, args: this.config.args, cwd: this.config.projectDir, env: this.config.env, logBuffer: this.config.logBufferSize, }); // Set up event handlers this.processWrapper.on('log', (log) => { // Here we could add handlers to send logs somewhere // For now, we just log system messages to the console if (log.type === 'system') { this.log(log.message); } }); this.processWrapper.on('exit', (code, signal) => { this.log(`Process exited with code ${code}, signal ${signal}.`); if (!this.stopped) { this.log('Restarting process...'); this.restartCount++; this.spawnProcess(); } }); this.processWrapper.on('error', (error) => { this.log(`Process error: ${error.message}`); if (!this.stopped) { this.log('Restarting process due to error...'); this.restartCount++; this.spawnProcess(); } }); // Start the process this.processWrapper.start(); } /** * Monitor the process group's memory usage. If the total memory exceeds the limit, * kill the process group so that the 'exit' handler can restart it. */ private async monitorProcessGroup(pid: number, memoryLimit: number): Promise { try { const memoryUsage = await this.getProcessGroupMemory(pid); this.log( `Current memory usage for process group (PID ${pid}): ${this.humanReadableBytes( memoryUsage )} (${memoryUsage} bytes)` ); if (memoryUsage > memoryLimit) { this.log( `Memory usage ${this.humanReadableBytes( memoryUsage )} exceeds limit of ${this.humanReadableBytes(memoryLimit)}. Restarting process.` ); // Stop the process wrapper, which will trigger the exit handler and restart if (this.processWrapper) { this.processWrapper.stop(); } } } catch (error) { this.log('Error monitoring process group: ' + error); } } /** * Get the total memory usage (in bytes) for the process group (the main process and its children). */ private getProcessGroupMemory(pid: number): Promise { return new Promise((resolve, reject) => { plugins.psTree(pid, (err, children) => { if (err) return reject(err); // Include the main process and its children. const pids: number[] = [pid, ...children.map(child => Number(child.PID))]; plugins.pidusage(pids, (err, stats) => { if (err) return reject(err); let totalMemory = 0; for (const key in stats) { totalMemory += stats[key].memory; } resolve(totalMemory); }); }); }); } /** * Convert a number of bytes into a human-readable string (e.g. "1.23 MB"). */ private humanReadableBytes(bytes: number, decimals: number = 2): string { if (bytes === 0) return '0 Bytes'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } /** * Stop the monitor and prevent any further respawns. */ public stop(): void { this.log('Stopping process monitor.'); this.stopped = true; if (this.intervalId) { clearInterval(this.intervalId); } if (this.processWrapper) { this.processWrapper.stop(); } } /** * Get the current logs from the process */ public getLogs(limit?: number): Array<{ timestamp: Date, type: string, message: string }> { if (!this.processWrapper) { return []; } return this.processWrapper.getLogs(limit); } /** * Get the number of times the process has been restarted */ public getRestartCount(): number { return this.restartCount; } /** * Get the process ID if running */ public getPid(): number | null { return this.processWrapper?.getPid() || null; } /** * Get process uptime in milliseconds */ public getUptime(): number { return this.processWrapper?.getUptime() || 0; } /** * Check if the process is currently running */ public isRunning(): boolean { return this.processWrapper?.isRunning() || false; } /** * Helper method for logging messages with the instance name. */ private log(message: string): void { const prefix = this.config.name ? `[${this.config.name}] ` : ''; console.log(prefix + message); } }