| /*--------------------------------------------------------------------------------------------- |
| * Copyright (c) Microsoft Corporation. All rights reserved. |
| * Licensed under the MIT License. See License.txt in the project root for license information. |
| *--------------------------------------------------------------------------------------------*/ |
| |
| import { |
| Logger, |
| logger, |
| LoggingDebugSession, |
| InitializedEvent, |
| TerminatedEvent, |
| StoppedEvent, |
| BreakpointEvent, |
| OutputEvent, |
| ProgressStartEvent, |
| ProgressUpdateEvent, |
| ProgressEndEvent, |
| InvalidatedEvent, |
| Thread, |
| StackFrame, |
| Scope, |
| Source, |
| Handles, |
| Breakpoint, |
| } from '@vscode/debugadapter' |
| import { DebugProtocol } from '@vscode/debugprotocol' |
| import { basename } from 'path' |
| import { |
| DaffodilRuntime, |
| IDaffodilBreakpoint, |
| FileAccessor, |
| } from './daffodilRuntime' |
| import { Subject } from 'await-notify' |
| |
| function timeout(ms: number) { |
| return new Promise((resolve) => setTimeout(resolve, ms)) |
| } |
| |
| /** |
| * This interface describes the daffodil-debugger specific launch attributes |
| * (which are not part of the Debug Adapter Protocol). |
| * The schema for these attributes lives in the package.json of the daffodil-debugger extension. |
| * The interface should always match this schema. |
| */ |
| interface ILaunchRequestArguments extends DebugProtocol.LaunchRequestArguments { |
| /** An absolute path to the "schema" to debug. */ |
| schema: string |
| /** Automatically stop target after launch. If not specified, target does not stop. */ |
| stopOnEntry?: boolean |
| /** enable logging the Debug Adapter Protocol */ |
| trace?: boolean |
| /** run without debugging */ |
| noDebug?: boolean |
| } |
| |
| export class DaffodilDebugSession extends LoggingDebugSession { |
| // we don't support multiple threads, so we can use a hardcoded ID for the default thread |
| private static threadID = 1 |
| |
| // a Daffodil runtime (or debugger) |
| private _runtime: DaffodilRuntime |
| |
| private _variableHandles = new Handles<string>() |
| |
| private _configurationDone = new Subject() |
| |
| private _cancelationTokens = new Map<number, boolean>() |
| private _isLongrunning = new Map<number, boolean>() |
| |
| private _reportProgress = false |
| private _progressId = 10000 |
| private _cancelledProgressId: string | undefined = undefined |
| private _isProgressCancellable = true |
| |
| private _showHex = false |
| private _useInvalidatedEvent = false |
| |
| /** |
| * Creates a new debug adapter that is used for one debug session. |
| * We configure the default implementation of a debug adapter here. |
| */ |
| public constructor(fileAccessor: FileAccessor) { |
| super('daffodil-debugger.txt') |
| |
| // this debugger uses zero-based lines and columns |
| this.setDebuggerLinesStartAt1(false) |
| this.setDebuggerColumnsStartAt1(false) |
| |
| this._runtime = new DaffodilRuntime(fileAccessor) |
| |
| // setup event handlers |
| this._runtime.on('stopOnEntry', () => { |
| this.sendEvent(new StoppedEvent('entry', DaffodilDebugSession.threadID)) |
| }) |
| this._runtime.on('stopOnStep', () => { |
| this.sendEvent(new StoppedEvent('step', DaffodilDebugSession.threadID)) |
| }) |
| this._runtime.on('stopOnBreakpoint', () => { |
| this.sendEvent( |
| new StoppedEvent('breakpoint', DaffodilDebugSession.threadID) |
| ) |
| }) |
| this._runtime.on('stopOnDataBreakpoint', () => { |
| this.sendEvent( |
| new StoppedEvent('data breakpoint', DaffodilDebugSession.threadID) |
| ) |
| }) |
| this._runtime.on('stopOnException', (exception) => { |
| if (exception) { |
| this.sendEvent( |
| new StoppedEvent( |
| `exception(${exception})`, |
| DaffodilDebugSession.threadID |
| ) |
| ) |
| } else { |
| this.sendEvent( |
| new StoppedEvent('exception', DaffodilDebugSession.threadID) |
| ) |
| } |
| }) |
| this._runtime.on('breakpointValidated', (bp: IDaffodilBreakpoint) => { |
| this.sendEvent( |
| new BreakpointEvent('changed', { |
| verified: bp.verified, |
| id: bp.id, |
| } as DebugProtocol.Breakpoint) |
| ) |
| }) |
| this._runtime.on('output', (text, filePath, line, column) => { |
| const e: DebugProtocol.OutputEvent = new OutputEvent(`${text}\n`) |
| |
| if (text === 'start' || text === 'startCollapsed' || text === 'end') { |
| e.body.group = text |
| e.body.output = `group-${text}\n` |
| } |
| |
| e.body.source = this.createSource(filePath) |
| e.body.line = this.convertDebuggerLineToClient(line) |
| e.body.column = this.convertDebuggerColumnToClient(column) |
| this.sendEvent(e) |
| }) |
| this._runtime.on('end', () => { |
| this.sendEvent(new TerminatedEvent()) |
| }) |
| } |
| |
| /** |
| * The 'initialize' request is the first request called by the frontend |
| * to interrogate the features the debug adapter provides. |
| */ |
| protected initializeRequest( |
| response: DebugProtocol.InitializeResponse, |
| args: DebugProtocol.InitializeRequestArguments |
| ): void { |
| if (args.supportsProgressReporting) { |
| this._reportProgress = true |
| } |
| if (args.supportsInvalidatedEvent) { |
| this._useInvalidatedEvent = true |
| } |
| |
| // build and return the capabilities of this debug adapter: |
| response.body = response.body || {} |
| |
| // the adapter implements the configurationDoneRequest. |
| response.body.supportsConfigurationDoneRequest = true |
| |
| // make VS Code use 'evaluate' when hovering over source |
| response.body.supportsEvaluateForHovers = true |
| |
| // make VS Code show a 'step back' button |
| response.body.supportsStepBack = true |
| |
| // make VS Code support data breakpoints |
| response.body.supportsDataBreakpoints = true |
| |
| // make VS Code support completion in REPL |
| response.body.supportsCompletionsRequest = true |
| response.body.completionTriggerCharacters = ['.', '['] |
| |
| // make VS Code send cancelRequests |
| response.body.supportsCancelRequest = true |
| |
| // make VS Code send the breakpointLocations request |
| response.body.supportsBreakpointLocationsRequest = true |
| |
| // make VS Code provide "Step in Target" functionality |
| response.body.supportsStepInTargetsRequest = true |
| |
| // the adapter defines two exceptions filters, one with support for conditions. |
| response.body.supportsExceptionFilterOptions = true |
| response.body.exceptionBreakpointFilters = [ |
| { |
| filter: 'namedException', |
| label: 'Named Exception', |
| description: `Break on named exceptions. Enter the exception's name as the Condition.`, |
| default: false, |
| supportsCondition: true, |
| conditionDescription: `Enter the exception's name`, |
| }, |
| { |
| filter: 'otherExceptions', |
| label: 'Other Exceptions', |
| description: 'This is a other exception', |
| default: true, |
| supportsCondition: false, |
| }, |
| ] |
| |
| // make VS Code send exceptionInfoRequests |
| response.body.supportsExceptionInfoRequest = true |
| |
| this.sendResponse(response) |
| |
| // since this debug adapter can accept configuration requests like 'setBreakpoint' at any time, |
| // we request them early by sending an 'initializeRequest' to the frontend. |
| // The frontend will end the configuration sequence by calling 'configurationDone' request. |
| this.sendEvent(new InitializedEvent()) |
| } |
| |
| /** |
| * Called at the end of the configuration sequence. |
| * Indicates that all breakpoints etc. have been sent to the DA and that the 'launch' can start. |
| */ |
| protected configurationDoneRequest( |
| response: DebugProtocol.ConfigurationDoneResponse, |
| args: DebugProtocol.ConfigurationDoneArguments |
| ): void { |
| super.configurationDoneRequest(response, args) |
| |
| // notify the launchRequest that configuration has finished |
| this._configurationDone.notify() |
| } |
| |
| protected async launchRequest( |
| response: DebugProtocol.LaunchResponse, |
| args: ILaunchRequestArguments |
| ) { |
| // make sure to 'Stop' the buffered logging if 'trace' is not set |
| logger.setup( |
| args.trace ? Logger.LogLevel.Verbose : Logger.LogLevel.Stop, |
| false |
| ) |
| |
| // wait until configuration has finished (and configurationDoneRequest has been called) |
| await this._configurationDone.wait(1000) |
| |
| // start the schema in the runtime |
| await this._runtime.start(args.schema, !!args.stopOnEntry, !!args.noDebug) |
| |
| this.sendResponse(response) |
| } |
| |
| protected async setBreakPointsRequest( |
| response: DebugProtocol.SetBreakpointsResponse, |
| args: DebugProtocol.SetBreakpointsArguments |
| ): Promise<void> { |
| const path = args.source.path as string |
| const clientLines = args.lines || [] |
| |
| // clear all breakpoints for this file |
| this._runtime.clearBreakpoints(path) |
| |
| // set and verify breakpoint locations |
| const actualBreakpoints0 = clientLines.map(async (l) => { |
| const { verified, line, id } = await this._runtime.setBreakPoint( |
| path, |
| this.convertClientLineToDebugger(l) |
| ) |
| const bp = new Breakpoint( |
| verified, |
| this.convertDebuggerLineToClient(line) |
| ) as DebugProtocol.Breakpoint |
| bp.id = id |
| return bp |
| }) |
| const actualBreakpoints = |
| await Promise.all<DebugProtocol.Breakpoint>(actualBreakpoints0) |
| |
| // send back the actual breakpoint positions |
| response.body = { |
| breakpoints: actualBreakpoints, |
| } |
| this.sendResponse(response) |
| } |
| |
| protected breakpointLocationsRequest( |
| response: DebugProtocol.BreakpointLocationsResponse, |
| args: DebugProtocol.BreakpointLocationsArguments, |
| request?: DebugProtocol.Request |
| ): void { |
| if (args.source.path) { |
| const bps = this._runtime.getBreakpoints( |
| args.source.path, |
| this.convertClientLineToDebugger(args.line) |
| ) |
| response.body = { |
| breakpoints: bps.map((col) => { |
| return { |
| line: args.line, |
| column: this.convertDebuggerColumnToClient(col), |
| } |
| }), |
| } |
| } else { |
| response.body = { |
| breakpoints: [], |
| } |
| } |
| this.sendResponse(response) |
| } |
| |
| protected async setExceptionBreakPointsRequest( |
| response: DebugProtocol.SetExceptionBreakpointsResponse, |
| args: DebugProtocol.SetExceptionBreakpointsArguments |
| ): Promise<void> { |
| let namedException: string | undefined = undefined |
| let otherExceptions = false |
| |
| if (args.filterOptions) { |
| for (const filterOption of args.filterOptions) { |
| switch (filterOption.filterId) { |
| case 'namedException': |
| namedException = args.filterOptions[0].condition |
| break |
| case 'otherExceptions': |
| otherExceptions = true |
| break |
| } |
| } |
| } |
| |
| if (args.filters) { |
| if (args.filters.indexOf('otherExceptions') >= 0) { |
| otherExceptions = true |
| } |
| } |
| |
| this._runtime.setExceptionsFilters(namedException, otherExceptions) |
| |
| this.sendResponse(response) |
| } |
| |
| protected exceptionInfoRequest( |
| response: DebugProtocol.ExceptionInfoResponse, |
| args: DebugProtocol.ExceptionInfoArguments |
| ) { |
| response.body = { |
| exceptionId: 'Exception ID', |
| description: 'This is a descriptive description of the exception.', |
| breakMode: 'always', |
| details: { |
| message: 'Message contained in the exception.', |
| typeName: 'Short type name of the exception object', |
| stackTrace: 'stack frame 1\nstack frame 2', |
| }, |
| } |
| this.sendResponse(response) |
| } |
| |
| protected threadsRequest(response: DebugProtocol.ThreadsResponse): void { |
| // runtime supports no threads so just return a default thread. |
| response.body = { |
| threads: [new Thread(DaffodilDebugSession.threadID, 'thread 1')], |
| } |
| this.sendResponse(response) |
| } |
| |
| protected stackTraceRequest( |
| response: DebugProtocol.StackTraceResponse, |
| args: DebugProtocol.StackTraceArguments |
| ): void { |
| const startFrame = typeof args.startFrame === 'number' ? args.startFrame : 0 |
| const maxLevels = typeof args.levels === 'number' ? args.levels : 1000 |
| const endFrame = startFrame + maxLevels |
| |
| const stk = this._runtime.stack(startFrame, endFrame) |
| |
| response.body = { |
| stackFrames: stk.frames.map((f) => { |
| const sf = new StackFrame( |
| f.index, |
| f.name, |
| this.createSource(f.file), |
| this.convertDebuggerLineToClient(f.line) |
| ) |
| if (typeof f.column === 'number') { |
| sf.column = this.convertDebuggerColumnToClient(f.column) |
| } |
| return sf |
| }), |
| //no totalFrames: // VS Code has to probe/guess. Should result in a max. of two requests |
| totalFrames: stk.count, // stk.count is the correct size, should result in a max. of two requests |
| //totalFrames: 1000000 // not the correct size, should result in a max. of two requests |
| //totalFrames: endFrame + 20 // dynamically increases the size with every requested chunk, results in paging |
| } |
| this.sendResponse(response) |
| } |
| |
| protected scopesRequest( |
| response: DebugProtocol.ScopesResponse, |
| args: DebugProtocol.ScopesArguments |
| ): void { |
| response.body = { |
| scopes: [ |
| new Scope('Local', this._variableHandles.create('local'), false), |
| new Scope('Global', this._variableHandles.create('global'), true), |
| ], |
| } |
| this.sendResponse(response) |
| } |
| |
| protected async variablesRequest( |
| response: DebugProtocol.VariablesResponse, |
| args: DebugProtocol.VariablesArguments, |
| request?: DebugProtocol.Request |
| ) { |
| const variables: DebugProtocol.Variable[] = [] |
| |
| if (this._isLongrunning.get(args.variablesReference)) { |
| // long running |
| |
| if (request) { |
| this._cancelationTokens.set(request.seq, false) |
| } |
| |
| for (let i = 0; i < 100; i++) { |
| await timeout(1000) |
| variables.push({ |
| name: `i_${i}`, |
| type: 'integer', |
| value: `${i}`, |
| variablesReference: 0, |
| }) |
| if (request && this._cancelationTokens.get(request.seq)) { |
| break |
| } |
| } |
| |
| if (request) { |
| this._cancelationTokens.delete(request.seq) |
| } |
| } else { |
| const id = this._variableHandles.get(args.variablesReference) |
| |
| if (id) { |
| const i = 12345678 |
| variables.push({ |
| name: id + '_i', |
| type: 'integer', |
| value: this._showHex ? '0x' + i.toString(16) : i.toString(10), |
| __vscodeVariableMenuContext: 'simple', |
| variablesReference: 0, |
| } as DebugProtocol.Variable) |
| variables.push({ |
| name: id + '_f', |
| type: 'float', |
| value: '3.14', |
| variablesReference: 0, |
| }) |
| variables.push({ |
| name: id + '_f', |
| type: 'float', |
| value: '6.28', |
| variablesReference: 0, |
| }) |
| variables.push({ |
| name: id + '_f', |
| type: 'float', |
| value: '6.28', |
| variablesReference: 0, |
| }) |
| variables.push({ |
| name: id + '_s', |
| type: 'string', |
| value: 'hello world', |
| variablesReference: 0, |
| }) |
| variables.push({ |
| name: id + '_o', |
| type: 'object', |
| value: 'Object', |
| variablesReference: this._variableHandles.create(id + '_o'), |
| }) |
| |
| // cancellation support for long running requests |
| const nm = id + '_long_running' |
| const ref = this._variableHandles.create(id + '_lr') |
| variables.push({ |
| name: nm, |
| type: 'object', |
| value: 'Object', |
| variablesReference: ref, |
| }) |
| this._isLongrunning.set(ref, true) |
| } |
| } |
| |
| response.body = { |
| variables: variables, |
| } |
| this.sendResponse(response) |
| } |
| |
| protected continueRequest( |
| response: DebugProtocol.ContinueResponse, |
| args: DebugProtocol.ContinueArguments |
| ): void { |
| this._runtime.continue() |
| this.sendResponse(response) |
| } |
| |
| protected reverseContinueRequest( |
| response: DebugProtocol.ReverseContinueResponse, |
| args: DebugProtocol.ReverseContinueArguments |
| ): void { |
| this._runtime.continue(true) |
| this.sendResponse(response) |
| } |
| |
| protected nextRequest( |
| response: DebugProtocol.NextResponse, |
| args: DebugProtocol.NextArguments |
| ): void { |
| this._runtime.step() |
| this.sendResponse(response) |
| } |
| |
| protected stepBackRequest( |
| response: DebugProtocol.StepBackResponse, |
| args: DebugProtocol.StepBackArguments |
| ): void { |
| this._runtime.step(true) |
| this.sendResponse(response) |
| } |
| |
| protected stepInTargetsRequest( |
| response: DebugProtocol.StepInTargetsResponse, |
| args: DebugProtocol.StepInTargetsArguments |
| ) { |
| const targets = this._runtime.getStepInTargets(args.frameId) |
| response.body = { |
| targets: targets.map((t) => { |
| return { id: t.id, label: t.label } |
| }), |
| } |
| this.sendResponse(response) |
| } |
| |
| protected stepInRequest( |
| response: DebugProtocol.StepInResponse, |
| args: DebugProtocol.StepInArguments |
| ): void { |
| this._runtime.stepIn(args.targetId) |
| this.sendResponse(response) |
| } |
| |
| protected stepOutRequest( |
| response: DebugProtocol.StepOutResponse, |
| args: DebugProtocol.StepOutArguments |
| ): void { |
| this._runtime.stepOut() |
| this.sendResponse(response) |
| } |
| |
| protected async evaluateRequest( |
| response: DebugProtocol.EvaluateResponse, |
| args: DebugProtocol.EvaluateArguments |
| ): Promise<void> { |
| let reply: string | undefined = undefined |
| |
| if (args.context === 'repl') { |
| // 'evaluate' supports to create and delete breakpoints from the 'repl': |
| const matches = /new +([0-9]+)/.exec(args.expression) |
| if (matches && matches.length === 2) { |
| const mbp = await this._runtime.setBreakPoint( |
| this._runtime.sourceFile, |
| this.convertClientLineToDebugger(parseInt(matches[1])) |
| ) |
| const bp = new Breakpoint( |
| mbp.verified, |
| this.convertDebuggerLineToClient(mbp.line), |
| undefined, |
| this.createSource(this._runtime.sourceFile) |
| ) as DebugProtocol.Breakpoint |
| bp.id = mbp.id |
| this.sendEvent(new BreakpointEvent('new', bp)) |
| reply = `breakpoint created` |
| } else { |
| const matches = /del +([0-9]+)/.exec(args.expression) |
| if (matches && matches.length === 2) { |
| const mbp = this._runtime.clearBreakPoint( |
| this._runtime.sourceFile, |
| this.convertClientLineToDebugger(parseInt(matches[1])) |
| ) |
| if (mbp) { |
| const bp = new Breakpoint(false) as DebugProtocol.Breakpoint |
| bp.id = mbp.id |
| this.sendEvent(new BreakpointEvent('removed', bp)) |
| reply = `breakpoint deleted` |
| } |
| } else { |
| const matches = /progress/.exec(args.expression) |
| if (matches && matches.length === 1) { |
| if (this._reportProgress) { |
| reply = `progress started` |
| await this.progressSequence() |
| } else { |
| reply = `frontend doesn't support progress (capability 'supportsProgressReporting' not set)` |
| } |
| } |
| } |
| } |
| } |
| |
| response.body = { |
| result: reply |
| ? reply |
| : `evaluate(context: '${args.context}', '${args.expression}')`, |
| variablesReference: 0, |
| } |
| this.sendResponse(response) |
| } |
| |
| private async progressSequence() { |
| const ID = '' + this._progressId++ |
| |
| await timeout(100) |
| |
| const title = this._isProgressCancellable |
| ? 'Cancellable operation' |
| : 'Long running operation' |
| const startEvent: DebugProtocol.ProgressStartEvent = new ProgressStartEvent( |
| ID, |
| title |
| ) |
| startEvent.body.cancellable = this._isProgressCancellable |
| this._isProgressCancellable = !this._isProgressCancellable |
| this.sendEvent(startEvent) |
| this.sendEvent(new OutputEvent(`start progress: ${ID}\n`)) |
| |
| let endMessage = 'progress ended' |
| |
| for (let i = 0; i < 100; i++) { |
| await timeout(500) |
| this.sendEvent(new ProgressUpdateEvent(ID, `progress: ${i}`)) |
| if (this._cancelledProgressId === ID) { |
| endMessage = 'progress cancelled' |
| this._cancelledProgressId = undefined |
| this.sendEvent(new OutputEvent(`cancel progress: ${ID}\n`)) |
| break |
| } |
| } |
| this.sendEvent(new ProgressEndEvent(ID, endMessage)) |
| this.sendEvent(new OutputEvent(`end progress: ${ID}\n`)) |
| |
| this._cancelledProgressId = undefined |
| } |
| |
| protected dataBreakpointInfoRequest( |
| response: DebugProtocol.DataBreakpointInfoResponse, |
| args: DebugProtocol.DataBreakpointInfoArguments |
| ): void { |
| response.body = { |
| dataId: null, |
| description: 'cannot break on data access', |
| accessTypes: undefined, |
| canPersist: false, |
| } |
| |
| if (args.variablesReference && args.name) { |
| const id = this._variableHandles.get(args.variablesReference) |
| if (id === 'global') { |
| response.body.dataId = args.name |
| response.body.description = args.name |
| response.body.accessTypes = ['write'] |
| response.body.canPersist = true |
| } else { |
| response.body.dataId = args.name |
| response.body.description = args.name |
| response.body.accessTypes = ['read', 'write', 'readWrite'] |
| response.body.canPersist = true |
| } |
| } |
| |
| this.sendResponse(response) |
| } |
| |
| protected setDataBreakpointsRequest( |
| response: DebugProtocol.SetDataBreakpointsResponse, |
| args: DebugProtocol.SetDataBreakpointsArguments |
| ): void { |
| // clear all data breakpoints |
| this._runtime.clearAllDataBreakpoints() |
| |
| response.body = { |
| breakpoints: [], |
| } |
| |
| for (const dbp of args.breakpoints) { |
| // assume that id is the "address" to break on |
| const dataId = |
| dbp.dataId + `_${dbp.accessType ? dbp.accessType : 'write'}` |
| const ok = this._runtime.setDataBreakpoint(dataId) |
| response.body.breakpoints.push({ |
| verified: ok, |
| }) |
| } |
| |
| this.sendResponse(response) |
| } |
| |
| protected completionsRequest( |
| response: DebugProtocol.CompletionsResponse, |
| args: DebugProtocol.CompletionsArguments |
| ): void { |
| response.body = { |
| targets: [ |
| { |
| label: 'item 10', |
| sortText: '10', |
| }, |
| { |
| label: 'item 1', |
| sortText: '01', |
| }, |
| { |
| label: 'item 2', |
| sortText: '02', |
| }, |
| { |
| label: 'array[]', |
| selectionStart: 6, |
| sortText: '03', |
| }, |
| { |
| label: 'func(arg)', |
| selectionStart: 5, |
| selectionLength: 3, |
| sortText: '04', |
| }, |
| ], |
| } |
| this.sendResponse(response) |
| } |
| |
| protected cancelRequest( |
| response: DebugProtocol.CancelResponse, |
| args: DebugProtocol.CancelArguments |
| ) { |
| if (args.requestId) { |
| this._cancelationTokens.set(args.requestId, true) |
| } |
| if (args.progressId) { |
| this._cancelledProgressId = args.progressId |
| } |
| } |
| |
| protected customRequest( |
| command: string, |
| response: DebugProtocol.Response, |
| args: any |
| ) { |
| if (command === 'toggleFormatting') { |
| this._showHex = !this._showHex |
| if (this._useInvalidatedEvent) { |
| this.sendEvent(new InvalidatedEvent(['variables'])) |
| } |
| this.sendResponse(response) |
| } else { |
| super.customRequest(command, response, args) |
| } |
| } |
| |
| //---- helpers |
| |
| private createSource(filePath: string): Source { |
| return new Source( |
| basename(filePath), |
| this.convertDebuggerPathToClient(filePath), |
| undefined, |
| undefined, |
| 'daffodil-adapter-data' |
| ) |
| } |
| } |