tspm/ts/classes.processmonitor.ts

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);
}
}