feat(daemon): Add central TSPM daemon and IPC client; refactor CLI to use daemon and improve monitoring/error handling
This commit is contained in:
@@ -24,29 +24,33 @@ export class ProcessWrapper extends EventEmitter {
|
||||
private logBufferSize: number;
|
||||
private startTime: Date | null = null;
|
||||
private logger: Logger;
|
||||
|
||||
|
||||
constructor(options: IProcessWrapperOptions) {
|
||||
super();
|
||||
this.options = options;
|
||||
this.logBufferSize = options.logBuffer || 100;
|
||||
this.logger = new Logger(`ProcessWrapper:${options.name}`);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Start the wrapped process
|
||||
*/
|
||||
public start(): void {
|
||||
this.addSystemLog('Starting process...');
|
||||
|
||||
|
||||
try {
|
||||
this.logger.debug(`Starting process: ${this.options.command}`);
|
||||
|
||||
|
||||
if (this.options.args && this.options.args.length > 0) {
|
||||
this.process = plugins.childProcess.spawn(this.options.command, this.options.args, {
|
||||
cwd: this.options.cwd,
|
||||
env: this.options.env || process.env,
|
||||
stdio: ['ignore', 'pipe', 'pipe'], // We need to pipe stdout and stderr
|
||||
});
|
||||
this.process = plugins.childProcess.spawn(
|
||||
this.options.command,
|
||||
this.options.args,
|
||||
{
|
||||
cwd: this.options.cwd,
|
||||
env: this.options.env || process.env,
|
||||
stdio: ['ignore', 'pipe', 'pipe'], // We need to pipe stdout and stderr
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// Use shell mode to allow a full command string
|
||||
this.process = plugins.childProcess.spawn(this.options.command, {
|
||||
@@ -56,9 +60,9 @@ export class ProcessWrapper extends EventEmitter {
|
||||
shell: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
this.startTime = new Date();
|
||||
|
||||
|
||||
// Handle process exit
|
||||
this.process.on('exit', (code, signal) => {
|
||||
const exitMessage = `Process exited with code ${code}, signal ${signal}`;
|
||||
@@ -66,19 +70,19 @@ export class ProcessWrapper extends EventEmitter {
|
||||
this.addSystemLog(exitMessage);
|
||||
this.emit('exit', code, signal);
|
||||
});
|
||||
|
||||
|
||||
// Handle errors
|
||||
this.process.on('error', (error) => {
|
||||
const processError = new ProcessError(
|
||||
error.message,
|
||||
'ERR_PROCESS_EXECUTION',
|
||||
{ command: this.options.command, pid: this.process?.pid }
|
||||
{ command: this.options.command, pid: this.process?.pid },
|
||||
);
|
||||
this.logger.error(processError);
|
||||
this.addSystemLog(`Process error: ${processError.toString()}`);
|
||||
this.emit('error', processError);
|
||||
});
|
||||
|
||||
|
||||
// Capture stdout
|
||||
if (this.process.stdout) {
|
||||
this.process.stdout.on('data', (data) => {
|
||||
@@ -90,7 +94,7 @@ export class ProcessWrapper extends EventEmitter {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Capture stderr
|
||||
if (this.process.stderr) {
|
||||
this.process.stderr.on('data', (data) => {
|
||||
@@ -102,27 +106,27 @@ export class ProcessWrapper extends EventEmitter {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
this.addSystemLog(`Process started with PID ${this.process.pid}`);
|
||||
this.logger.info(`Process started with PID ${this.process.pid}`);
|
||||
this.emit('start', this.process.pid);
|
||||
|
||||
} catch (error: Error | unknown) {
|
||||
const processError = error instanceof ProcessError
|
||||
? error
|
||||
: new ProcessError(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
'ERR_PROCESS_START_FAILED',
|
||||
{ command: this.options.command }
|
||||
);
|
||||
|
||||
const processError =
|
||||
error instanceof ProcessError
|
||||
? error
|
||||
: new ProcessError(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
'ERR_PROCESS_START_FAILED',
|
||||
{ command: this.options.command },
|
||||
);
|
||||
|
||||
this.logger.error(processError);
|
||||
this.addSystemLog(`Failed to start process: ${processError.toString()}`);
|
||||
this.emit('error', processError);
|
||||
throw processError;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Stop the wrapped process
|
||||
*/
|
||||
@@ -132,28 +136,34 @@ export class ProcessWrapper extends EventEmitter {
|
||||
this.addSystemLog('No process running');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this.logger.info('Stopping process...');
|
||||
this.addSystemLog('Stopping process...');
|
||||
|
||||
|
||||
// First try SIGTERM for graceful shutdown
|
||||
if (this.process.pid) {
|
||||
try {
|
||||
this.logger.debug(`Sending SIGTERM to process ${this.process.pid}`);
|
||||
process.kill(this.process.pid, 'SIGTERM');
|
||||
|
||||
|
||||
// Give it 5 seconds to shut down gracefully
|
||||
setTimeout((): void => {
|
||||
if (this.process && this.process.pid) {
|
||||
this.logger.warn(`Process ${this.process.pid} did not exit gracefully, force killing...`);
|
||||
this.addSystemLog('Process did not exit gracefully, force killing...');
|
||||
this.logger.warn(
|
||||
`Process ${this.process.pid} did not exit gracefully, force killing...`,
|
||||
);
|
||||
this.addSystemLog(
|
||||
'Process did not exit gracefully, force killing...',
|
||||
);
|
||||
try {
|
||||
process.kill(this.process.pid, 'SIGKILL');
|
||||
} catch (error: Error | unknown) {
|
||||
// Process might have exited between checks
|
||||
this.logger.debug(`Failed to send SIGKILL, process probably already exited: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`);
|
||||
this.logger.debug(
|
||||
`Failed to send SIGKILL, process probably already exited: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
@@ -161,21 +171,21 @@ export class ProcessWrapper extends EventEmitter {
|
||||
const processError = new ProcessError(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
'ERR_PROCESS_STOP_FAILED',
|
||||
{ pid: this.process.pid }
|
||||
{ pid: this.process.pid },
|
||||
);
|
||||
this.logger.error(processError);
|
||||
this.addSystemLog(`Error stopping process: ${processError.toString()}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the process ID if running
|
||||
*/
|
||||
public getPid(): number | null {
|
||||
return this.process?.pid || null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the current logs
|
||||
*/
|
||||
@@ -183,7 +193,7 @@ export class ProcessWrapper extends EventEmitter {
|
||||
// Return the most recent logs up to the limit
|
||||
return this.logs.slice(-limit);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get uptime in milliseconds
|
||||
*/
|
||||
@@ -191,14 +201,14 @@ export class ProcessWrapper extends EventEmitter {
|
||||
if (!this.startTime) return 0;
|
||||
return Date.now() - this.startTime.getTime();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the process is currently running
|
||||
*/
|
||||
public isRunning(): boolean {
|
||||
return this.process !== null && typeof this.process.exitCode !== 'number';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a log entry from stdout or stderr
|
||||
*/
|
||||
@@ -208,18 +218,18 @@ export class ProcessWrapper extends EventEmitter {
|
||||
type,
|
||||
message,
|
||||
};
|
||||
|
||||
|
||||
this.logs.push(log);
|
||||
|
||||
|
||||
// Trim logs if they exceed buffer size
|
||||
if (this.logs.length > this.logBufferSize) {
|
||||
this.logs = this.logs.slice(-this.logBufferSize);
|
||||
}
|
||||
|
||||
|
||||
// Emit log event for potential handlers
|
||||
this.emit('log', log);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a system log entry (not from the process itself)
|
||||
*/
|
||||
@@ -229,15 +239,15 @@ export class ProcessWrapper extends EventEmitter {
|
||||
type: 'system',
|
||||
message,
|
||||
};
|
||||
|
||||
|
||||
this.logs.push(log);
|
||||
|
||||
|
||||
// Trim logs if they exceed buffer size
|
||||
if (this.logs.length > this.logBufferSize) {
|
||||
this.logs = this.logs.slice(-this.logBufferSize);
|
||||
}
|
||||
|
||||
|
||||
// Emit log event for potential handlers
|
||||
this.emit('log', log);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user