UNPKG

@debugmcp/mcp-debugger

Version:

Step-through debugging MCP server for LLMs

160 lines 7.14 kB
/** * Generic adapter process management for DAP proxy * Language-agnostic version that can spawn any debug adapter */ /** * Generic adapter manager that can spawn any debug adapter process */ export class GenericAdapterManager { processSpawner; logger; fileSystem; constructor(processSpawner, logger, fileSystem) { this.processSpawner = processSpawner; this.logger = logger; this.fileSystem = fileSystem; } /** * Ensure the log directory exists */ async ensureLogDirectory(logDir) { try { await this.fileSystem.ensureDir(logDir); this.logger.info(`[AdapterManager] Ensured adapter log directory exists: ${logDir}`); } catch (error) { const message = error instanceof Error ? error.message : String(error); this.logger.error(`[AdapterManager] Failed to ensure adapter log directory ${logDir}:`, error); throw new Error(`Failed to create adapter log directory: ${message}`); } } /** * Spawn a generic debug adapter process */ async spawn(config) { const { command, args, logDir, cwd, env } = config; // Ensure log directory exists await this.ensureLogDirectory(logDir); const fullCommand = `${command} ${args.join(' ')}`; this.logger.info(`[AdapterManager] Spawning adapter: ${fullCommand}`); // Spawn options - no cwd manipulation, inherit from parent // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Need dynamic cwd property const spawnOptions = { stdio: ['ignore', 'inherit', 'inherit', 'ipc'], env: env || process.env, detached: true, windowsHide: true }; // Only set cwd if explicitly provided if (cwd) { spawnOptions.cwd = cwd; } // Log critical environment variables for debugging const criticalEnvVars = { NODE_OPTIONS: spawnOptions.env?.NODE_OPTIONS || '<not set>', NODE_DEBUG: spawnOptions.env?.NODE_DEBUG || '<not set>', NODE_ENV: spawnOptions.env?.NODE_ENV || '<not set>', DEBUG: spawnOptions.env?.DEBUG || '<not set>', VSCODE_INSPECTOR_OPTIONS: spawnOptions.env?.VSCODE_INSPECTOR_OPTIONS || '<not set>', // Check for any inspector-related variables hasInspectVars: Object.keys(spawnOptions.env || {}).some(k => k.includes('INSPECT') || k.includes('DEBUG')) }; this.logger.info('[AdapterManager] Spawn configuration:', { command: command, args: args, cwd: cwd || 'inherited', envVars: Object.keys(spawnOptions.env || {}).length, criticalEnvVars }); // Log the full command being executed this.logger.info('[AdapterManager] Full command to execute:', { fullCommand: fullCommand, execArgv: args.filter(arg => arg.startsWith('--inspect')), hasInspectFlag: args.some(arg => arg.includes('--inspect')) }); // Spawn the process const adapterProcess = this.processSpawner.spawn(command, args, spawnOptions); if (!adapterProcess || !adapterProcess.pid) { throw new Error('Failed to spawn adapter process or get PID'); } // Detach and unref so proxy lifecycle is not blocked by child adapter try { adapterProcess.unref(); this.logger.info(`[AdapterManager] Called unref() on adapter process PID: ${adapterProcess.pid}`); } catch { // ignore unref errors (older Node or platform quirk) } // Spawned adapter process; hide console on Windows and keep attached for lifecycle management this.logger.info(`[AdapterManager] Spawned adapter process PID: ${adapterProcess.pid} (windowsHide=${!!spawnOptions.windowsHide}, detached=${!!spawnOptions.detached})`); // Set up error handlers this.setupProcessHandlers(adapterProcess); return { process: adapterProcess, pid: adapterProcess.pid }; } /** * Set up process event handlers */ setupProcessHandlers(process) { process.on('error', (err) => { this.logger.error('[AdapterManager] Adapter process spawn error:', err); }); process.on('exit', (code, signal) => { this.logger.info(`[AdapterManager] Adapter process exited. Code: ${code}, Signal: ${signal}`); }); } /** * Gracefully shutdown an adapter process */ async shutdown(process) { if (!process || !process.pid) { this.logger.info('[AdapterManager] No active adapter process to terminate.'); return; } this.logger.info(`[AdapterManager] Attempting to terminate adapter process PID: ${process.pid}`); try { if (!process.killed) { this.logger.info(`[AdapterManager] Sending SIGTERM to adapter process PID: ${process.pid}`); process.kill('SIGTERM'); // Wait a short period for graceful exit await new Promise(resolve => setTimeout(resolve, 300)); if (!process.killed) { this.logger.warn(`[AdapterManager] Adapter process PID: ${process.pid} did not exit after SIGTERM. Sending SIGKILL.`); try { process.kill('SIGKILL'); } catch { // ignore SIGKILL errors } // Windows-specific fallback: force kill entire process tree if (globalThis.process.platform === 'win32' && process.pid) { try { this.logger.warn(`[AdapterManager] Forcing termination via taskkill /T /F for PID: ${process.pid}`); this.processSpawner.spawn('taskkill', ['/PID', String(process.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true }); } catch (tkErr) { this.logger.error('[AdapterManager] taskkill fallback failed:', tkErr); } } } else { this.logger.info(`[AdapterManager] Adapter process PID: ${process.pid} exited after SIGTERM.`); } } else { this.logger.info(`[AdapterManager] Adapter process PID: ${process.pid} was already marked as killed.`); } } catch (e) { const message = e instanceof Error ? e.message : String(e); this.logger.error(`[AdapterManager] Error during adapter process termination (PID: ${process.pid}): ${message}`, e); } } } // DebugpyAdapterManager removed - functionality moved to PythonAdapterPolicy //# sourceMappingURL=dap-proxy-adapter-manager.js.map