UNPKG

mcp-subagents

Version:

Multi-Agent AI Orchestration via Model Context Protocol - Access specialized CLI AI agents (Aider, Qwen, Gemini, Goose, etc.) with intelligent fallback and configuration

271 lines 8.94 kB
/** * Process Manager * * Handles proper process spawning, management, and cleanup to prevent * zombie processes and ensure all child processes are terminated. */ import { spawn } from 'child_process'; import { promisify } from 'util'; import { exec as execCallback } from 'child_process'; const exec = promisify(execCallback); /** * Spawn a process with proper management and cleanup */ export class ManagedProcess { process = null; output = []; killed = false; cleanupTimer; earlyFailCallback; async spawn(options) { const { command, args, env, cwd, timeout } = options; // Use provided environment directly const processEnv = env; // Spawn with process group (detached: true on Unix) this.process = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'], shell: false, env: processEnv, cwd, detached: process.platform !== 'win32', // Create new process group on Unix }); // Set up output capture with early error detection this.process.stdout?.on('data', (data) => { const lines = data.toString().split('\n').filter(line => line.trim()); this.output.push(...lines); this.checkForEarlyErrors(lines); }); this.process.stderr?.on('data', (data) => { const lines = data.toString().split('\n').filter(line => line.trim()); this.output.push(...lines); this.checkForEarlyErrors(lines); }); // Set up timeout if specified - with race condition protection if (timeout && timeout > 0) { this.cleanupTimer = setTimeout(() => { if (!this.killed && this.process && this.process.exitCode === null) { this.terminate(); } }, timeout); } // Wait for process completion return new Promise((resolve, reject) => { if (!this.process) { reject(new Error('Process not initialized')); return; } // Set up early fail callback this.earlyFailCallback = (_reason) => { if (this.cleanupTimer) { clearTimeout(this.cleanupTimer); delete this.cleanupTimer; } this.terminate(); const result = { output: this.output, exitCode: 1, // Indicate failure signal: null }; // Use setImmediate to ensure immediate resolution setImmediate(() => resolve(result)); }; this.process.on('exit', (code, signal) => { // Clear timeout immediately to prevent race conditions if (this.cleanupTimer) { clearTimeout(this.cleanupTimer); delete this.cleanupTimer; } // Resolve immediately - no additional delays const result = { output: this.output, exitCode: code, signal }; // Use setImmediate to ensure resolution happens in next tick setImmediate(() => resolve(result)); }); this.process.on('error', (error) => { // Clear timeout immediately if (this.cleanupTimer) { clearTimeout(this.cleanupTimer); delete this.cleanupTimer; } // Reject immediately setImmediate(() => reject(error)); }); }); } /** * Send input to the process via stdin */ sendInput(input) { if (this.process?.stdin && !this.process.stdin.destroyed) { this.process.stdin.write(input); this.process.stdin.end(); } } /** * Terminate the process and all its children */ async terminate() { if (this.killed || !this.process) { return; } this.killed = true; const pid = this.process.pid; if (!pid) { return; } try { if (process.platform === 'win32') { // On Windows, use taskkill to kill the process tree await this.killWindowsProcessTree(pid); } else { // On Unix, kill the process group await this.killUnixProcessGroup(pid); } } catch (error) { console.error(`Failed to terminate process ${pid}:`, error); // Last resort: try basic kill try { this.process.kill('SIGKILL'); } catch { // Ignore errors if the process is already gone } } } async killUnixProcessGroup(pid) { try { // First try SIGTERM to the process group process.kill(-pid, 'SIGTERM'); // Give it 2 seconds to clean up await new Promise(resolve => setTimeout(resolve, 2000)); // Check if still running and force kill if needed try { process.kill(-pid, 0); // Check if process group exists // Still running, force kill process.kill(-pid, 'SIGKILL'); } catch { // Process group is gone, good } } catch (error) { if (error.code !== 'ESRCH') { // ESRCH means process not found throw error; } } } async killWindowsProcessTree(pid) { try { // Kill the process tree on Windows await exec(`taskkill /F /T /PID ${pid}`); } catch { // Fallback to basic kill this.process?.kill('SIGKILL'); } } /** * Get the process ID */ getPid() { return this.process?.pid; } /** * Check if the process is still running */ isRunning() { return this.process !== null && !this.killed && this.process.exitCode === null; } /** * Check output lines for patterns that indicate early failure */ checkForEarlyErrors(lines) { if (!this.earlyFailCallback) return; for (const line of lines) { const lowerLine = line.toLowerCase(); // Gemini specific error patterns if (lowerLine.includes('quota exceeded') && lowerLine.includes('gemini')) { this.earlyFailCallback('Gemini quota exceeded'); return; } // Generic API rate limit patterns if (lowerLine.includes('rate limit') || lowerLine.includes('429')) { this.earlyFailCallback('Rate limit exceeded'); return; } // Authentication errors if (lowerLine.includes('unauthorized') || lowerLine.includes('401')) { this.earlyFailCallback('Authentication failed'); return; } // Permission errors if (lowerLine.includes('permission denied') || lowerLine.includes('403')) { this.earlyFailCallback('Permission denied'); return; } // Generic error patterns that suggest hanging if (lowerLine.includes('error:') && (lowerLine.includes('gaxioserror') || lowerLine.includes('connection'))) { this.earlyFailCallback('Connection error detected'); return; } } } } /** * Check if we're running as a child of an MCP server */ export function isSpawnedByMCPServer() { return process.env['MCP_SPAWNED_BY_SERVER'] === '1'; } /** * Process pool to manage concurrent processes */ export class ProcessPool { processes = new Map(); /** * Add a process to the pool */ add(id, process) { this.processes.set(id, process); } /** * Remove a process from the pool */ remove(id) { this.processes.delete(id); } /** * Get a process by ID */ get(id) { return this.processes.get(id); } /** * Terminate all processes in the pool */ async terminateAll() { const promises = Array.from(this.processes.values()).map(process => process.terminate()); await Promise.allSettled(promises); this.processes.clear(); } /** * Get the number of running processes */ getRunningCount() { let count = 0; for (const process of this.processes.values()) { if (process.isRunning()) { count++; } } return count; } } //# sourceMappingURL=process-manager.js.map