Kendryte-dev-extension/src/debug/backend/kendryteDebugger.ts

629 lines
20 KiB
TypeScript

import { posix } from 'path'
import { DebugSession, Handles, InitializedEvent, Scope, Source, StackFrame, Thread } from 'vscode-debugadapter'
import { DebugProtocol } from 'vscode-debugprotocol'
import { IMyLogger } from '@utils/baseLogger'
import { objectPath } from '../common/library/objectPath'
import { errorMessage, errorStack } from '../common/library/strings'
import { isCommandIssueWhenRunning } from '../common/mi2/mi2AutomaticResponder'
import { createStopEvent, StopReason } from '../common/mi2/pause'
import { BreakpointType, MyBreakpointFunc, MyBreakpointLine } from '../common/mi2/types'
import { toProtocolBreakpoint } from '../common/mi2/types.convert'
import { BackendLogger } from './lib/backendLogger'
import { IDebugConsole, wrapDebugConsole } from './lib/duplexDebugConsole'
import { ErrorCode, ErrorMi2, handleMethodPromise } from './lib/handleMethodPromise'
import { expandValue } from './session/gdb_expansion'
import { DebuggingSession } from './session/session'
import { AttachRequestArguments, LaunchRequestArguments, ValuesFormattingMode, VariableObject } from './type'
const resolve = posix.resolve
const relative = posix.relative
class ExtendedVariable {
constructor(public name: VariableId, public options: any) {
}
}
type VariableId = number | string | VariableObject | ExtendedVariable
enum ErrorCodeValue {
Unknown = 1,
Restart,
Initialize,
Evaluate,
Attach,
Launch,
Disconnect,
SetVariable,
Breakpoints,
Threads,
StackTrace,
init,
Scopes,
VariableNumber,
VariableString,
ExpandVariable,
VariableObject,
VariableExtend,
Continue,
Pause,
StepIn,
StepOut,
StepNext,
}
const STACK_HANDLES_START = 1000
const VAR_HANDLES_START = 512 * 256 + 1000
export class KendryteDebugger extends DebugSession {
protected variableHandles = new Handles<VariableId>(VAR_HANDLES_START)
protected variableHandlesReverse: { [id: string]: number } = {}
protected useVarObjects!: boolean
protected debugInstance!: DebuggingSession
protected readonly debugLogger: IMyLogger
protected readonly vscodeProtocolLogger: IMyLogger
private readonly debugConsole: IDebugConsole
private initComplete: boolean = false
private autoContinue: boolean = true
private firstThreadRequest: boolean = true
public constructor(debuggerLinesStartAt1: boolean, isServer: boolean = false) {
super(debuggerLinesStartAt1, isServer)
this.vscodeProtocolLogger = new BackendLogger('protocol', this)
this.debugLogger = new BackendLogger('gdb', this)
this.debugConsole = wrapDebugConsole(this, this.vscodeProtocolLogger)
process.on('unhandledRejection', (reason, p) => {
p.catch((e) => {
if (e instanceof ErrorMi2) {
this.debugLogger.error('Unhandled Mi2 Rejection: ' + (e.node.token ? 'token=' + e.node.token : 'result=' + e.node.rawLine))
this.debugLogger.error(errorStack(e))
} else {
this.debugLogger.error('Unhandled Rejection: ' + errorStack(e))
}
this.debugConsole.errorUser('[kendryte debug] Unhandled Rejection: ' + errorMessage(e))
})
})
process.on('uncaughtException', (err) => {
console.error(err)
this.debugLogger.error('uncaughtException: ' + errorStack(err))
this.debugConsole.errorUser('[kendryte debug] Fatal error during debugging session. Catched unhandled exception.\n' + errorStack(err))
setTimeout(() => {
process.exit(1)
}, 2000)
})
}
private fireEvent(ev: DebugProtocol.Event) {
if (this.autoContinue && !this.initComplete && (ev.event === 'stopped' || ev.event === 'continued')) {
this.vscodeProtocolLogger.info('initialize not complete, event muted:', JSON.stringify(ev))
return
}
this.vscodeProtocolLogger.info('fireEvent: [%s]%s', ev.event, JSON.stringify(ev.body))
this.sendEvent(ev)
}
private resetStatus() {
delete this.debugInstance
this.initComplete = false
this.autoContinue = true
}
private async attachOrLaunch(args: AttachRequestArguments | LaunchRequestArguments, load: boolean) {
if (this.debugInstance) {
await this.debugInstance.terminate()
}
const logger = new BackendLogger(args.id ? 'gdb-' + args.id : 'gdb-main', this)
const debugConsole = wrapDebugConsole(this, logger)
this.debugInstance = new DebuggingSession(args, logger, debugConsole)
this.debugInstance.onEvent((ev) => this.fireEvent(ev))
await this.debugInstance.connect(load)
this.setValuesFormattingMode(args.valuesFormatting)
this.debugInstance.disconnected.then((self) => {
if (self === this.debugInstance) {
this.resetStatus()
}
})
this.sendEvent(new InitializedEvent())
this.initComplete = true
}
protected setValuesFormattingMode(mode: ValuesFormattingMode) {
switch (mode) {
case 'disabled':
this.useVarObjects = true
// this.debugInstance.prettyPrint = false
break
case 'prettyPrinters':
this.useVarObjects = true
// this.debugInstance.prettyPrint = true
break
case 'parseText':
default:
this.useVarObjects = false
// this.debugInstance.prettyPrint = false
}
}
// Supports 256 threads.
protected threadAndLevelToFrameId(threadId: number, level: number) {
return level << 8 | threadId
}
protected frameIdToThreadAndLevel(frameId: number) {
return [frameId & 0xff, frameId >> 8]
}
private _createVariable(arg: VariableId, options?: any) {
if (options) {
return this.variableHandles.create(new ExtendedVariable(arg, options))
} else {
return this.variableHandles.create(arg)
}
}
private _findOrCreateVariable(varObj: VariableObject): number {
let id: number
if (this.variableHandlesReverse.hasOwnProperty(varObj.name)) {
id = this.variableHandlesReverse[varObj.name]
} else {
id = this._createVariable(varObj)
this.variableHandlesReverse[varObj.name] = id
}
return varObj.isCompound() ? id : 0
}
@handleMethodPromise(ErrorCodeValue.VariableNumber)
private async variableNumber(response: DebugProtocol.VariablesResponse, id: number): Promise<void> {
const variables: DebugProtocol.Variable[] = []
const [threadId, level] = this.frameIdToThreadAndLevel(id)
const stack = await this.debugInstance.getStackVariables(threadId, level)
for (const variable of stack) {
if (this.useVarObjects) {
try {
const varObjName = `var_${id}_${variable.name}`
let varObj: VariableObject
try {
const changes = await this.debugInstance.varUpdate(varObjName)
const changelist = changes.result('changelist')
changelist.forEach((change: any) => {
const name = objectPath(change, 'name')
const vId = this.variableHandlesReverse[name]
const v = this.variableHandles.get(vId) as any
v.applyChanges(change)
})
const varId = this.variableHandlesReverse[varObjName]
varObj = this.variableHandles.get(varId) as any
} catch (err) {
if (err.message.includes('Variable object not found')) {
varObj = await this.debugInstance.varCreate(variable.name, varObjName)
const varId = this._findOrCreateVariable(varObj)
varObj.exp = variable.name
varObj.id = varId
} else {
return Promise.reject(err)
}
}
variables.push(varObj.toProtocolVariable())
} catch (err) {
variables.push({
name: variable.name,
value: `<${err}>`,
variablesReference: 0,
})
}
} else {
if (variable.valueStr !== undefined) {
let expanded = expandValue(this._createVariable.bind(this), `{${variable.name}=${variable.valueStr})`, '', variable.raw)
if (expanded) {
if (typeof expanded[0] == 'string') {
expanded = [
{
name: '<value>',
value: prettyStringArray(expanded),
variablesReference: 0,
},
]
}
variables.push(expanded[0])
}
} else {
variables.push({
name: variable.name,
type: variable.type,
value: '<unknown>',
variablesReference: this._createVariable(variable.name),
})
}
}
}
response.body = { variables }
}
@handleMethodPromise(ErrorCodeValue.VariableString)
private async variableString(response: DebugProtocol.VariablesResponse, id: string) {
// Variable members
// TODO: this evals on an (effectively) unknown thread for multithreaded programs.
const variable = await this.debugInstance.evalExpression(JSON.stringify(id), 0, 0)
let expanded = expandValue(this._createVariable.bind(this), variable.result('value'), id, variable)
if (!expanded) {
throw new ErrorCode(ErrorCodeValue.ExpandVariable, `Could not expand variable`)
} else {
if (typeof expanded[0] == 'string') {
expanded = [
{
name: '<value>',
value: prettyStringArray(expanded),
variablesReference: 0,
},
]
}
response.body = { variables: expanded }
}
}
@handleMethodPromise(ErrorCodeValue.VariableObject)
private async variableObject(response: DebugProtocol.VariablesResponse, id: VariableObject) {
// Variable members
const children: VariableObject[] = await this.debugInstance.varListChildren(id.name)
const vars = children.map(child => {
child.id = this._findOrCreateVariable(child)
return child.toProtocolVariable()
})
response.body = { variables: vars }
}
@handleMethodPromise(ErrorCodeValue.VariableExtend)
private async variableExtended(response: DebugProtocol.VariablesResponse, varReq: ExtendedVariable) {
response.body = { variables: [] }
if (varReq.options.arg) {
let argsPart = true
let arrIndex = 0
const addOne = async () => {
// TODO: this evals on an (effectively) unknown thread for multithreaded programs.
const variable = await this.debugInstance.evalExpression(JSON.stringify(`${varReq.name}+${arrIndex})`), 0, 0)
const expanded = expandValue(this._createVariable.bind(this), variable.result('value'), '' + varReq.name, variable)
if (!expanded) {
throw new ErrorCode(ErrorCodeValue.ExpandVariable, `Could not expand variable`)
}
try {
if (typeof expanded == 'string') {
if (expanded == '<nullptr>') {
if (argsPart) {
argsPart = false
} else {
return
}
} else if (expanded[0] != '"') {
response.body.variables.push({
name: '[err]',
value: expanded,
variablesReference: 0,
})
return
}
response.body.variables.push({
name: `[${(arrIndex++)}]`,
value: expanded,
variablesReference: 0,
})
await addOne()
} else {
response.body.variables.push({
name: '[err]',
value: expanded,
variablesReference: 0,
})
return
}
} catch (e) {
throw new ErrorCode(ErrorCodeValue.ExpandVariable, `Could not expand variable: ${e}`)
}
}
await addOne()
} else {
throw new ErrorCode(13, `Unimplemented variable request options: ${JSON.stringify(varReq.options)}`)
}
}
@handleMethodPromise(ErrorCodeValue.Initialize)
protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void {
this.vscodeProtocolLogger.info('initializeRequest: ')
if (response.body) {
response.body.supportsConfigurationDoneRequest = true
response.body.supportsConditionalBreakpoints = true
response.body.supportsFunctionBreakpoints = true
response.body.supportsEvaluateForHovers = true
response.body.supportsSetVariable = true
response.body.supportsRestartRequest = true
response.body.supportsLogPoints = true
}
}
@handleMethodPromise(ErrorCodeValue.Disconnect)
disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments) {
return this.debugInstance.terminate()
}
@handleMethodPromise(ErrorCodeValue.Launch)
protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments) {
return this.attachOrLaunch(args, true)
}
@handleMethodPromise(ErrorCodeValue.Attach)
protected async attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments) {
return this.attachOrLaunch(args, false)
}
@handleMethodPromise(ErrorCodeValue.Restart)
async restartRequest(response: DebugProtocol.RestartResponse, args: DebugProtocol.RestartArguments) {
return this.debugInstance.reload()
}
@handleMethodPromise(ErrorCodeValue.Breakpoints)
async setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments) {
await this.debugInstance.connected
const myBreaks: MyBreakpointLine[] = args.breakpoints ? args.breakpoints.map((bk) => {
return <MyBreakpointLine>{
type: BreakpointType.Line,
file: args.source.path,
line: bk.line,
condition: bk.condition,
logMessage: bk.logMessage,
}
}) : []
const resultBreaks = await this.debugInstance.updateBreakPoints(args.source.path || '', myBreaks)
if (!response.body) {
response.body = { breakpoints: [] }
}
if (!response.body.breakpoints) {
response.body.breakpoints = []
}
for (const newBreak of resultBreaks) {
response.body.breakpoints.push(toProtocolBreakpoint(newBreak))
}
}
@handleMethodPromise(ErrorCodeValue.Breakpoints)
async setFunctionBreakPointsRequest(response: DebugProtocol.SetFunctionBreakpointsResponse, args: DebugProtocol.SetFunctionBreakpointsArguments) {
await this.debugInstance.connected
const myBreaks: MyBreakpointFunc[] = args.breakpoints.map((bk) => {
return <MyBreakpointFunc>{
type: BreakpointType.Function,
name: bk.name,
condition: bk.condition,
}
})
const resultBreaks = await this.debugInstance.updateBreakPoints('', myBreaks)
if (!response.body) {
response.body = { breakpoints: [] }
}
if (!response.body.breakpoints) {
response.body.breakpoints = []
}
for (const newBreak of resultBreaks) {
response.body.breakpoints.push(toProtocolBreakpoint(newBreak))
}
}
@handleMethodPromise(ErrorCodeValue.init)
protected configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse, args: DebugProtocol.ConfigurationDoneArguments): void {
this.debugConsole.logUser('Configuration done!')
setImmediate(() => {
if (this.autoContinue && !this.debugInstance.isRunning) {
this.debugInstance.continue().finally(() => {
this.debugLogger.writeln('')
})
} else if (!this.autoContinue && this.debugInstance.isRunning) {
this.debugInstance.interrupt().finally(() => {
this.debugLogger.writeln('')
})
}
})
}
@handleMethodPromise(ErrorCodeValue.Continue)
continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments) {
console.log('get continue')
return this.debugInstance.continue()
}
@handleMethodPromise(ErrorCodeValue.StepNext)
protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments) {
console.log('get next')
return this.debugInstance.next()
}
@handleMethodPromise(ErrorCodeValue.StepIn)
stepInRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments) {
return this.debugInstance.step()
}
@handleMethodPromise(ErrorCodeValue.StepOut)
stepOutRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments) {
return this.debugInstance.stepOut()
}
@handleMethodPromise(ErrorCodeValue.Pause)
async pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments) {
await this.debugInstance.interrupt()
setTimeout(() => {
this.fireEvent(createStopEvent(StopReason.Pausing, undefined, 'pause button clicked'))
}, 500)
}
@handleMethodPromise(ErrorCodeValue.Threads)
async threadsRequest(response: DebugProtocol.ThreadsResponse) {
if (!this.debugInstance) {
return
}
const threads = await this.debugInstance.getThreads(this.firstThreadRequest).catch((e) => {
if (isCommandIssueWhenRunning(e)) {
return []
} else {
throw e
}
})
this.firstThreadRequest = false
response.body = {
threads: [],
}
for (const thread of threads) {
let threadName = thread.name
// TODO: Thread names are undefined on LLDB
if (threadName === undefined) {
threadName = thread.targetId
}
if (threadName === undefined) {
threadName = '<unnamed>'
}
response.body.threads.push(new Thread(thread.id, thread.id + ':' + threadName))
}
}
@handleMethodPromise(ErrorCodeValue.StackTrace)
protected async stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments) {
const stack = await this.debugInstance.getStack(args.levels || 0, args.threadId)
const ret: StackFrame[] = []
stack.forEach(element => {
let source = undefined
let file = element.file
if (file) {
if (process.platform === 'win32') {
if (file.startsWith('\\cygdrive\\') || file.startsWith('/cygdrive/')) {
file = file[10] + ':' + file.substr(11) // replaces /cygdrive/c/foo/bar.txt with c:/foo/bar.txt
}
}
source = new Source(element.fileName, file)
}
ret.push(new StackFrame(
this.threadAndLevelToFrameId(args.threadId, element.level),
element.function + '@' + element.address,
source,
element.line,
0,
))
})
response.body = {
stackFrames: ret,
}
}
/*
@handleMethodPromise(4, 'Could not step back: %s - Try running \'target record-full\' before stepping back')
protected stepBackRequest(response: DebugProtocol.StepBackResponse, args: DebugProtocol.StepBackArguments): void {
return this.debugInstance.step()
}
*/
@handleMethodPromise(ErrorCodeValue.Scopes)
protected scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments) {
const scopes: Scope[] = []
scopes.push(new Scope('Local', STACK_HANDLES_START + (parseInt(args.frameId as any) || 0), false))
response.body = {
scopes: scopes,
}
}
protected variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments): Promise<void> | undefined {
let id: VariableId
if (args.variablesReference < VAR_HANDLES_START) {
id = args.variablesReference - STACK_HANDLES_START
} else {
id = this.variableHandles.get(args.variablesReference)
}
if (typeof id == 'number') {
return this.variableNumber(response, id)
} else if (typeof id == 'string') {
return this.variableString(response, id)
} else if (id && id instanceof VariableObject) {
return this.variableObject(response, id)
} else if (id && id instanceof ExtendedVariable) {
return this.variableExtended(response, id)
// } else if (typeof id === 'object') {
// response.body = {
// variables: id as any,
// }
// this.sendResponse(response)
} else {
response.body = { variables: [] }
this.sendResponse(response)
}
}
@handleMethodPromise(ErrorCodeValue.SetVariable)
async setVariableRequest(response: DebugProtocol.SetVariableResponse, args: DebugProtocol.SetVariableArguments): Promise<void> {
if (this.useVarObjects) {
let name = args.name
if (args.variablesReference >= VAR_HANDLES_START) {
const parent = this.variableHandles.get(args.variablesReference) as VariableObject
name = `${parent.name}.${name}`
}
const newValue = await this.debugInstance.varAssign(name, args.value)
response.body = { value: newValue }
} else {
const value = await this.debugInstance.changeVariable(args.name, args.value)
response.body = { value }
}
}
@handleMethodPromise(ErrorCodeValue.Evaluate)
async evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments) {
const [threadId, level] = this.frameIdToThreadAndLevel(args.frameId || 0)
if (args.context == 'watch' || args.context == 'hover') {
const res = await this.debugInstance.evalExpression(args.expression, threadId, level)
response.body = {
variablesReference: 0,
result: res.result('value'),
}
} else {
const output = await this.debugInstance.sendUserInput(args.expression, threadId, level)
if (typeof output == 'undefined') {
response.body = {
result: '',
variablesReference: 0,
}
} else {
response.body = {
result: JSON.stringify(output),
variablesReference: 0,
}
}
}
}
}
function prettyStringArray(strings: any) {
if (typeof strings == 'object') {
if (strings.length !== undefined) {
return strings.join(', ')
} else {
return JSON.stringify(strings)
}
} else {
return strings
}
}