UNPKG

@debugmcp/mcp-debugger

Version:

Step-through debugging MCP server for LLMs

754 lines 33.6 kB
/** * ProxyManager - Handles spawning and communication with debug proxy processes */ import { EventEmitter } from 'events'; import { v4 as uuidv4 } from 'uuid'; import path from 'path'; import { fileURLToPath } from 'url'; import { createInitialState, handleProxyMessage, isValidProxyMessage, addPendingRequest, removePendingRequest, clearPendingRequests } from '../dap-core/index.js'; import { ErrorMessages } from '../utils/error-messages.js'; const DEFAULT_RUNTIME_ENVIRONMENT = { moduleUrl: import.meta.url, cwd: () => process.cwd() }; /** * Concrete implementation of ProxyManager */ export class ProxyManager extends EventEmitter { adapter; proxyProcessLauncher; fileSystem; logger; proxyProcess = null; sessionId = null; currentThreadId = null; pendingDapRequests = new Map(); isInitialized = false; isDryRun = false; dryRunCompleteReceived = false; dryRunCommandSnapshot; dryRunScriptPath; adapterConfigured = false; dapState = null; stderrBuffer = []; lastExitDetails; runtimeEnv; activeLaunchBarrier = null; activeLaunchBarrierRequestId = null; proxyMessageCounter = 0; constructor(adapter, // Optional adapter for language-agnostic support proxyProcessLauncher, fileSystem, logger, runtimeEnv = DEFAULT_RUNTIME_ENVIRONMENT) { super(); this.adapter = adapter; this.proxyProcessLauncher = proxyProcessLauncher; this.fileSystem = fileSystem; this.logger = logger; this.runtimeEnv = runtimeEnv; } async start(config) { if (this.proxyProcess) { throw new Error('Proxy already running'); } this.sessionId = config.sessionId; this.isDryRun = config.dryRunSpawn === true; this.dryRunCompleteReceived = false; this.dryRunCommandSnapshot = undefined; this.dryRunScriptPath = config.scriptPath; this.lastExitDetails = undefined; if (config.adapterCommand?.command) { const parts = [config.adapterCommand.command, ...(config.adapterCommand.args ?? [])] .filter((part) => typeof part === 'string' && part.length > 0); if (parts.length > 0) { this.dryRunCommandSnapshot = parts.join(' '); } } else if (!this.dryRunCommandSnapshot && config.executablePath) { this.dryRunCommandSnapshot = config.executablePath; } // Initialize functional core state this.dapState = createInitialState(config.sessionId); const { executablePath, proxyScriptPath, env } = await this.prepareSpawnContext(config); this.logger.info(`[ProxyManager] Spawning proxy for session ${config.sessionId}. Path: ${proxyScriptPath}`); try { this.proxyProcess = this.proxyProcessLauncher.launchProxy(proxyScriptPath, config.sessionId, env); } catch (error) { this.logger.error(`[ProxyManager] Failed to spawn proxy:`, error); throw error; } if (!this.proxyProcess || typeof this.proxyProcess.pid === 'undefined') { throw new Error('Proxy process is invalid or PID is missing'); } this.logger.info(`[ProxyManager] Proxy spawned with PID: ${this.proxyProcess.pid}`); // Set up event handlers this.setupEventHandlers(); // Wait a brief moment for the process to start before sending init await new Promise(resolve => setTimeout(resolve, 50)); // Send initialization command with retry logic const initCommand = { cmd: 'init', sessionId: config.sessionId, executablePath: executablePath, // Using resolved executable path adapterHost: config.adapterHost, adapterPort: config.adapterPort, logDir: config.logDir, scriptPath: config.scriptPath, scriptArgs: config.scriptArgs, stopOnEntry: config.stopOnEntry, justMyCode: config.justMyCode, initialBreakpoints: config.initialBreakpoints, dryRunSpawn: config.dryRunSpawn, launchConfig: config.launchConfig, // Pass adapter command info for language-agnostic adapter spawning adapterCommand: config.adapterCommand }; // Debug log the command being sent this.logger.info(`[ProxyManager] Sending init command with adapterCommand:`, { hasAdapterCommand: !!config.adapterCommand, adapterCommand: config.adapterCommand ? { command: config.adapterCommand.command, args: config.adapterCommand.args, hasEnv: !!config.adapterCommand.env } : null }); // Send init command with retry logic await this.sendInitWithRetry(initCommand); // Wait for initialization or dry run completion return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error(ErrorMessages.proxyInitTimeout(30))); }, 30000); const cleanup = () => { clearTimeout(timeout); this.removeListener('initialized', handleInitialized); this.removeListener('dry-run-complete', handleDryRun); this.removeListener('error', handleError); this.removeListener('exit', handleExit); }; const handleInitialized = () => { this.isInitialized = true; cleanup(); resolve(); }; const handleDryRun = () => { cleanup(); resolve(); }; const handleError = (error) => { cleanup(); reject(error); }; const handleExit = (code, signal) => { cleanup(); if (this.isDryRun && code === 0) { // Normal exit for dry run resolve(); } else { let errorMessage = `Proxy exited during initialization. Code: ${code}, Signal: ${signal}`; if (this.stderrBuffer.length > 0) { errorMessage += `\nStderr output:\n${this.stderrBuffer.join('\n')}`; } reject(new Error(errorMessage)); } }; this.once('initialized', handleInitialized); this.once('dry-run-complete', handleDryRun); this.once('error', handleError); this.once('exit', handleExit); }); } async stop() { if (!this.proxyProcess) { return; } this.logger.info(`[ProxyManager] Stopping proxy for session ${this.sessionId}`); // Mark as shutting down to stop processing new messages const process = this.proxyProcess; // Immediately cleanup to prevent "unknown request" warnings this.cleanup(); // Send terminate command if process is still running try { if (!process.killed) { process.send({ cmd: 'terminate', sessionId: this.sessionId }); } } catch (error) { this.logger.error(`[ProxyManager] Error sending terminate command:`, error); } // Wait for graceful exit or force kill after timeout return new Promise((resolve) => { const timeout = setTimeout(() => { this.logger.warn(`[ProxyManager] Timeout waiting for proxy exit. Force killing.`); if (!process.killed) { process.kill('SIGKILL'); } resolve(); }, 5000); process.once('exit', () => { clearTimeout(timeout); resolve(); }); // If already killed/exited, resolve immediately if (process.killed || process.exitCode !== null) { clearTimeout(timeout); resolve(); } }); } async sendDapRequest(command, args) { if (!this.proxyProcess || !this.isInitialized) { throw new Error('Proxy not initialized'); } const barrier = this.adapter?.createLaunchBarrier?.(command, args); const requestId = uuidv4(); const commandToSend = { cmd: 'dap', sessionId: this.sessionId, requestId, dapCommand: command, dapArgs: args }; if (barrier && !barrier.awaitResponse) { this.logger.info(`[ProxyManager] Sending DAP command with adapter barrier (fire-and-forget): ${command}, requestId: ${requestId}`); this.setActiveLaunchBarrier(barrier, requestId); barrier.onRequestSent(requestId); try { this.sendCommand(commandToSend); } catch (error) { this.clearActiveLaunchBarrier(barrier); throw error; } try { await barrier.waitUntilReady(); return {}; } finally { this.clearActiveLaunchBarrier(barrier); } } this.logger.info(`[ProxyManager] Sending DAP command: ${command}, requestId: ${requestId}`); if (barrier) { this.setActiveLaunchBarrier(barrier, requestId); barrier.onRequestSent(requestId); } return new Promise((resolve, reject) => { this.pendingDapRequests.set(requestId, { resolve: resolve, reject, command }); // Mirror into functional core for observability (ProxyManager remains authoritative) if (this.dapState) { this.dapState = addPendingRequest(this.dapState, { requestId, command, seq: 0, timestamp: Date.now() }); } try { this.sendCommand(commandToSend); } catch (error) { this.pendingDapRequests.delete(requestId); if (barrier) { this.clearActiveLaunchBarrier(barrier); } reject(error); } // Timeout handler setTimeout(() => { if (this.pendingDapRequests.has(requestId)) { this.pendingDapRequests.delete(requestId); if (this.activeLaunchBarrier && this.activeLaunchBarrierRequestId === requestId) { this.clearActiveLaunchBarrier(); } reject(new Error(ErrorMessages.dapRequestTimeout(command, 35))); } }, 35000); }); } isRunning() { return this.proxyProcess !== null && !this.proxyProcess.killed; } getCurrentThreadId() { return this.currentThreadId; } async prepareSpawnContext(config) { let executablePath = config.executablePath; if (this.adapter) { const validation = await this.adapter.validateEnvironment(); if (!validation.valid) { throw new Error(`Invalid environment for ${this.adapter.language}: ${validation.errors[0].message}`); } if (!executablePath) { executablePath = await this.adapter.resolveExecutablePath(); this.logger.info(`[ProxyManager] Adapter resolved executable path: ${executablePath}`); } } else if (!executablePath) { throw new Error('No executable path provided and no adapter available to resolve it'); } const proxyScriptPath = await this.findProxyScript(); if (!executablePath) { throw new Error('Executable path could not be determined after validation'); } const env = this.cloneProcessEnv(); return { executablePath, proxyScriptPath, env }; } cloneProcessEnv() { const env = {}; for (const [key, value] of Object.entries(process.env)) { if (value !== undefined) { env[key] = value; } } return env; } async findProxyScript() { const modulePath = fileURLToPath(this.runtimeEnv.moduleUrl); const moduleDir = path.dirname(modulePath); const dirParts = moduleDir.split(path.sep); const cwd = this.runtimeEnv.cwd(); const lastPart = dirParts[dirParts.length - 1]; const secondLast = dirParts[dirParts.length - 2]; let distPath; if (lastPart === 'dist') { distPath = path.join(moduleDir, 'proxy', 'proxy-bootstrap.js'); } else if (lastPart === 'proxy' && secondLast === 'dist') { distPath = path.join(moduleDir, 'proxy-bootstrap.js'); } else { // Fallback to development layout distPath = path.resolve(moduleDir, '../../dist/proxy/proxy-bootstrap.js'); } this.logger.info(`[ProxyManager] Checking for proxy script at: ${distPath}`); if (!(await this.fileSystem.pathExists(distPath))) { throw new Error(`Bootstrap worker script not found at: ${distPath}\n` + `Module directory: ${moduleDir}\n` + `Current working directory: ${cwd}\n` + `This usually means:\n` + ` 1. You need to run 'npm run build' first\n` + ` 2. The build failed to copy proxy files\n` + ` 3. The TypeScript compilation structure is unexpected`); } return distPath; } async sendInitWithRetry(initCommand) { const maxRetries = 5; const delays = [500, 1000, 2000, 4000, 8000]; // More generous backoff for Windows CI let lastError; for (let attempt = 0; attempt <= maxRetries; attempt++) { const timeoutMs = delays[Math.min(attempt, delays.length - 1)]; try { const received = await new Promise((resolve, reject) => { let resolved = false; const handler = () => { if (resolved) return; resolved = true; if (timer) clearTimeout(timer); resolve(true); }; const cleanup = () => { this.removeListener('init-received', handler); if (timer) clearTimeout(timer); }; this.on('init-received', handler); const timer = setTimeout(() => { if (resolved) return; resolved = true; this.removeListener('init-received', handler); resolve(false); }, timeoutMs); try { this.sendCommand(initCommand); } catch (error) { cleanup(); reject(error); } }); if (received) { this.logger.info(`[ProxyManager] Init command acknowledged on attempt ${attempt + 1}`); return; } this.logger.warn(`[ProxyManager] Init not acknowledged, attempt ${attempt + 1}/${maxRetries + 1}`); } catch (error) { lastError = error; this.logger.warn(`[ProxyManager] Error sending init on attempt ${attempt + 1}: ${lastError.message}`); } if (attempt < maxRetries) { const waitMs = delays[Math.min(attempt, delays.length - 1)]; await new Promise((resolve) => setTimeout(resolve, waitMs)); } } let detailMessage = `Failed to initialize proxy after ${maxRetries + 1} attempts. ${lastError ? `Last error: ${lastError.message}` : 'Init command not acknowledged'}`; if (this.lastExitDetails) { const { code, signal, capturedStderr } = this.lastExitDetails; const stderrSnippet = capturedStderr.length ? capturedStderr.slice(-10).join('\n') : '<<no stderr captured>>'; detailMessage += ` Proxy exit details -> code=${code} signal=${signal} stderr:\n${stderrSnippet}`; } throw new Error(detailMessage); } sendCommand(command) { if (!this.proxyProcess || this.proxyProcess.killed) { if (this.lastExitDetails) { this.logger.error(`[ProxyManager] Attempted to send command after proxy unavailable. Last exit -> code=${this.lastExitDetails.code} signal=${this.lastExitDetails.signal}`, this.lastExitDetails.capturedStderr); } else { this.logger.error('[ProxyManager] Attempted to send command but proxy process is not available (no exit details recorded).'); } throw new Error('Proxy process not available'); } const rawChild = this.proxyProcess .childProcess; const requestId = command.requestId; const cmd = command.cmd; const dapCommand = command.dapCommand; const connectedBefore = rawChild && typeof rawChild.connected === 'boolean' ? rawChild.connected : undefined; const childPid = rawChild?.pid; this.logger.debug(`[ProxyManager] IPC pre-send pid=${childPid ?? 'unknown'} connected=${connectedBefore} cmd=${cmd}${dapCommand ? `/${dapCommand}` : ''} requestId=${requestId ?? 'n/a'}`); this.logger.info(`[ProxyManager] Sending command to proxy: ${JSON.stringify(command).substring(0, 500)}`); try { this.proxyProcess.sendCommand(command); this.logger.info(`[ProxyManager] Command dispatched via proxy process`); const connectedAfter = rawChild && typeof rawChild.connected === 'boolean' ? rawChild.connected : undefined; this.logger.debug(`[ProxyManager] IPC post-send pid=${childPid ?? 'unknown'} connected=${connectedAfter} cmd=${cmd}${dapCommand ? `/${dapCommand}` : ''} requestId=${requestId ?? 'n/a'}`); } catch (error) { const connectedAfter = rawChild && typeof rawChild.connected === 'boolean' ? rawChild.connected : undefined; this.logger.error(`[ProxyManager] Failed to send command (pid=${childPid ?? 'unknown'} connected=${connectedAfter} cmd=${cmd}${dapCommand ? `/${dapCommand}` : ''} requestId=${requestId ?? 'n/a'})`, error); this.logger.error(`[ProxyManager] Failed to send command:`, error); throw error; } } setupEventHandlers() { if (!this.proxyProcess) return; // Handle IPC messages this.proxyProcess.on('message', (rawMessage) => { this.handleProxyMessage(rawMessage); }); this.proxyProcess.on('ipc-send-start', (data) => { this.logger.debug(`[ProxyManager] IPC send start pid=${data?.pid ?? 'unknown'} connected=${data?.connectedBefore} summary=${data?.summary ?? 'n/a'}`); }); this.proxyProcess.on('ipc-send-complete', (data) => { this.logger.debug(`[ProxyManager] IPC send complete pid=${data?.pid ?? 'unknown'} connected=${data?.connectedAfter} summary=${data?.summary ?? 'n/a'} queueBefore=${data?.queueSizeBefore ?? 'n/a'} queueAfter=${data?.queueSizeAfter ?? 'n/a'}`); }); this.proxyProcess.on('ipc-send-failed', (data) => { this.logger.warn(`[ProxyManager] IPC send returned false pid=${data?.pid ?? 'unknown'} killed=${data?.killed} childKilled=${data?.childProcessKilled} summary=${data?.summary ?? 'n/a'}`); }); this.proxyProcess.on('ipc-send-error', (data) => { this.logger.error(`[ProxyManager] IPC send error pid=${data?.pid ?? 'unknown'} error=${data?.error ?? 'unknown'} summary=${data?.summary ?? 'n/a'}`); }); // Handle stderr this.proxyProcess.stderr?.on('data', (data) => { const output = data.toString().trim(); this.logger.error(`[ProxyManager STDERR] ${output}`); // Capture stderr for error reporting during initialization if (!this.isInitialized) { this.stderrBuffer.push(output); } }); // Handle exit this.proxyProcess.on('exit', (code, signal) => { this.logger.info(`[ProxyManager] Proxy exited. Code: ${code}, Signal: ${signal}`); this.lastExitDetails = { code, signal, timestamp: Date.now(), capturedStderr: [...this.stderrBuffer], }; if (!this.isInitialized) { this.logger.error(`[ProxyManager] Proxy exited before initialization. code=${code} signal=${signal} stderrLines=${this.stderrBuffer.length}`, this.stderrBuffer); } this.handleProxyExit(code, signal); }); // Handle errors this.proxyProcess.on('error', (err) => { this.logger.error(`[ProxyManager] Proxy error:`, err); this.emit('error', err); this.cleanup(); }); } handleProxyMessage(rawMessage) { if (rawMessage?.type === 'ipc-heartbeat') { const heartbeat = rawMessage; this.logger.debug(`[ProxyManager] Received worker heartbeat counter=${heartbeat.counter ?? 'n/a'} timestamp=${heartbeat.timestamp ?? 'n/a'}`); return; } if (rawMessage?.type === 'ipc-heartbeat-tick') { const heartbeatTick = rawMessage; this.logger.debug(`[ProxyManager] Received worker heartbeat tick timestamp=${heartbeatTick.timestamp ?? 'n/a'}`); return; } this.proxyMessageCounter += 1; this.logger.debug(`[ProxyManager] Received message #${this.proxyMessageCounter}:`, rawMessage); // Validate message format if (!isValidProxyMessage(rawMessage)) { this.logger.warn(`[ProxyManager] Invalid message format:`, rawMessage); return; } const message = rawMessage; // Fast-path: always forward DAP events to consumers to avoid missing stops/output if (message.type === 'dapEvent') { this.handleDapEvent(message); } // Handle status messages if (message.type === 'status') { this.handleStatusMessage(message); } // Use functional core if state is initialized if (this.dapState) { const result = handleProxyMessage(this.dapState, message); // Execute commands from functional core for (const command of result.commands) { switch (command.type) { case 'log': this.logger[command.level](command.message, command.data); break; case 'emitEvent': { const args = command.args ?? []; this.emit(command.event, ...args); } break; case 'killProcess': this.proxyProcess?.kill(); break; case 'sendToProxy': this.sendCommand(command.command); break; // Note: sendToClient is not used in ProxyManager context } } // Update state if changed if (result.newState) { this.dapState = result.newState; // Sync local state with functional core state this.isInitialized = result.newState.initialized; this.adapterConfigured = result.newState.adapterConfigured; // Only update currentThreadId if the core provided a concrete number. // Avoid overwriting the value we set in the fast-path dapEvent handler with null/undefined. const coreTid = result.newState.currentThreadId; if (typeof coreTid === 'number') { this.currentThreadId = coreTid; } } // Handle pending DAP responses (still done imperatively for now) if (message.type === 'dapResponse') { this.handleDapResponse(message); } } else { // Fallback if state not initialized (shouldn't happen) this.logger.error(`[ProxyManager] DAP state not initialized`); } } handleDapResponse(message) { const pending = this.pendingDapRequests.get(message.requestId); if (!pending) { // During shutdown, it's normal to receive responses for requests that were cancelled if (this.proxyProcess) { this.logger.debug(`[ProxyManager] Received response for unknown/cancelled request: ${message.requestId}`); } return; } this.pendingDapRequests.delete(message.requestId); // Mirror completion into functional core if (this.dapState) { this.dapState = removePendingRequest(this.dapState, message.requestId); } if (this.activeLaunchBarrier && this.activeLaunchBarrierRequestId === message.requestId) { this.clearActiveLaunchBarrier(); } if (message.success) { // If this was a 'threads' response, opportunistically capture a usable thread id try { if (pending.command === 'threads') { const resp = (message.response || message.body); // eslint-disable-next-line @typescript-eslint/no-explicit-any const threads = (resp && resp.body && Array.isArray(resp.body.threads)) ? resp.body.threads : []; const first = threads.length ? threads[0]?.id : undefined; if (typeof first === 'number') { this.currentThreadId = first; } } } catch { // ignore capture errors } pending.resolve((message.response || message.body)); } else { pending.reject(new Error(message.error || `DAP request '${pending.command}' failed`)); } } handleDapEvent(message) { this.activeLaunchBarrier?.onDapEvent(message.event, message.body); this.logger.info(`[ProxyManager] DAP event: ${message.event}`, message.body); switch (message.event) { case 'stopped': const stoppedBody = message.body; const threadIdMaybe = (typeof stoppedBody?.threadId === 'number') ? stoppedBody.threadId : undefined; const reason = stoppedBody?.reason || 'unknown'; if (typeof threadIdMaybe === 'number') { this.currentThreadId = threadIdMaybe; } // Do not fabricate a threadId; emit undefined if adapter omitted it this.emit('stopped', threadIdMaybe, reason, stoppedBody); break; case 'continued': this.emit('continued'); break; case 'terminated': this.emit('terminated'); break; case 'exited': this.emit('exited'); break; // Forward other events as generic DAP events default: this.emit('dap-event', message.event, message.body); } } handleStatusMessage(message) { this.activeLaunchBarrier?.onProxyStatus(message.status, message); switch (message.status) { case 'proxy_minimal_ran_ipc_test': this.logger.info(`[ProxyManager] IPC test message received`); this.proxyProcess?.kill(); break; case 'init_received': this.logger.info(`[ProxyManager] Init command acknowledged by proxy`); this.emit('init-received'); break; case 'dry_run_complete': this.logger.info(`[ProxyManager] Dry run complete`); this.dryRunCompleteReceived = true; if (typeof message.command === 'string' && message.command.trim().length > 0) { this.dryRunCommandSnapshot = message.command; } if (typeof message.script === 'string' && message.script.trim().length > 0) { this.dryRunScriptPath = message.script; } this.emit('dry-run-complete', message.command, message.script); break; case 'adapter_configured_and_launched': this.logger.info(`[ProxyManager] Adapter configured and launched`); this.adapterConfigured = true; this.emit('adapter-configured'); if (!this.isInitialized) { this.isInitialized = true; this.emit('initialized'); } break; case 'adapter_connected': // Adapter transport is up; allow client to proceed with DAP handshake. this.logger.info(`[ProxyManager] Adapter transport connected. Marking initialized to unblock client handshake.`); if (!this.isInitialized) { this.isInitialized = true; this.emit('initialized'); } break; case 'adapter_exited': case 'dap_connection_closed': case 'terminated': this.logger.info(`[ProxyManager] Status: ${message.status}`); this.emit('exit', message.code || 1, message.signal || undefined); break; } } handleProxyExit(code, signal) { this.activeLaunchBarrier?.onProxyExit(code, signal); this.clearActiveLaunchBarrier(); if (this.isDryRun && code === 0 && !this.dryRunCompleteReceived) { const fallbackCommand = this.dryRunCommandSnapshot ?? '(command unavailable)'; const fallbackScript = this.dryRunScriptPath ?? ''; this.logger.warn(`[ProxyManager] Dry run proxy exited without reporting completion; synthesizing dry-run-complete event.`); this.dryRunCompleteReceived = true; this.dryRunCommandSnapshot = fallbackCommand; this.dryRunScriptPath = fallbackScript; this.emit('dry-run-complete', fallbackCommand, fallbackScript); } // Clean up pending requests this.pendingDapRequests.forEach(pending => { pending.reject(new Error('Proxy exited')); }); this.pendingDapRequests.clear(); // Emit exit event this.emit('exit', code, signal || undefined); // Clean up this.cleanup(); } cleanup() { // Clear pending DAP requests to avoid "unknown request" warnings during shutdown if (this.pendingDapRequests.size > 0) { this.logger.debug(`[ProxyManager] Clearing ${this.pendingDapRequests.size} pending DAP requests during cleanup`); for (const pending of this.pendingDapRequests.values()) { pending.reject(new Error(`Request cancelled during proxy shutdown: ${pending.command}`)); } this.pendingDapRequests.clear(); } // Clear functional core mirror if (this.dapState) { this.dapState = clearPendingRequests(this.dapState); } // Clear adapter-provided launch barriers this.clearActiveLaunchBarrier(); this.proxyProcess = null; this.isInitialized = false; this.adapterConfigured = false; this.currentThreadId = null; } setActiveLaunchBarrier(barrier, requestId) { if (this.activeLaunchBarrier && this.activeLaunchBarrier !== barrier) { this.activeLaunchBarrier.dispose(); } this.activeLaunchBarrier = barrier; this.activeLaunchBarrierRequestId = requestId; } clearActiveLaunchBarrier(barrier) { if (!this.activeLaunchBarrier) { return; } if (barrier && this.activeLaunchBarrier !== barrier) { return; } try { this.activeLaunchBarrier.dispose(); } catch (error) { this.logger.warn('[ProxyManager] Error disposing adapter launch barrier', error); } this.activeLaunchBarrier = null; this.activeLaunchBarrierRequestId = null; } hasDryRunCompleted() { return this.dryRunCompleteReceived; } getDryRunSnapshot() { if (!this.dryRunCommandSnapshot && !this.dryRunScriptPath) { return undefined; } return { command: this.dryRunCommandSnapshot, script: this.dryRunScriptPath }; } } //# sourceMappingURL=proxy-manager.js.map