fix(core): Improve error handling, logging, and test suite; update dependency versions

This commit is contained in:
2025-03-10 23:38:21 +00:00
parent 5c4836fd68
commit 779593f73a
12 changed files with 1648 additions and 236 deletions

View File

@@ -1,5 +1,6 @@
import * as plugins from './plugins.js';
import { ProcessWrapper, type IProcessLog } from './classes.processwrapper.js';
import { Logger, ProcessError, handleError } from './utils.errorhandler.js';
export interface IMonitorConfig {
name?: string; // Optional name to identify the instance
@@ -18,9 +19,11 @@ export class ProcessMonitor {
private intervalId: NodeJS.Timeout | null = null;
private stopped: boolean = true; // Initially stopped until start() is called
private restartCount: number = 0;
private logger: Logger;
constructor(config: IMonitorConfig) {
this.config = config;
this.logger = new Logger(`ProcessMonitor:${config.name || 'unnamed'}`);
}
public start(): void {
@@ -31,7 +34,7 @@ export class ProcessMonitor {
// Set the monitoring interval.
const interval = this.config.monitorIntervalMs || 5000;
this.intervalId = setInterval(() => {
this.intervalId = setInterval((): void => {
if (this.processWrapper && this.processWrapper.getPid()) {
this.monitorProcessGroup(this.processWrapper.getPid()!, this.config.memoryLimitBytes);
}
@@ -40,7 +43,12 @@ export class ProcessMonitor {
private spawnProcess(): void {
// Don't spawn if the monitor has been stopped.
if (this.stopped) return;
if (this.stopped) {
this.logger.debug('Not spawning process because monitor is stopped');
return;
}
this.logger.info(`Spawning process: ${this.config.command}`);
// Create a new process wrapper
this.processWrapper = new ProcessWrapper({
@@ -53,7 +61,7 @@ export class ProcessMonitor {
});
// Set up event handlers
this.processWrapper.on('log', (log) => {
this.processWrapper.on('log', (log: IProcessLog): void => {
// Here we could add handlers to send logs somewhere
// For now, we just log system messages to the console
if (log.type === 'system') {
@@ -61,26 +69,47 @@ export class ProcessMonitor {
}
});
this.processWrapper.on('exit', (code, signal) => {
this.log(`Process exited with code ${code}, signal ${signal}.`);
this.processWrapper.on('exit', (code: number | null, signal: string | null): void => {
const exitMsg = `Process exited with code ${code}, signal ${signal}.`;
this.logger.info(exitMsg);
this.log(exitMsg);
if (!this.stopped) {
this.logger.info('Restarting process...');
this.log('Restarting process...');
this.restartCount++;
this.spawnProcess();
} else {
this.logger.debug('Not restarting process because monitor is stopped');
}
});
this.processWrapper.on('error', (error) => {
this.log(`Process error: ${error.message}`);
this.processWrapper.on('error', (error: Error | ProcessError): void => {
const errorMsg = error instanceof ProcessError
? `Process error: ${error.toString()}`
: `Process error: ${error.message}`;
this.logger.error(error);
this.log(errorMsg);
if (!this.stopped) {
this.logger.info('Restarting process due to error...');
this.log('Restarting process due to error...');
this.restartCount++;
this.spawnProcess();
} else {
this.logger.debug('Not restarting process because monitor is stopped');
}
});
// Start the process
this.processWrapper.start();
try {
this.processWrapper.start();
} catch (error: Error | unknown) {
// The process wrapper will handle logging the error
// Just prevent it from bubbling up further
this.logger.error(`Failed to start process: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
@@ -90,24 +119,40 @@ export class ProcessMonitor {
private async monitorProcessGroup(pid: number, memoryLimit: number): Promise<void> {
try {
const memoryUsage = await this.getProcessGroupMemory(pid);
this.logger.debug(
`Memory usage for PID ${pid}: ${this.humanReadableBytes(memoryUsage)} (${memoryUsage} bytes)`
);
// Only log to the process log at longer intervals to avoid spamming
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.`
);
const memoryLimitMsg = `Memory usage ${this.humanReadableBytes(
memoryUsage
)} exceeds limit of ${this.humanReadableBytes(memoryLimit)}. Restarting process.`;
this.logger.warn(memoryLimitMsg);
this.log(memoryLimitMsg);
// 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);
} catch (error: Error | unknown) {
const processError = new ProcessError(
error instanceof Error ? error.message : String(error),
'ERR_MEMORY_MONITORING_FAILED',
{ pid }
);
this.logger.error(processError);
this.log(`Error monitoring process group: ${processError.toString()}`);
}
}
@@ -116,16 +161,40 @@ export class ProcessMonitor {
*/
private getProcessGroupMemory(pid: number): Promise<number> {
return new Promise((resolve, reject) => {
plugins.psTree(pid, (err, children) => {
if (err) return reject(err);
this.logger.debug(`Getting memory usage for process group with PID ${pid}`);
plugins.psTree(pid, (err: Error | null, children: Array<{ PID: string }>) => {
if (err) {
const processError = new ProcessError(
`Failed to get process tree: ${err.message}`,
'ERR_PSTREE_FAILED',
{ pid }
);
this.logger.debug(`psTree error: ${err.message}`);
return reject(processError);
}
// 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);
this.logger.debug(`Found ${pids.length} processes in group with parent PID ${pid}`);
plugins.pidusage(pids, (err: Error | null, stats: Record<string, { memory: number }>) => {
if (err) {
const processError = new ProcessError(
`Failed to get process usage stats: ${err.message}`,
'ERR_PIDUSAGE_FAILED',
{ pids }
);
this.logger.debug(`pidusage error: ${err.message}`);
return reject(processError);
}
let totalMemory = 0;
for (const key in stats) {
totalMemory += stats[key].memory;
}
this.logger.debug(`Total memory for process group: ${this.humanReadableBytes(totalMemory)}`);
resolve(totalMemory);
});
});