Skip to main content
Glama
by nstrayer
processManager.ts7.6 kB
/** * Process management for daemon processes */ import { ChildProcess, spawn } from 'child_process'; import { RunningProcess, ProcessState, ProcessOutputBuffer, ProcessStatus } from './types.js'; import { logger } from './logger.js'; class ProcessManager { private runningProcesses: Map<string, RunningProcess> = new Map(); private readonly MAX_OUTPUT_LINES = 1000; /** * Create an output buffer for a process */ private createOutputBuffer(): ProcessOutputBuffer { return { stdout: [], stderr: [], maxLines: this.MAX_OUTPUT_LINES }; } /** * Add a line to the output buffer (circular buffer behavior) */ private addToBuffer(buffer: string[], line: string, maxLines: number): void { buffer.push(line); if (buffer.length > maxLines) { buffer.shift(); // Remove oldest line } } /** * Start a daemon process */ async startDaemon(name: string, command: string, cwd: string): Promise<{ success: boolean; message: string }> { // Check if daemon is already running if (this.runningProcesses.has(name)) { const existing = this.runningProcesses.get(name)!; logger.info(`Daemon ${name} is already running with PID ${existing.pid}`); return { success: false, message: `Daemon '${name}' is already running (PID: ${existing.pid})` }; } logger.debug(`Starting daemon ${name}: ${command}`); try { // Start the process using shell to handle complex commands const child = spawn(command, [], { shell: true, cwd, detached: false, stdio: ['ignore', 'pipe', 'pipe'] }); const outputBuffer = this.createOutputBuffer(); // Store the process const runningProcess: RunningProcess = { name, process: child, pid: child.pid!, startTime: new Date(), state: ProcessState.STARTING, outputBuffer }; this.runningProcesses.set(name, runningProcess); // Set up output handlers child.stdout?.on('data', (data) => { const lines = data.toString().split('\n').filter(line => line.trim()); lines.forEach(line => { this.addToBuffer(outputBuffer.stdout, line, outputBuffer.maxLines); logger.debug(`[${name}:stdout] ${line}`); }); }); child.stderr?.on('data', (data) => { const lines = data.toString().split('\n').filter(line => line.trim()); lines.forEach(line => { this.addToBuffer(outputBuffer.stderr, line, outputBuffer.maxLines); logger.debug(`[${name}:stderr] ${line}`); }); }); // Give it a moment to ensure it starts properly await new Promise(resolve => setTimeout(resolve, 1000)); // Check if process is still running if (child.killed || child.exitCode !== null) { this.runningProcesses.delete(name); // Collect any error output const errorOutput = outputBuffer.stderr.join('\n') || 'Process exited immediately'; logger.error(`Daemon ${name} failed to start: ${errorOutput}`); return { success: false, message: `Error: Daemon failed to start:\n${errorOutput}` }; } // Mark as running runningProcess.state = ProcessState.RUNNING; // Set up handlers for when the process exits child.on('exit', (code, signal) => { logger.info(`Daemon ${name} exited with code ${code} and signal ${signal}`); const proc = this.runningProcesses.get(name); if (proc) { proc.state = code === 0 ? ProcessState.STOPPED : ProcessState.FAILED; // Keep the process in the map briefly for status queries setTimeout(() => this.runningProcesses.delete(name), 5000); } }); child.on('error', (err) => { logger.error(`Daemon ${name} error: ${err.message}`); const proc = this.runningProcesses.get(name); if (proc) { proc.state = ProcessState.FAILED; this.addToBuffer(proc.outputBuffer.stderr, `Error: ${err.message}`, proc.outputBuffer.maxLines); } // Keep the process in the map briefly for status queries setTimeout(() => this.runningProcesses.delete(name), 5000); }); logger.info(`Started daemon ${name} with PID ${child.pid}`); return { success: true, message: `Started daemon '${name}' (PID: ${child.pid})` }; } catch (error) { logger.error(`Failed to start daemon ${name}: ${error}`); return { success: false, message: `Failed to start daemon: ${error instanceof Error ? error.message : String(error)}` }; } } /** * Stop a specific daemon process */ stopDaemon(name: string): { success: boolean; message: string } { const running = this.runningProcesses.get(name); if (!running) { return { success: false, message: `Daemon '${name}' is not running` }; } try { logger.info(`Stopping daemon ${name} (PID: ${running.pid})`); running.process.kill('SIGTERM'); this.runningProcesses.delete(name); return { success: true, message: `Stopped daemon '${name}' (PID: ${running.pid})` }; } catch (error) { logger.error(`Failed to stop daemon ${name}: ${error}`); return { success: false, message: `Failed to stop daemon: ${error instanceof Error ? error.message : String(error)}` }; } } /** * Check if a daemon is running */ isDaemonRunning(name: string): boolean { const proc = this.runningProcesses.get(name); return proc !== undefined && proc.state === ProcessState.RUNNING; } /** * Get detailed status of a specific daemon */ getProcessStatus(name: string): ProcessStatus | null { const proc = this.runningProcesses.get(name); if (!proc) { return null; } const uptime = Math.floor((Date.now() - proc.startTime.getTime()) / 1000); const recentOutput = [ ...proc.outputBuffer.stdout.slice(-10), ...proc.outputBuffer.stderr.slice(-5).map(line => `[stderr] ${line}`) ]; return { name: proc.name, pid: proc.pid, state: proc.state, startTime: proc.startTime, uptime, recentOutput }; } /** * Get logs from a specific daemon */ getProcessLogs(name: string, lines: number = 50): { stdout: string[]; stderr: string[] } | null { const proc = this.runningProcesses.get(name); if (!proc) { return null; } return { stdout: proc.outputBuffer.stdout.slice(-lines), stderr: proc.outputBuffer.stderr.slice(-lines) }; } /** * Get information about running daemons */ getRunningDaemons(): Array<{ name: string; pid: number; startTime: Date; state: ProcessState }> { return Array.from(this.runningProcesses.values()).map(p => ({ name: p.name, pid: p.pid, startTime: p.startTime, state: p.state })); } /** * Clean up all running processes */ cleanup(): void { logger.info(`Cleaning up ${this.runningProcesses.size} running processes`); for (const [name, proc] of this.runningProcesses) { try { logger.info(`Terminating daemon '${name}' (PID: ${proc.pid})`); proc.process.kill('SIGTERM'); // Give it time to terminate gracefully setTimeout(() => { if (!proc.process.killed) { logger.warn(`Force killing daemon '${name}'`); proc.process.kill('SIGKILL'); } }, 5000); } catch (error) { logger.error(`Error terminating daemon '${name}': ${error}`); } } this.runningProcesses.clear(); } } // Export singleton instance export const processManager = new ProcessManager(); // Set up cleanup on process exit process.on('SIGINT', () => { logger.info('Received SIGINT, cleaning up processes...'); processManager.cleanup(); process.exit(0); }); process.on('SIGTERM', () => { logger.info('Received SIGTERM, cleaning up processes...'); processManager.cleanup(); process.exit(0); }); process.on('exit', () => { processManager.cleanup(); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/nstrayer/simple-commands-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server