@debugmcp/mcp-debugger
Version:
Step-through debugging MCP server for LLMs
300 lines • 12.5 kB
JavaScript
/**
* DAP Proxy Core - Pure business logic without side effects
*
* This module contains the core proxy runner functionality that can be
* instantiated and controlled programmatically without auto-execution.
*/
import readline from 'readline';
import { DapProxyWorker } from './dap-proxy-worker.js';
import { MessageParser } from './dap-proxy-message-parser.js';
import { ProxyState } from './dap-proxy-interfaces.js';
import { getErrorMessage } from '../errors/debug-errors.js';
/**
* Core proxy runner that encapsulates all proxy logic
* without auto-execution or environment detection
*/
export class ProxyRunner {
dependencies;
options;
worker;
logger;
rl;
messageHandler;
isRunning = false;
_initTimeout;
ipcMessageCounter = 0;
heartbeatInterval;
heartbeatTickCounter = 0;
constructor(dependencies, logger, options = {}) {
this.dependencies = dependencies;
this.options = options;
this.worker = new DapProxyWorker(dependencies);
this.logger = logger;
}
/**
* Start the proxy runner and set up communication channels
*/
async start() {
if (this.isRunning) {
throw new Error('Proxy runner is already running');
}
this.isRunning = true;
this.logger.info('[ProxyRunner] Starting proxy runner...');
try {
// Set up message processing
const processMessage = this.options.onMessage || this.createMessageProcessor();
// Set up communication channels based on options and availability
if (this.options.useIPC !== false && typeof process.send === 'function') {
this.setupIPCCommunication(processMessage);
}
else if (this.options.useStdin !== false) {
this.setupStdinCommunication(processMessage);
}
else {
this.logger.warn('[ProxyRunner] No communication channel configured');
}
this.logger.info('[ProxyRunner] Ready to receive commands');
if (typeof process.send === 'function') {
this.heartbeatInterval = setInterval(() => {
try {
this.heartbeatTickCounter += 1;
this.logger.debug(`[ProxyRunner] Heartbeat tick #${this.heartbeatTickCounter} send attempt (process.connected=${process.connected})`);
process.send?.({
type: 'ipc-heartbeat-tick',
timestamp: Date.now(),
counter: this.heartbeatTickCounter
});
}
catch (tickError) {
this.logger.warn('[ProxyRunner] Failed to send heartbeat tick:', tickError);
}
}, 5000);
}
// Set up initialization timeout - exit if no init command received
// This prevents orphaned processes from consuming resources
// Use much shorter timeout to prevent resource consumption
const timeoutDuration = 10000; // 10 seconds - should be enough for normal initialization
const initTimeout = setTimeout(() => {
this.logger.warn(`[ProxyRunner] No initialization received within ${timeoutDuration / 1000} seconds, exiting...`);
process.exit(1);
}, timeoutDuration);
// Store timeout so we can clear it when init is received
this._initTimeout = initTimeout;
}
catch (error) {
this.isRunning = false;
this.logger.error('[ProxyRunner] Failed to start:', error);
throw error;
}
}
/**
* Stop the proxy runner and clean up resources
*/
async stop() {
if (!this.isRunning) {
return;
}
this.logger.info('[ProxyRunner] Stopping proxy runner...');
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = undefined;
}
// Shutdown worker
await this.worker.shutdown();
// Clean up communication channels
if (this.messageHandler && process.removeListener) {
process.removeListener('message', this.messageHandler);
}
if (this.rl) {
this.rl.close();
}
this.isRunning = false;
this.logger.info('[ProxyRunner] Stopped');
}
/**
* Get the current worker state
*/
getWorkerState() {
return this.worker.getState();
}
/**
* Get the worker instance (for testing)
*/
getWorker() {
return this.worker;
}
/**
* Create the default message processor
*/
createMessageProcessor() {
return async (messageStr) => {
this.logger.info(`[ProxyRunner] Received message (first 200 chars): ${messageStr.substring(0, 200)}...`);
let command = null;
try {
command = MessageParser.parseCommand(messageStr);
// Clear initialization timeout when init command is received
if (command.cmd === 'init' && this._initTimeout) {
clearTimeout(this._initTimeout);
this._initTimeout = undefined;
this.logger.info('[ProxyRunner] Initialization timeout cleared');
}
await this.worker.handleCommand(command);
}
catch (error) {
const errorMsg = getErrorMessage(error);
this.logger.error('[ProxyRunner] Error processing message:', { error: errorMsg });
this.dependencies.messageSender.send({
type: 'error',
message: `Proxy error processing command: ${errorMsg}`,
sessionId: command?.sessionId || 'unknown'
});
}
// Check if we should exit after handling the command
if (this.worker.getState() === ProxyState.TERMINATED) {
const isDryRun = command?.cmd === 'init' && command.dryRunSpawn;
const exitDelay = isDryRun ? 500 : 0;
this.logger.info(`[ProxyRunner] Worker state is TERMINATED. Exiting in ${exitDelay}ms.`);
setTimeout(() => {
process.exit(0);
}, exitDelay);
}
};
}
/**
* Set up IPC communication channel
*/
setupIPCCommunication(processMessage) {
this.logger.info('[ProxyRunner] Setting up IPC communication');
// Test if IPC channel exists
if (typeof process.send !== 'function') {
this.logger.error('[ProxyRunner] ERROR: process.send is not a function - IPC channel not available!');
return;
}
this.logger.info('[ProxyRunner] IPC channel confirmed available');
this.messageHandler = async (message) => {
this.ipcMessageCounter += 1;
this.logger.info(`[ProxyRunner] IPC message #${this.ipcMessageCounter} received type=${typeof message}`);
this.logger.debug(`[ProxyRunner] IPC listener count=${process.listenerCount('message')}`);
this.logger.debug(`[ProxyRunner] Raw message snapshot:`, message);
this.logger.debug('[ProxyRunner] IPC message received (raw):', JSON.stringify(message).substring(0, 200));
this.logger.debug(`[ProxyRunner] IPC channel status on receive: connected=${process.connected}`);
if (typeof process.send === 'function') {
try {
process.send({
type: 'ipc-heartbeat',
counter: this.ipcMessageCounter,
timestamp: Date.now()
});
}
catch (heartbeatError) {
this.logger.warn('[ProxyRunner] Failed to send heartbeat:', heartbeatError);
}
}
try {
if (typeof message === 'string') {
await processMessage(message);
this.logger.debug(`[ProxyRunner] IPC message #${this.ipcMessageCounter} processed successfully (string)`);
}
else if (typeof message === 'object' && message !== null) {
this.logger.debug('[ProxyRunner] Received object message, stringifying');
try {
await processMessage(JSON.stringify(message));
this.logger.debug(`[ProxyRunner] IPC message #${this.ipcMessageCounter} processed successfully (object)`);
}
catch (e) {
this.logger.error('[ProxyRunner] Could not process object message:', {
message,
error: getErrorMessage(e)
});
throw e;
}
}
else {
this.logger.warn('[ProxyRunner] Received message of unexpected type:', typeof message, message);
}
}
catch (handlerError) {
this.logger.error('[ProxyRunner] Error handling IPC message:', handlerError);
}
};
process.on('message', this.messageHandler);
this.logger.info('[ProxyRunner] IPC message handler attached');
process.on('disconnect', () => {
this.logger.warn('[ProxyRunner] IPC channel disconnected');
});
process.on('error', (err) => {
this.logger.error('[ProxyRunner] IPC channel error:', err);
});
}
/**
* Set up stdin/readline communication channel
*/
setupStdinCommunication(processMessage) {
this.logger.info('[ProxyRunner] Setting up stdin/readline communication');
this.rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false
});
this.rl.on('line', (line) => processMessage(line));
}
/**
* Set up global error handlers
*/
setupGlobalErrorHandlers(errorShutdown, getCurrentSessionId) {
// Uncaught exception handler
process.on('uncaughtException', (error) => {
this.logger.error('[ProxyRunner] Uncaught exception:', error);
const sessionId = getCurrentSessionId() || 'unknown';
this.dependencies.messageSender.send({
type: 'error',
message: `Proxy uncaught exception: ${error.message}`,
sessionId
});
errorShutdown().finally(() => {
process.exit(1);
});
});
// Unhandled rejection handler
process.on('unhandledRejection', (reason, promise) => {
this.logger.error('[ProxyRunner] Unhandled rejection:', { reason, promise });
const sessionId = getCurrentSessionId() || 'unknown';
this.dependencies.messageSender.send({
type: 'error',
message: `Proxy unhandled rejection: ${reason}`,
sessionId
});
});
// SIGTERM handler
process.on('SIGTERM', () => {
this.logger.info('[ProxyRunner] Received SIGTERM, shutting down gracefully');
errorShutdown().finally(() => {
process.exit(0);
});
});
// SIGINT handler
process.on('SIGINT', () => {
this.logger.info('[ProxyRunner] Received SIGINT, shutting down gracefully');
errorShutdown().finally(() => {
process.exit(0);
});
});
}
}
/**
* Detect if the module is being run directly or as a worker
*/
export function detectExecutionMode() {
const isDirectRun = (typeof require !== 'undefined' && require.main === module) ||
(typeof import.meta !== 'undefined' && import.meta.url === `file://${process.argv[1]}`);
const hasIPC = typeof process.send === 'function';
const isWorkerEnv = process.env.DAP_PROXY_WORKER === 'true';
return { isDirectRun, hasIPC, isWorkerEnv };
}
/**
* Check if the module should auto-execute based on execution mode
*/
export function shouldAutoExecute(mode) {
return mode.isDirectRun || mode.hasIPC || mode.isWorkerEnv;
}
//# sourceMappingURL=dap-proxy-core.js.map