206 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			206 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 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<void> {
 | |
|     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<number> {
 | |
|     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);
 | |
|   }
 | |
| } |