feat(daemon): Add central TSPM daemon and IPC client; refactor CLI to use daemon and improve monitoring/error handling

This commit is contained in:
2025-08-25 08:52:57 +00:00
parent 1c06fb54b9
commit 3ad8f29e1c
23 changed files with 4761 additions and 3252 deletions

View File

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