feat(daemon): Add central TSPM daemon and IPC client; refactor CLI to use daemon and improve monitoring/error handling
This commit is contained in:
@@ -3,14 +3,14 @@ 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
|
||||
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)
|
||||
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 {
|
||||
@@ -36,7 +36,10 @@ export class ProcessMonitor {
|
||||
const interval = this.config.monitorIntervalMs || 5000;
|
||||
this.intervalId = setInterval((): void => {
|
||||
if (this.processWrapper && this.processWrapper.getPid()) {
|
||||
this.monitorProcessGroup(this.processWrapper.getPid()!, this.config.memoryLimitBytes);
|
||||
this.monitorProcessGroup(
|
||||
this.processWrapper.getPid()!,
|
||||
this.config.memoryLimitBytes,
|
||||
);
|
||||
}
|
||||
}, interval);
|
||||
}
|
||||
@@ -69,29 +72,35 @@ export class ProcessMonitor {
|
||||
}
|
||||
});
|
||||
|
||||
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(
|
||||
'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: Error | ProcessError): void => {
|
||||
const errorMsg = error instanceof ProcessError
|
||||
? `Process error: ${error.toString()}`
|
||||
: `Process error: ${error.message}`;
|
||||
|
||||
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...');
|
||||
@@ -108,7 +117,9 @@ export class ProcessMonitor {
|
||||
} 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)}`);
|
||||
this.logger.error(
|
||||
`Failed to start process: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,29 +127,32 @@ export class ProcessMonitor {
|
||||
* 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> {
|
||||
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)`
|
||||
`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)`
|
||||
memoryUsage,
|
||||
)} (${memoryUsage} bytes)`,
|
||||
);
|
||||
|
||||
|
||||
if (memoryUsage > memoryLimit) {
|
||||
const memoryLimitMsg = `Memory usage ${this.humanReadableBytes(
|
||||
memoryUsage
|
||||
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();
|
||||
@@ -148,9 +162,9 @@ export class ProcessMonitor {
|
||||
const processError = new ProcessError(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
'ERR_MEMORY_MONITORING_FAILED',
|
||||
{ pid }
|
||||
{ pid },
|
||||
);
|
||||
|
||||
|
||||
this.logger.error(processError);
|
||||
this.log(`Error monitoring process group: ${processError.toString()}`);
|
||||
}
|
||||
@@ -161,43 +175,58 @@ export class ProcessMonitor {
|
||||
*/
|
||||
private getProcessGroupMemory(pid: number): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
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))];
|
||||
this.logger.debug(`Found ${pids.length} processes in group with parent PID ${pid}`);
|
||||
|
||||
plugins.pidusage(pids, (err: Error | null, stats: Record<string, { memory: number }>) => {
|
||||
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 usage stats: ${err.message}`,
|
||||
'ERR_PIDUSAGE_FAILED',
|
||||
{ pids }
|
||||
`Failed to get process tree: ${err.message}`,
|
||||
'ERR_PSTREE_FAILED',
|
||||
{ pid },
|
||||
);
|
||||
this.logger.debug(`pidusage error: ${err.message}`);
|
||||
this.logger.debug(`psTree 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);
|
||||
});
|
||||
});
|
||||
|
||||
// Include the main process and its children.
|
||||
const pids: number[] = [
|
||||
pid,
|
||||
...children.map((child) => Number(child.PID)),
|
||||
];
|
||||
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);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -226,7 +255,7 @@ export class ProcessMonitor {
|
||||
this.processWrapper.stop();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the current logs from the process
|
||||
*/
|
||||
@@ -236,28 +265,28 @@ export class ProcessMonitor {
|
||||
}
|
||||
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
|
||||
*/
|
||||
@@ -272,4 +301,4 @@ export class ProcessMonitor {
|
||||
const prefix = this.config.name ? `[${this.config.name}] ` : '';
|
||||
console.log(prefix + message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user