mcp-rewatch
Version:
MCP server that enables Claude Code to manage long-running development processes
212 lines • 9.17 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProcessManager = void 0;
const child_process_1 = require("child_process");
const log_buffer_1 = require("./log-buffer");
class ProcessManager {
processes = new Map();
constructor(processConfigs) {
for (const [name, config] of Object.entries(processConfigs)) {
this.processes.set(name, {
config,
status: 'stopped',
logBuffer: new log_buffer_1.LogBuffer()
});
}
}
async restart(name) {
const managed = this.processes.get(name);
if (!managed) {
throw new Error(`Process '${name}' not found`);
}
await this.stop(name);
managed.logBuffer.clear();
await this.start(name);
// Wait for the process to start up
const startupDelay = managed.config.startupDelay || 3000; // Default 3 seconds
await new Promise(resolve => setTimeout(resolve, startupDelay));
// Check if process is still running
const isRunning = managed.status === 'running';
const logs = managed.logBuffer.getLines(20); // Get initial logs
return {
success: isRunning,
logs
};
}
async start(name) {
const managed = this.processes.get(name);
if (!managed) {
throw new Error(`Process '${name}' not found`);
}
if (managed.status === 'running' || managed.status === 'starting') {
return;
}
managed.status = 'starting';
managed.error = undefined;
try {
const env = {
...process.env,
...managed.config.env
};
const proc = (0, child_process_1.spawn)(managed.config.command, managed.config.args, {
cwd: managed.config.cwd || process.cwd(),
env,
shell: false, // Don't use shell to avoid shell syntax issues
detached: process.platform !== 'win32' // Create new process group on Unix-like systems
});
managed.process = proc;
managed.pid = proc.pid;
managed.startTime = new Date();
// Listen for spawn event to confirm successful start
proc.once('spawn', () => {
managed.status = 'running';
managed.logBuffer.add(`[system] Process started successfully (PID: ${proc.pid})`);
});
proc.stdout?.on('data', (data) => {
const lines = data.toString().split('\n').filter(Boolean);
lines.forEach((line) => managed.logBuffer.add(`[stdout] ${line}`));
});
proc.stderr?.on('data', (data) => {
const lines = data.toString().split('\n').filter(Boolean);
lines.forEach((line) => managed.logBuffer.add(`[stderr] ${line}`));
});
proc.on('error', (error) => {
managed.status = 'error';
// Provide specific error messages based on error code
if (error.code === 'ENOENT') {
managed.error = `Command not found: ${managed.config.command}`;
managed.logBuffer.add(`[error] Command '${managed.config.command}' not found. Check if it's installed and in PATH.`);
}
else if (error.code === 'EACCES') {
managed.error = `Permission denied: ${managed.config.command}`;
managed.logBuffer.add(`[error] Permission denied executing '${managed.config.command}'. Check file permissions.`);
}
else if (error.code === 'ENOTDIR') {
managed.error = `Invalid working directory: ${managed.config.cwd}`;
managed.logBuffer.add(`[error] Working directory '${managed.config.cwd}' is not a directory.`);
}
else if (error.code === 'EMFILE') {
managed.error = 'Too many open files';
managed.logBuffer.add('[error] Too many open files. System limit reached.');
}
else {
managed.error = error.message;
managed.logBuffer.add(`[error] Process error: ${error.message} (${error.code || 'unknown code'})`);
}
});
proc.on('exit', (code, signal) => {
managed.status = 'stopped';
managed.pid = undefined;
if (signal) {
managed.logBuffer.add(`[exit] Process terminated by signal ${signal}`);
}
else if (code === 0) {
managed.logBuffer.add('[exit] Process exited successfully (code 0)');
}
else {
managed.logBuffer.add(`[exit] Process exited with code ${code}`);
}
});
}
catch (error) {
managed.status = 'error';
// Handle spawn errors that might be thrown synchronously
if (error.code === 'ENOENT') {
managed.error = `Command not found: ${managed.config.command}`;
managed.logBuffer.add(`[error] Failed to spawn process: command '${managed.config.command}' not found`);
}
else {
managed.error = error instanceof Error ? error.message : String(error);
managed.logBuffer.add(`[error] Failed to spawn process: ${managed.error}`);
}
throw error;
}
}
async stop(name) {
const managed = this.processes.get(name);
if (!managed || !managed.process) {
return;
}
return new Promise((resolve, reject) => {
const proc = managed.process;
let killed = false;
let forceKillTimer;
const cleanup = (reason) => {
if (!killed) {
killed = true;
clearTimeout(forceKillTimer);
managed.process = undefined;
managed.pid = undefined;
managed.status = 'stopped';
managed.logBuffer.add(`[system] Process stopped: ${reason}`);
resolve();
}
};
proc.once('exit', () => cleanup('graceful shutdown'));
// Send SIGTERM for graceful shutdown
try {
// On Unix-like systems, kill the entire process group
if (process.platform !== 'win32' && proc.pid) {
process.kill(-proc.pid, 'SIGTERM');
managed.logBuffer.add(`[system] Sent SIGTERM signal to process group ${proc.pid}`);
}
else {
proc.kill('SIGTERM');
managed.logBuffer.add('[system] Sent SIGTERM signal for graceful shutdown');
}
}
catch (error) {
managed.logBuffer.add(`[error] Failed to send SIGTERM: ${error.message}`);
cleanup('kill failed');
return;
}
// Force kill after timeout (5 seconds)
forceKillTimer = setTimeout(() => {
if (!killed) {
managed.logBuffer.add('[warning] Process did not respond to SIGTERM within 5 seconds, sending SIGKILL');
try {
// Kill entire process group on Unix-like systems
if (process.platform !== 'win32' && proc.pid) {
process.kill(-proc.pid, 'SIGKILL');
managed.logBuffer.add(`[system] Sent SIGKILL to process group ${proc.pid}`);
}
else {
proc.kill('SIGKILL');
}
}
catch (error) {
managed.logBuffer.add(`[error] Failed to send SIGKILL: ${error.message}`);
}
// Give it another second after SIGKILL
setTimeout(() => cleanup('force killed'), 1000);
}
}, 5000);
});
}
async stopAll() {
const stopPromises = Array.from(this.processes.keys()).map(name => this.stop(name));
await Promise.all(stopPromises);
}
getLogs(name, lines) {
const managed = this.processes.get(name);
if (!managed) {
throw new Error(`Process '${name}' not found`);
}
return managed.logBuffer.getLines(lines);
}
listProcesses() {
return Array.from(this.processes.entries()).map(([name, managed]) => ({
name,
status: managed.status,
pid: managed.pid,
startTime: managed.startTime,
error: managed.error
}));
}
// Expose processes for emergency cleanup
getProcesses() {
return this.processes;
}
}
exports.ProcessManager = ProcessManager;
//# sourceMappingURL=process-manager.js.map