@debugmcp/mcp-debugger
Version:
Run-time step-through debugging for LLM agents.
403 lines • 17.4 kB
JavaScript
/**
* Core worker class for DAP Proxy functionality
* Encapsulates all business logic in a testable form
*/
import path from 'path';
import { ProxyState } from './dap-proxy-interfaces.js';
import { CallbackRequestTracker } from './dap-proxy-request-tracker.js';
import { GenericAdapterManager, DebugpyAdapterManager } from './dap-proxy-adapter-manager.js';
import { DapConnectionManager } from './dap-proxy-connection-manager.js';
import { validateProxyInitPayload, validateAdapterCommand, logAdapterCommandValidation } from '../utils/type-guards.js';
export class DapProxyWorker {
dependencies;
logger = null;
dapClient = null;
adapterProcess = null;
currentSessionId = null;
currentInitPayload = null;
state = ProxyState.UNINITIALIZED;
requestTracker;
processManager = null;
connectionManager = null;
constructor(dependencies) {
this.dependencies = dependencies;
this.requestTracker = new CallbackRequestTracker((requestId, command) => this.handleRequestTimeout(requestId, command));
}
/**
* Get current state for testing
*/
getState() {
return this.state;
}
/**
* Main command handler
*/
async handleCommand(command) {
this.currentSessionId = command.sessionId;
try {
switch (command.cmd) {
case 'init':
await this.handleInitCommand(command);
break;
case 'dap':
await this.handleDapCommand(command);
break;
case 'terminate':
await this.handleTerminate();
break;
}
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.logger?.error(`[Worker] Error handling command ${command.cmd}:`, error);
this.sendError(`Error handling ${command.cmd}: ${message}`);
}
}
/**
* Handle initialization command
*/
async handleInitCommand(payload) {
if (this.state !== ProxyState.UNINITIALIZED) {
throw new Error(`Invalid state for init: ${this.state}`);
}
// Validate payload structure
const validatedPayload = validateProxyInitPayload(payload);
this.state = ProxyState.INITIALIZING;
this.currentInitPayload = validatedPayload;
try {
// Create logger
const logPath = path.join(payload.logDir, `proxy-${payload.sessionId}.log`);
await this.dependencies.fileSystem.ensureDir(path.dirname(logPath));
this.logger = await this.dependencies.loggerFactory(payload.sessionId, payload.logDir);
this.logger.info(`[Worker] DAP Proxy worker initialized for session ${payload.sessionId}`);
// Create managers with logger
// Use generic adapter manager if adapter command is provided, otherwise fall back to Python
if (payload.adapterCommand) {
this.processManager = new GenericAdapterManager(this.dependencies.processSpawner, this.logger, this.dependencies.fileSystem);
}
else {
// Backward compatibility - use Python adapter manager
this.processManager = new DebugpyAdapterManager(this.dependencies.processSpawner, this.logger, this.dependencies.fileSystem);
}
this.connectionManager = new DapConnectionManager(this.dependencies.dapClientFactory, this.logger);
// No path validation - let debugpy handle it
this.logger.info(`[Worker] Script path to debug: ${payload.scriptPath}`);
// Handle dry run
if (payload.dryRunSpawn) {
this.handleDryRun(payload);
return;
}
// Start adapter and connect
await this.startDebugpyAdapterAndConnect(payload);
}
catch (error) {
this.state = ProxyState.UNINITIALIZED;
const message = error instanceof Error ? error.message : String(error);
this.logger?.error(`[Worker] Critical initialization error: ${message}`, error);
// For any initialization error, ensure we shut down
await this.shutdown();
// Exit the process to trigger the 'exit' event in ProxyManager
process.exit(1);
}
}
/**
* Handle dry run mode
*/
handleDryRun(payload) {
let command;
let args;
if (payload.adapterCommand) {
// Use provided adapter command
command = payload.adapterCommand.command;
args = payload.adapterCommand.args;
}
else if (this.processManager instanceof DebugpyAdapterManager) {
// Use Python-specific command building
const spawnCommand = this.processManager.buildSpawnCommand(payload.executablePath, payload.adapterHost, payload.adapterPort, payload.logDir);
command = spawnCommand.command;
args = spawnCommand.args;
}
else {
throw new Error('Cannot determine adapter command for dry run');
}
const fullCommand = `${command} ${args.join(' ')}`;
this.logger.warn(`[Worker DRY_RUN] Would execute: ${fullCommand}`);
this.logger.warn(`[Worker DRY_RUN] Script to debug: ${payload.scriptPath}`);
this.sendStatus('dry_run_complete', { command: fullCommand, script: payload.scriptPath });
// Indicate that the process should terminate
this.state = ProxyState.TERMINATED;
this.logger.info('[Worker DRY_RUN] Dry run complete. State set to TERMINATED.');
}
/**
* Start debugpy adapter and establish connection
*/
async startDebugpyAdapterAndConnect(payload) {
// Spawn adapter process
let spawnResult;
if (payload.adapterCommand) {
// Validate adapter command with detailed logging
try {
const validatedCommand = validateAdapterCommand(payload.adapterCommand, 'proxy-worker-init');
logAdapterCommandValidation(validatedCommand, 'proxy-worker-init', true, {
executablePath: payload.executablePath,
scriptPath: payload.scriptPath
});
this.logger.info('[Worker] Adapter command validated successfully:', {
command: validatedCommand.command,
argsLength: validatedCommand.args.length,
hasEnv: !!validatedCommand.env
});
}
catch (validationError) {
logAdapterCommandValidation(payload.adapterCommand, 'proxy-worker-init', false, {
error: validationError instanceof Error ? validationError.message : String(validationError),
rawPayload: payload
});
throw validationError;
}
// Use validated adapter command
const validatedCommand = validateAdapterCommand(payload.adapterCommand, 'proxy-worker-spawn');
spawnResult = await this.processManager.spawn({
command: validatedCommand.command,
args: validatedCommand.args,
host: payload.adapterHost,
port: payload.adapterPort,
logDir: payload.logDir,
env: validatedCommand.env
});
}
else if (this.processManager instanceof DebugpyAdapterManager) {
// Use Python-specific spawning
spawnResult = await this.processManager.spawnDebugpy({
pythonPath: payload.executablePath,
host: payload.adapterHost,
port: payload.adapterPort,
logDir: payload.logDir
});
}
else {
throw new Error('Cannot determine how to spawn adapter');
}
this.adapterProcess = spawnResult.process;
this.logger.info(`[Worker] Adapter spawned with PID: ${spawnResult.pid}`);
// Monitor adapter process
this.adapterProcess.on('error', (err) => {
this.logger.error('[Worker] Adapter process error:', err);
this.sendError(`Adapter process error: ${err.message}`);
});
this.adapterProcess.on('exit', (code, signal) => {
this.logger.info(`[Worker] Adapter process exited. Code: ${code}, Signal: ${signal}`);
this.sendStatus('adapter_exited', { code, signal });
});
// Connect to adapter
try {
this.dapClient = await this.connectionManager.connectWithRetry(payload.adapterHost, payload.adapterPort);
// Set up event handlers
this.setupDapEventHandlers();
// Initialize DAP session
await this.connectionManager.initializeSession(this.dapClient, payload.sessionId);
// Send launch request
// DIAGNOSTIC: Log the scriptPath before sending to connection manager
this.logger.info('[Worker] DIAGNOSTIC: About to send launch request with scriptPath:', payload.scriptPath);
this.logger.info('[Worker] DIAGNOSTIC: scriptPath type:', typeof payload.scriptPath);
this.logger.info('[Worker] DIAGNOSTIC: scriptPath length:', payload.scriptPath.length);
this.logger.info('[Worker] DIAGNOSTIC: Full payload object:', JSON.stringify(payload, null, 2));
await this.connectionManager.sendLaunchRequest(this.dapClient, payload.scriptPath, payload.scriptArgs, payload.stopOnEntry, payload.justMyCode);
this.logger.info('[Worker] Waiting for "initialized" event from adapter.');
}
catch (error) {
await this.shutdown();
throw error;
}
}
/**
* Set up DAP event handlers
*/
setupDapEventHandlers() {
if (!this.dapClient || !this.connectionManager)
return;
this.connectionManager.setupEventHandlers(this.dapClient, {
onInitialized: async () => {
await this.handleInitializedEvent();
},
onOutput: (body) => {
this.logger.debug('[Worker] DAP event: output', body);
this.sendDapEvent('output', body);
},
onStopped: (body) => {
this.logger.info('[Worker] DAP event: stopped', body);
this.sendDapEvent('stopped', body);
},
onContinued: (body) => {
this.logger.info('[Worker] DAP event: continued', body);
this.sendDapEvent('continued', body);
},
onThread: (body) => {
this.logger.debug('[Worker] DAP event: thread', body);
this.sendDapEvent('thread', body);
},
onExited: (body) => {
this.logger.info('[Worker] DAP event: exited (debuggee)', body);
this.sendDapEvent('exited', body);
},
onTerminated: (body) => {
this.logger.info('[Worker] DAP event: terminated (session)', body);
this.sendDapEvent('terminated', body);
this.shutdown();
},
onError: (err) => {
this.logger.error('[Worker] DAP client error:', err);
this.sendError(`DAP client error: ${err.message}`);
},
onClose: () => {
this.logger.info('[Worker] DAP client connection closed.');
this.sendStatus('dap_connection_closed');
this.shutdown();
}
});
}
/**
* Handle DAP initialized event
*/
async handleInitializedEvent() {
this.logger.info('[Worker] DAP "initialized" event received.');
if (!this.currentInitPayload || !this.dapClient || !this.connectionManager) {
throw new Error('Missing required state in initialized handler');
}
try {
// Set initial breakpoints if provided
if (this.currentInitPayload.initialBreakpoints?.length) {
await this.connectionManager.setBreakpoints(this.dapClient, this.currentInitPayload.scriptPath, this.currentInitPayload.initialBreakpoints);
}
// Send configuration done
await this.connectionManager.sendConfigurationDone(this.dapClient);
// Update state and notify parent
this.state = ProxyState.CONNECTED;
this.sendStatus('adapter_configured_and_launched');
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.logger.error('[Worker] Error in initialized handler:', error);
this.sendError(`Error in DAP sequence: ${message}`);
await this.shutdown();
}
}
/**
* Handle DAP command
*/
async handleDapCommand(payload) {
if (this.state !== ProxyState.CONNECTED || !this.dapClient) {
this.sendDapResponse(payload.requestId, false, undefined, 'DAP client not connected');
return;
}
try {
// Track request
this.requestTracker.track(payload.requestId, payload.dapCommand);
// Log setBreakpoints for debugging
if (payload.dapCommand === 'setBreakpoints') {
this.logger.info(`[Worker] Sending 'setBreakpoints' command. Args:`, payload.dapArgs);
}
// Send request
const response = await this.dapClient.sendRequest(payload.dapCommand, payload.dapArgs);
// Complete tracking
this.requestTracker.complete(payload.requestId);
// Log setBreakpoints response
if (payload.dapCommand === 'setBreakpoints') {
this.logger.info(`[Worker] Response from adapter for 'setBreakpoints':`, response);
}
// Send response
this.sendDapResponse(payload.requestId, true, response);
}
catch (error) {
this.requestTracker.complete(payload.requestId);
const message = error instanceof Error ? error.message : String(error);
this.logger.error(`[Worker] DAP command ${payload.dapCommand} failed:`, { error: message });
this.sendDapResponse(payload.requestId, false, undefined, message);
}
}
/**
* Handle request timeout
*/
handleRequestTimeout(requestId, command) {
this.logger.error(`[Worker] DAP request '${command}' (id: ${requestId}) timed out`);
this.sendDapResponse(requestId, false, undefined, `Request '${command}' timed out`);
}
/**
* Handle terminate command
*/
async handleTerminate() {
this.logger.info('[Worker] Received terminate command.');
await this.shutdown();
this.sendStatus('terminated');
}
/**
* Shutdown the worker
*/
async shutdown() {
if (this.state === ProxyState.SHUTTING_DOWN || this.state === ProxyState.TERMINATED) {
this.logger?.info('[Worker] Shutdown already in progress.');
return;
}
this.state = ProxyState.SHUTTING_DOWN;
this.logger?.info('[Worker] Initiating shutdown sequence...');
// Clear request tracking
this.requestTracker.clear();
// Reject any in-flight DAP requests and clear timers immediately
if (this.dapClient) {
this.dapClient.shutdown('worker shutdown');
}
// Disconnect DAP client
if (this.connectionManager && this.dapClient) {
await this.connectionManager.disconnect(this.dapClient);
}
this.dapClient = null;
// Terminate adapter process
if (this.processManager && this.adapterProcess) {
await this.processManager.shutdown(this.adapterProcess);
}
this.adapterProcess = null;
this.state = ProxyState.TERMINATED;
this.logger?.info('[Worker] Shutdown sequence completed.');
}
// Message sending helpers
sendStatus(status, extra = {}) {
const message = {
type: 'status',
status,
sessionId: this.currentSessionId || 'unknown',
...extra
};
this.dependencies.messageSender.send(message);
}
sendDapResponse(requestId, success, response, error) {
const message = {
type: 'dapResponse',
requestId,
success,
sessionId: this.currentSessionId || 'unknown',
...(success && response ? {
body: response.body,
response: response
} : { error })
};
this.dependencies.messageSender.send(message);
}
sendDapEvent(event, body) {
const message = {
type: 'dapEvent',
event,
body,
sessionId: this.currentSessionId || 'unknown'
};
this.dependencies.messageSender.send(message);
}
sendError(message) {
const errorMessage = {
type: 'error',
message,
sessionId: this.currentSessionId || 'unknown'
};
this.dependencies.messageSender.send(errorMessage);
}
}
//# sourceMappingURL=dap-proxy-worker.js.map