blob: ec098cb544d78ccff82dcee12992cc6e23094036 [file]
/*---------------------------------------------------------------------------------------------
* 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'
)
}
}