tspm/ts/classes.processmonitor.ts

158 lines
5.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as plugins from './plugins.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)
}
export class ProcessMonitor {
private child: plugins.childProcess.ChildProcess | null = null;
private config: IMonitorConfig;
private intervalId: NodeJS.Timeout | null = null;
private stopped: boolean = true; // Initially stopped until start() is called
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.spawnChild();
// Set the monitoring interval.
const interval = this.config.monitorIntervalMs || 5000;
this.intervalId = setInterval(() => {
if (this.child && this.child.pid) {
this.monitorProcessGroup(this.child.pid, this.config.memoryLimitBytes);
}
}, interval);
}
private spawnChild(): void {
// Don't spawn if the monitor has been stopped.
if (this.stopped) return;
if (this.config.args && this.config.args.length > 0) {
this.log(
`Spawning command "${this.config.command}" with args [${this.config.args.join(
', '
)}] in directory: ${this.config.projectDir}`
);
this.child = plugins.childProcess.spawn(this.config.command, this.config.args, {
cwd: this.config.projectDir,
detached: true,
stdio: 'inherit',
});
} else {
this.log(
`Spawning command "${this.config.command}" in directory: ${this.config.projectDir}`
);
// Use shell mode to allow a full command string.
this.child = plugins.childProcess.spawn(this.config.command, {
cwd: this.config.projectDir,
detached: true,
stdio: 'inherit',
shell: true,
});
}
this.log(`Spawned process with PID ${this.child.pid}`);
// When the child process exits, restart it if the monitor isn't stopped.
this.child.on('exit', (code, signal) => {
this.log(`Child process exited with code ${code}, signal ${signal}.`);
if (!this.stopped) {
this.log('Restarting process...');
this.spawnChild();
}
});
}
/**
* Monitor the process groups 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.`
);
// Kill the entire process group by sending a signal to -PID.
process.kill(-pid, 'SIGKILL');
}
} 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.child && this.child.pid) {
process.kill(-this.child.pid, 'SIGKILL');
}
}
/**
* 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);
}
}