@debugmcp/mcp-debugger
Version:
Step-through debugging MCP server for LLMs
160 lines • 7.14 kB
JavaScript
/**
* 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