See More

import * as vscode from 'vscode-debugadapter' import { DebugProtocol as VSCodeDebugProtocol } from 'vscode-debugprotocol' import * as net from 'net' import * as xdebug from './xdebugConnection' import moment = require('moment') import * as url from 'url' import * as childProcess from 'child_process' import * as path from 'path' import * as util from 'util' import * as fs from 'fs' import { Terminal } from './terminal' import { isSameUri, convertClientPathToDebugger, convertDebuggerPathToClient } from './paths' import minimatch = require('minimatch') if (process.env['VSCODE_NLS_CONFIG']) { try { moment.locale(JSON.parse(process.env['VSCODE_NLS_CONFIG']).locale) } catch (e) { // ignore } } /** formats a xdebug property value for VS Code */ function formatPropertyValue(property: xdebug.BaseProperty): string { let displayValue: string if (property.hasChildren || property.type === 'array' || property.type === 'object') { if (property.type === 'array') { // for arrays, show the length, like a var_dump would do displayValue = 'array(' + (property.hasChildren ? property.numberOfChildren : 0) + ')' } else if (property.type === 'object' && property.class) { // for objects, show the class name as type (if specified) displayValue = property.class } else { // edge case: show the type of the property as the value displayValue = property.type } } else { // for null, uninitialized, resource, etc. show the type displayValue = property.value || property.type === 'string' ? property.value : property.type if (property.type === 'string') { displayValue = '"' + displayValue + '"' } else if (property.type === 'bool') { displayValue = !!parseInt(displayValue) + '' } } return displayValue } /** * This interface should always match the schema found in the mock-debug extension manifest. */ interface LaunchRequestArguments extends VSCodeDebugProtocol.LaunchRequestArguments { /** The address to bind to for listening for XDebug connections (default: all IPv6 connections if available, else all IPv4 connections) */ hostname?: string /** The port where the adapter should listen for XDebug connections (default: 9000) */ port?: number /** Automatically stop target after launch. If not specified, target does not stop. */ stopOnEntry?: boolean /** The source root on the server when doing remote debugging on a different host */ serverSourceRoot?: string /** The path to the source root on this machine that is the equivalent to the serverSourceRoot on the server. */ localSourceRoot?: string /** The path to the source root on this machine that is the equivalent to the serverSourceRoot on the server. */ pathMappings?: { [index: string]: string } /** If true, will log all communication between VS Code and the adapter to the console */ log?: boolean /** Array of glob patterns that errors should be ignored from */ ignore?: string[] /** XDebug configuration */ xdebugSettings?: { [featureName: string]: string | number } // CLI options /** If set, launches the specified PHP script in CLI mode */ program?: string /** Optional arguments passed to the debuggee. */ args?: string[] /** Launch the debuggee in this working directory (specified as an absolute path). If omitted the debuggee is lauched in its own directory. */ cwd?: string /** Absolute path to the runtime executable to be used. Default is the runtime executable on the PATH. */ runtimeExecutable?: string /** Optional arguments passed to the runtime executable. */ runtimeArgs?: string[] /** Optional environment variables to pass to the debuggee. The string valued properties of the 'environmentVariables' are used as key/value pairs. */ env?: { [key: string]: string } /** If true launch the target in an external console. */ externalConsole?: boolean } class PhpDebugSession extends vscode.DebugSession { /** The arguments that were given to launchRequest */ private _args: LaunchRequestArguments /** The TCP server that listens for XDebug connections */ private _server: net.Server /** The child process of the launched PHP script, if launched by the debug adapter */ private _phpProcess?: childProcess.ChildProcess /** * A map from VS Code thread IDs to XDebug Connections. * XDebug makes a new connection for each request to the webserver, we present these as threads to VS Code. * The threadId key is equal to the id attribute of the connection. */ private _connections = new Map() /** A set of connections which are not yet running and are waiting for configurationDoneRequest */ private _waitingConnections = new Set() /** A counter for unique source IDs */ private _sourceIdCounter = 1 /** A map of VS Code source IDs to XDebug file URLs for virtual files (dpgp://whatever) and the corresponding connection */ private _sources = new Map() /** A counter for unique stackframe IDs */ private _stackFrameIdCounter = 1 /** A map from unique stackframe IDs (even across connections) to XDebug stackframes */ private _stackFrames = new Map() /** A map from XDebug connections to their current status */ private _statuses = new Map() /** A counter for unique context, property and eval result properties (as these are all requested by a VariableRequest from VS Code) */ private _variableIdCounter = 1 /** A map from unique VS Code variable IDs to XDebug statuses for virtual error stack frames */ private _errorStackFrames = new Map() /** A map from unique VS Code variable IDs to XDebug statuses for virtual error scopes */ private _errorScopes = new Map() /** A map from unique VS Code variable IDs to an XDebug contexts */ private _contexts = new Map() /** A map from unique VS Code variable IDs to a XDebug properties */ private _properties = new Map() /** A map from unique VS Code variable IDs to XDebug eval result properties, because property children returned from eval commands are always inlined */ private _evalResultProperties = new Map() public constructor() { super() this.setDebuggerColumnsStartAt1(true) this.setDebuggerLinesStartAt1(true) this.setDebuggerPathFormat('uri') } protected initializeRequest( response: VSCodeDebugProtocol.InitializeResponse, args: VSCodeDebugProtocol.InitializeRequestArguments ): void { response.body = { supportsConfigurationDoneRequest: true, supportsEvaluateForHovers: false, supportsConditionalBreakpoints: true, supportsFunctionBreakpoints: true, exceptionBreakpointFilters: [ { filter: 'Notice', label: 'Notices', }, { filter: 'Warning', label: 'Warnings', }, { filter: 'Error', label: 'Errors', }, { filter: 'Exception', label: 'Exceptions', }, { filter: '*', label: 'Everything', default: true, }, ], } this.sendResponse(response) } protected attachRequest( response: VSCodeDebugProtocol.AttachResponse, args: VSCodeDebugProtocol.AttachRequestArguments ) { this.sendErrorResponse(response, new Error('Attach requests are not supported')) this.shutdown() } protected async launchRequest(response: VSCodeDebugProtocol.LaunchResponse, args: LaunchRequestArguments) { if (args.localSourceRoot && args.serverSourceRoot) { let pathMappings: { [index: string]: string } = {} if (args.pathMappings) { pathMappings = args.pathMappings } pathMappings[args.serverSourceRoot] = args.localSourceRoot args.pathMappings = pathMappings } this._args = args /** launches the script as CLI */ const launchScript = async () => { // check if program exists await new Promise((resolve, reject) => fs.access(args.program!, fs.constants.F_OK, err => (err ? reject(err) : resolve())) ) const runtimeArgs = args.runtimeArgs || [] const runtimeExecutable = args.runtimeExecutable || 'php' const programArgs = args.args || [] const cwd = args.cwd || process.cwd() const env = args.env || process.env // launch in CLI mode if (args.externalConsole) { const script = await Terminal.launchInTerminal( cwd, [runtimeExecutable, ...runtimeArgs, args.program!, ...programArgs], env ) // we only do this for CLI mode. In normal listen mode, only a thread exited event is send. script.on('exit', () => { this.sendEvent(new vscode.TerminatedEvent()) }) } else { const script = childProcess.spawn(runtimeExecutable, [...runtimeArgs, args.program!, ...programArgs], { cwd, env, }) // redirect output to debug console script.stdout.on('data', (data: Buffer) => { this.sendEvent(new vscode.OutputEvent(data + '', 'stdout')) }) script.stderr.on('data', (data: Buffer) => { this.sendEvent(new vscode.OutputEvent(data + '', 'stderr')) }) // we only do this for CLI mode. In normal listen mode, only a thread exited event is send. script.on('exit', () => { this.sendEvent(new vscode.TerminatedEvent()) }) script.on('error', (error: Error) => { this.sendEvent(new vscode.OutputEvent(util.inspect(error) + '\n')) }) this._phpProcess = script } } /** sets up a TCP server to listen for XDebug connections */ const createServer = () => new Promise((resolve, reject) => { const server = (this._server = net.createServer()) server.on('connection', async (socket: net.Socket) => { try { // new XDebug connection const connection = new xdebug.Connection(socket) if (args.log) { this.sendEvent(new vscode.OutputEvent('new connection ' + connection.id + '\n'), true) } this._connections.set(connection.id, connection) this._waitingConnections.add(connection) const disposeConnection = (error?: Error) => { if (this._connections.has(connection.id)) { if (args.log) { this.sendEvent(new vscode.OutputEvent('connection ' + connection.id + ' closed\n')) } if (error) { this.sendEvent( new vscode.OutputEvent( 'connection ' + connection.id + ': ' + error.message + '\n' ) ) } this.sendEvent(new vscode.ThreadEvent('exited', connection.id)) connection.close() this._connections.delete(connection.id) this._waitingConnections.delete(connection) } } connection.on('warning', (warning: string) => { this.sendEvent(new vscode.OutputEvent(warning + '\n')) }) connection.on('error', disposeConnection) connection.on('close', disposeConnection) await connection.waitForInitPacket() // override features from launch.json try { const xdebugSettings = args.xdebugSettings || {} await Promise.all( Object.keys(xdebugSettings).map(setting => connection.sendFeatureSetCommand(setting, xdebugSettings[setting]) ) ) } catch (error) { throw new Error( 'Error applying xdebugSettings: ' + (error instanceof Error ? error.message : error) ) } this.sendEvent(new vscode.ThreadEvent('started', connection.id)) // request breakpoints from VS Code await this.sendEvent(new vscode.InitializedEvent()) } catch (error) { this.sendEvent( new vscode.OutputEvent((error instanceof Error ? error.message : error) + '\n', 'stderr') ) this.shutdown() } }) server.on('error', (error: Error) => { this.sendEvent(new vscode.OutputEvent(util.inspect(error) + '\n')) this.sendErrorResponse(response, error) }) server.listen( args.port || 9000, args.hostname, (error: NodeJS.ErrnoException) => (error ? reject(error) : resolve()) ) }) try { if (!args.noDebug) { await createServer() } if (args.program) { await launchScript() } } catch (error) { this.sendErrorResponse(response, error) return } this.sendResponse(response) } /** * Checks the status of a StatusResponse and notifies VS Code accordingly * @param {xdebug.StatusResponse} response */ private async _checkStatus(response: xdebug.StatusResponse): Promise { const connection = response.connection this._statuses.set(connection, response) if (response.status === 'stopping') { const response = await connection.sendStopCommand() this._checkStatus(response) } else if (response.status === 'stopped') { this._connections.delete(connection.id) this.sendEvent(new vscode.ThreadEvent('exited', connection.id)) connection.close() } else if (response.status === 'break') { // StoppedEvent reason can be 'step', 'breakpoint', 'exception' or 'pause' let stoppedEventReason: 'step' | 'breakpoint' | 'exception' | 'pause' | 'entry' let exceptionText: string | undefined if (response.exception) { // If one of the ignore patterns matches, ignore this exception if ( this._args.ignore && this._args.ignore.some(glob => minimatch(convertDebuggerPathToClient(response.fileUri).replace(/\\/g, '/'), glob) ) ) { const response = await connection.sendRunCommand() await this._checkStatus(response) return } stoppedEventReason = 'exception' exceptionText = response.exception.name + ': ' + response.exception.message // this seems to be ignored currently by VS Code } else if (this._args.stopOnEntry) { stoppedEventReason = 'entry' } else if (response.command.indexOf('step') === 0) { stoppedEventReason = 'step' } else { stoppedEventReason = 'breakpoint' } const event: VSCodeDebugProtocol.StoppedEvent = new vscode.StoppedEvent( stoppedEventReason, connection.id, exceptionText ) event.body.allThreadsStopped = false this.sendEvent(event) } } /** Logs all requests before dispatching */ protected dispatchRequest(request: VSCodeDebugProtocol.Request): void { if (this._args && this._args.log) { const log = `-> ${request.command}Request\n${util.inspect(request, { depth: Infinity })}\n\n` super.sendEvent(new vscode.OutputEvent(log)) } super.dispatchRequest(request) } public sendEvent(event: VSCodeDebugProtocol.Event, bypassLog: boolean = false): void { if (this._args && this._args.log && !bypassLog) { const log = `<- ${event.event}Event\n${util.inspect(event, { depth: Infinity })}\n\n` super.sendEvent(new vscode.OutputEvent(log)) } super.sendEvent(event) } public sendResponse(response: VSCodeDebugProtocol.Response): void { if (this._args && this._args.log) { const log = `<- ${response.command}Response\n${util.inspect(response, { depth: Infinity })}\n\n` super.sendEvent(new vscode.OutputEvent(log)) } super.sendResponse(response) } protected sendErrorResponse( response: VSCodeDebugProtocol.Response, error: Error, dest?: vscode.ErrorDestination ): void protected sendErrorResponse( response: VSCodeDebugProtocol.Response, codeOrMessage: number | VSCodeDebugProtocol.Message, format?: string, variables?: any, dest?: vscode.ErrorDestination ): void protected sendErrorResponse(response: VSCodeDebugProtocol.Response) { if (arguments[1] instanceof Error) { const error = arguments[1] as Error & { code?: number | string; errno?: number } const dest = arguments[2] as vscode.ErrorDestination let code: number if (typeof error.code === 'number') { code = error.code as number } else if (typeof error.errno === 'number') { code = error.errno } else { code = 0 } super.sendErrorResponse(response, code, error.message, dest) } else { super.sendErrorResponse(response, arguments[1], arguments[2], arguments[3], arguments[4]) } } /** This is called for each source file that has breakpoints with all the breakpoints in that file and whenever these change. */ protected async setBreakPointsRequest( response: VSCodeDebugProtocol.SetBreakpointsResponse, args: VSCodeDebugProtocol.SetBreakpointsArguments ) { try { const fileUri = convertClientPathToDebugger(args.source.path!, this._args.pathMappings) const connections = Array.from(this._connections.values()) let xdebugBreakpoints: Array response.body = { breakpoints: [] } // this is returned to VS Code let vscodeBreakpoints: VSCodeDebugProtocol.Breakpoint[] if (connections.length === 0) { // if there are no connections yet, we cannot verify any breakpoint vscodeBreakpoints = args.breakpoints!.map(breakpoint => ({ verified: false, line: breakpoint.line })) } else { vscodeBreakpoints = [] // create XDebug breakpoints from the arguments xdebugBreakpoints = args.breakpoints!.map(breakpoint => { if (breakpoint.condition) { return new xdebug.ConditionalBreakpoint(breakpoint.condition, fileUri, breakpoint.line) } else { return new xdebug.LineBreakpoint(fileUri, breakpoint.line) } }) // for all connections await Promise.all( connections.map(async (connection, connectionIndex) => { const promise = (async () => { const { breakpoints } = await connection.sendBreakpointListCommand() // clear breakpoints for this file // in the future when VS Code returns the breakpoint IDs it would be better to calculate the diff await Promise.all( breakpoints .filter( breakpoint => breakpoint instanceof xdebug.LineBreakpoint && isSameUri(fileUri, breakpoint.fileUri) ) .map(breakpoint => breakpoint.remove()) ) // set new breakpoints await Promise.all( xdebugBreakpoints.map(async (breakpoint, index) => { try { await connection.sendBreakpointSetCommand(breakpoint) vscodeBreakpoints[index] = { verified: true, line: breakpoint.line } } catch (error) { vscodeBreakpoints[index] = { verified: false, line: breakpoint.line, message: (error).message, } } }) ) })() if (connection.isPendingExecuteCommand) { // There is a pending execute command which could lock the connection up, so do not // wait on the response before continuing or it can get into a deadlock promise.catch(err => this.sendEvent(new vscode.OutputEvent(util.inspect(err) + '\n'))) } else { await promise } }) ) } response.body = { breakpoints: vscodeBreakpoints } } catch (error) { this.sendErrorResponse(response, error) return } this.sendResponse(response) } /** This is called once after all line breakpoints have been set and whenever the breakpoints settings change */ protected async setExceptionBreakPointsRequest( response: VSCodeDebugProtocol.SetExceptionBreakpointsResponse, args: VSCodeDebugProtocol.SetExceptionBreakpointsArguments ) { try { const connections = Array.from(this._connections.values()) await Promise.all( connections.map(async connection => { const promise = (async () => { const { breakpoints } = await connection.sendBreakpointListCommand() await Promise.all( breakpoints .filter(breakpoint => breakpoint.type === 'exception') .map(breakpoint => breakpoint.remove()) ) await Promise.all( args.filters.map(filter => connection.sendBreakpointSetCommand(new xdebug.ExceptionBreakpoint(filter)) ) ) })() if (connection.isPendingExecuteCommand) { // There is a pending execute command which could lock the connection up, so do not // wait on the response before continuing or it can get into a deadlock promise.catch(err => this.sendEvent(new vscode.OutputEvent(util.inspect(err) + '\n'))) } else { await promise } }) ) } catch (error) { this.sendErrorResponse(response, error) return } this.sendResponse(response) } protected async setFunctionBreakPointsRequest( response: VSCodeDebugProtocol.SetFunctionBreakpointsResponse, args: VSCodeDebugProtocol.SetFunctionBreakpointsArguments ) { try { const connections = Array.from(this._connections.values()) // this is returned to VS Code let vscodeBreakpoints: VSCodeDebugProtocol.Breakpoint[] if (connections.length === 0) { // if there are no connections yet, we cannot verify any breakpoint vscodeBreakpoints = args.breakpoints.map(breakpoint => ({ verified: false, message: 'No connection' })) } else { vscodeBreakpoints = [] // for all connections await Promise.all( connections.map(async (connection, connectionIndex) => { const promise = (async () => { const { breakpoints } = await connection.sendBreakpointListCommand() await Promise.all( breakpoints .filter(breakpoint => breakpoint.type === 'call') .map(breakpoint => breakpoint.remove()) ) await Promise.all( args.breakpoints.map(async (functionBreakpoint, index) => { try { await connection.sendBreakpointSetCommand( new xdebug.CallBreakpoint( functionBreakpoint.name, functionBreakpoint.condition ) ) vscodeBreakpoints[index] = { verified: true } } catch (error) { vscodeBreakpoints[index] = { verified: false, message: error instanceof Error ? error.message : error, } } }) ) })() if (connection.isPendingExecuteCommand) { // There is a pending execute command which could lock the connection up, so do not // wait on the response before continuing or it can get into a deadlock promise.catch(err => this.sendEvent(new vscode.OutputEvent(util.inspect(err) + '\n'))) } else { await promise } }) ) } response.body = { breakpoints: vscodeBreakpoints } } catch (error) { this.sendErrorResponse(response, error) return } this.sendResponse(response) } /** Executed after all breakpoints have been set by VS Code */ protected async configurationDoneRequest( response: VSCodeDebugProtocol.ConfigurationDoneResponse, args: VSCodeDebugProtocol.ConfigurationDoneArguments ) { let xdebugResponses: xdebug.StatusResponse[] = [] try { xdebugResponses = await Promise.all( Array.from(this._waitingConnections).map(connection => { this._waitingConnections.delete(connection) // either tell VS Code we stopped on entry or run the script if (this._args.stopOnEntry) { // do one step to the first statement return connection.sendStepIntoCommand() } else { return connection.sendRunCommand() } }) ) } catch (error) { this.sendErrorResponse(response, error) for (const response of xdebugResponses) { this._checkStatus(response) } return } this.sendResponse(response) for (const response of xdebugResponses) { this._checkStatus(response) } } /** Executed after a successfull launch or attach request and after a ThreadEvent */ protected threadsRequest(response: VSCodeDebugProtocol.ThreadsResponse): void { // PHP doesn't have threads, but it may have multiple requests in parallel. // Think about a website that makes multiple, parallel AJAX requests to your PHP backend. // XDebug opens a new socket connection for each of them, we tell VS Code that these are our threads. const connections = Array.from(this._connections.values()) response.body = { threads: connections.map( connection => new vscode.Thread( connection.id, `Request ${connection.id} (${moment(connection.timeEstablished).format('LTS')})` ) ), } this.sendResponse(response) } /** Called by VS Code after a StoppedEvent */ protected async stackTraceRequest( response: VSCodeDebugProtocol.StackTraceResponse, args: VSCodeDebugProtocol.StackTraceArguments ) { try { const connection = this._connections.get(args.threadId) if (!connection) { throw new Error('Unknown thread ID') } const { stack } = await connection.sendStackGetCommand() // First delete the old stack trace info ??? // this._stackFrames.clear(); // this._properties.clear(); // this._contexts.clear(); const status = this._statuses.get(connection) if (stack.length === 0 && status && status.exception) { // special case: if a fatal error occurs (for example after an uncaught exception), the stack trace is EMPTY. // in that case, VS Code would normally not show any information to the user at all // to avoid this, we create a virtual stack frame with the info from the last status response we got const status = this._statuses.get(connection)! const id = this._stackFrameIdCounter++ const name = status.exception.name let line = status.line let source: VSCodeDebugProtocol.Source const urlObject = url.parse(status.fileUri) if (urlObject.protocol === 'dbgp:') { const sourceReference = this._sourceIdCounter++ this._sources.set(sourceReference, { connection, url: status.fileUri }) // for eval code, we need to include .php extension to get syntax highlighting source = { name: status.exception.name + '.php', sourceReference, origin: status.exception.name } // for eval code, we add a " { let source: VSCodeDebugProtocol.Source let line = stackFrame.line const urlObject = url.parse(stackFrame.fileUri) if (urlObject.protocol === 'dbgp:') { const sourceReference = this._sourceIdCounter++ this._sources.set(sourceReference, { connection, url: stackFrame.fileUri }) // for eval code, we need to include .php extension to get syntax highlighting source = { name: stackFrame.type === 'eval' ? 'eval.php' : stackFrame.name, sourceReference, origin: stackFrame.type, } // for eval code, we add a " { const variableId = this._variableIdCounter++ // remember that this new variable ID is assigned to a SCOPE (in XDebug "context"), not a variable (in XDebug "property"), // so when VS Code does a variablesRequest with that ID we do a context_get and not a property_get this._contexts.set(variableId, context) // send VS Code the variable ID as identifier return new vscode.Scope(context.name, variableId) }) const status = this._statuses.get(stackFrame.connection) if (status && status.exception) { const variableId = this._variableIdCounter++ this._errorScopes.set(variableId, status) scopes.unshift(new vscode.Scope(status.exception.name.replace(/^(.*\\)+/g, ''), variableId)) } } response.body = { scopes } } catch (error) { this.sendErrorResponse(response, error) return } this.sendResponse(response) } protected async variablesRequest( response: VSCodeDebugProtocol.VariablesResponse, args: VSCodeDebugProtocol.VariablesArguments ) { try { const variablesReference = args.variablesReference let variables: VSCodeDebugProtocol.Variable[] if (this._errorScopes.has(variablesReference)) { // this is a virtual error scope const status = this._errorScopes.get(variablesReference)! variables = [ new vscode.Variable('type', status.exception.name), new vscode.Variable('message', '"' + status.exception.message + '"'), ] if (status.exception.code !== undefined) { variables.push(new vscode.Variable('code', status.exception.code + '')) } } else { // it is a real scope let properties: xdebug.BaseProperty[] if (this._contexts.has(variablesReference)) { // VS Code is requesting the variables for a SCOPE, so we have to do a context_get const context = this._contexts.get(variablesReference)! properties = await context.getProperties() } else if (this._properties.has(variablesReference)) { // VS Code is requesting the subelements for a variable, so we have to do a property_get const property = this._properties.get(variablesReference)! if (property.hasChildren) { if (property.children.length === property.numberOfChildren) { properties = property.children } else { properties = await property.getChildren() } } else { properties = [] } } else if (this._evalResultProperties.has(variablesReference)) { // the children of properties returned from an eval command are always inlined, so we simply resolve them const property = this._evalResultProperties.get(variablesReference)! properties = property.hasChildren ? property.children : [] } else { throw new Error('Unknown variable reference') } variables = properties.map(property => { const displayValue = formatPropertyValue(property) let variablesReference: number let evaluateName: string if (property.hasChildren || property.type === 'array' || property.type === 'object') { // if the property has children, we have to send a variableReference back to VS Code // so it can receive the child elements in another request. // for arrays and objects we do it even when it does not have children so the user can still expand/collapse the entry variablesReference = this._variableIdCounter++ if (property instanceof xdebug.Property) { this._properties.set(variablesReference, property) } else if (property instanceof xdebug.EvalResultProperty) { this._evalResultProperties.set(variablesReference, property) } } else { variablesReference = 0 } if (property instanceof xdebug.Property) { evaluateName = property.fullName } else { evaluateName = property.name } const variable: VSCodeDebugProtocol.Variable = { name: property.name, value: displayValue, type: property.type, variablesReference, evaluateName, } return variable }) } response.body = { variables } } catch (error) { this.sendErrorResponse(response, error) return } this.sendResponse(response) } protected async continueRequest( response: VSCodeDebugProtocol.ContinueResponse, args: VSCodeDebugProtocol.ContinueArguments ) { let xdebugResponse: xdebug.StatusResponse | undefined try { const connection = this._connections.get(args.threadId) if (!connection) { throw new Error('Unknown thread ID ' + args.threadId) } xdebugResponse = await connection.sendRunCommand() } catch (error) { this.sendErrorResponse(response, error) if (xdebugResponse) { this._checkStatus(xdebugResponse) } return } response.body = { allThreadsContinued: false, } this.sendResponse(response) this._checkStatus(xdebugResponse) } protected async nextRequest(response: VSCodeDebugProtocol.NextResponse, args: VSCodeDebugProtocol.NextArguments) { let xdebugResponse: xdebug.StatusResponse | undefined try { const connection = this._connections.get(args.threadId) if (!connection) { throw new Error('Unknown thread ID ' + args.threadId) } xdebugResponse = await connection.sendStepOverCommand() } catch (error) { this.sendErrorResponse(response, error) if (xdebugResponse) { this._checkStatus(xdebugResponse) } return } this.sendResponse(response) this._checkStatus(xdebugResponse) } protected async stepInRequest( response: VSCodeDebugProtocol.StepInResponse, args: VSCodeDebugProtocol.StepInArguments ) { let xdebugResponse: xdebug.StatusResponse | undefined try { const connection = this._connections.get(args.threadId) if (!connection) { throw new Error('Unknown thread ID ' + args.threadId) } xdebugResponse = await connection.sendStepIntoCommand() } catch (error) { this.sendErrorResponse(response, error) if (xdebugResponse) { this._checkStatus(xdebugResponse) } return } this.sendResponse(response) this._checkStatus(xdebugResponse) } protected async stepOutRequest( response: VSCodeDebugProtocol.StepOutResponse, args: VSCodeDebugProtocol.StepOutArguments ) { let xdebugResponse: xdebug.StatusResponse | undefined try { const connection = this._connections.get(args.threadId) if (!connection) { throw new Error('Unknown thread ID ' + args.threadId) } xdebugResponse = await connection.sendStepOutCommand() } catch (error) { this.sendErrorResponse(response, error) if (xdebugResponse) { this._checkStatus(xdebugResponse) } return } this.sendResponse(response) this._checkStatus(xdebugResponse) } protected pauseRequest(response: VSCodeDebugProtocol.PauseResponse, args: VSCodeDebugProtocol.PauseArguments) { this.sendErrorResponse(response, new Error('Pausing the execution is not supported by XDebug')) } protected async disconnectRequest( response: VSCodeDebugProtocol.DisconnectResponse, args: VSCodeDebugProtocol.DisconnectArguments ) { try { await Promise.all( Array.from(this._connections).map(async ([id, connection]) => { // Try to send stop command for 500ms // If the script is running, just close the connection await Promise.race([connection.sendStopCommand(), new Promise(resolve => setTimeout(resolve, 500))]) await connection.close() this._connections.delete(id) this._waitingConnections.delete(connection) }) ) // If listening for connections, close server if (this._server) { await new Promise(resolve => this._server.close(resolve)) } // If launched as CLI, kill process if (this._phpProcess) { this._phpProcess.kill() } } catch (error) { this.sendErrorResponse(response, error) return } this.sendResponse(response) this.shutdown() } protected async evaluateRequest( response: VSCodeDebugProtocol.EvaluateResponse, args: VSCodeDebugProtocol.EvaluateArguments ) { try { if (!args.frameId) { throw new Error('Cannot evaluate code without a connection') } if (!this._stackFrames.has(args.frameId)) { throw new Error(`Unknown frameId ${args.frameId}`) } const connection = this._stackFrames.get(args.frameId)!.connection const { result } = await connection.sendEvalCommand(args.expression) if (result) { const displayValue = formatPropertyValue(result) let variablesReference: number // if the property has children, generate a variable ID and save the property (including children) so VS Code can request them if (result.hasChildren || result.type === 'array' || result.type === 'object') { variablesReference = this._variableIdCounter++ this._evalResultProperties.set(variablesReference, result) } else { variablesReference = 0 } response.body = { result: displayValue, variablesReference } } else { response.body = { result: 'no result', variablesReference: 0 } } this.sendResponse(response) } catch (error) { this.sendErrorResponse(response, error) } } } vscode.DebugSession.run(PhpDebugSession)