| /*--------------------------------------------------------- |
| * Copyright (C) Microsoft Corporation. All rights reserved. |
| * Licensed under the MIT License. See LICENSE in the project root for license information. |
| *--------------------------------------------------------*/ |
| |
| import { ChildProcess, execFile, spawn } from 'child_process'; |
| import { EventEmitter } from 'events'; |
| import * as fs from 'fs'; |
| import { existsSync, lstatSync } from 'fs'; |
| import * as glob from 'glob'; |
| import { Client, RPCConnection } from 'json-rpc2'; |
| import * as os from 'os'; |
| import * as path from 'path'; |
| import kill = require('tree-kill'); |
| import * as util from 'util'; |
| import { |
| DebugSession, |
| ErrorDestination, |
| Handles, |
| InitializedEvent, |
| logger, |
| Logger, |
| LoggingDebugSession, |
| OutputEvent, |
| Scope, |
| Source, |
| StackFrame, |
| StoppedEvent, |
| TerminatedEvent, |
| Thread |
| } from 'vscode-debugadapter'; |
| import { DebugProtocol } from 'vscode-debugprotocol'; |
| import { |
| envPath, |
| fixDriveCasingInWindows, |
| getBinPathWithPreferredGopathGoroot, |
| getCurrentGoWorkspaceFromGOPATH, |
| getInferredGopath, |
| parseEnvFile |
| } from '../goPath'; |
| |
| const fsAccess = util.promisify(fs.access); |
| const fsUnlink = util.promisify(fs.unlink); |
| |
| // This enum should stay in sync with https://golang.org/pkg/reflect/#Kind |
| |
| enum GoReflectKind { |
| Invalid = 0, |
| Bool, |
| Int, |
| Int8, |
| Int16, |
| Int32, |
| Int64, |
| Uint, |
| Uint8, |
| Uint16, |
| Uint32, |
| Uint64, |
| Uintptr, |
| Float32, |
| Float64, |
| Complex64, |
| Complex128, |
| Array, |
| Chan, |
| Func, |
| Interface, |
| Map, |
| Ptr, |
| Slice, |
| String, |
| Struct, |
| UnsafePointer |
| } |
| |
| // These types should stay in sync with: |
| // https://github.com/go-delve/delve/blob/master/service/api/types.go |
| |
| interface CommandOut { |
| State: DebuggerState; |
| } |
| |
| interface DebuggerState { |
| exited: boolean; |
| exitStatus: number; |
| breakPoint: DebugBreakpoint; |
| breakPointInfo: {}; |
| currentThread: DebugThread; |
| currentGoroutine: DebugGoroutine; |
| Running: boolean; |
| } |
| |
| export interface PackageBuildInfo { |
| ImportPath: string; |
| DirectoryPath: string; |
| Files: string[]; |
| } |
| |
| export interface ListPackagesBuildInfoOut { |
| List: PackageBuildInfo[]; |
| } |
| |
| export interface ListSourcesOut { |
| Sources: string[]; |
| } |
| |
| interface CreateBreakpointOut { |
| Breakpoint: DebugBreakpoint; |
| } |
| |
| interface GetVersionOut { |
| DelveVersion: string; |
| APIVersion: number; |
| } |
| |
| interface DebugBreakpoint { |
| addr: number; |
| continue: boolean; |
| file: string; |
| functionName?: string; |
| goroutine: boolean; |
| id: number; |
| name: string; |
| line: number; |
| stacktrace: number; |
| variables?: DebugVariable[]; |
| loadArgs?: LoadConfig; |
| loadLocals?: LoadConfig; |
| cond?: string; |
| } |
| |
| interface LoadConfig { |
| // FollowPointers requests pointers to be automatically dereferenced. |
| followPointers: boolean; |
| // MaxVariableRecurse is how far to recurse when evaluating nested types. |
| maxVariableRecurse: number; |
| // MaxStringLen is the maximum number of bytes read from a string |
| maxStringLen: number; |
| // MaxArrayValues is the maximum number of elements read from an array, a slice or a map. |
| maxArrayValues: number; |
| // MaxStructFields is the maximum number of fields read from a struct, -1 will read all fields. |
| maxStructFields: number; |
| } |
| |
| interface DebugThread { |
| file: string; |
| id: number; |
| line: number; |
| pc: number; |
| goroutineID: number; |
| function?: DebugFunction; |
| } |
| |
| interface StacktraceOut { |
| Locations: DebugLocation[]; |
| } |
| |
| interface DebugLocation { |
| pc: number; |
| file: string; |
| line: number; |
| function: DebugFunction; |
| } |
| |
| interface DebugFunction { |
| name: string; |
| value: number; |
| type: number; |
| goType: number; |
| args: DebugVariable[]; |
| locals: DebugVariable[]; |
| optimized: boolean; |
| } |
| |
| interface ListVarsOut { |
| Variables: DebugVariable[]; |
| } |
| |
| interface ListFunctionArgsOut { |
| Args: DebugVariable[]; |
| } |
| |
| interface EvalOut { |
| Variable: DebugVariable; |
| } |
| |
| enum GoVariableFlags { |
| VariableEscaped = 1, |
| VariableShadowed = 2, |
| VariableConstant = 4, |
| VariableArgument = 8, |
| VariableReturnArgument = 16 |
| } |
| |
| interface DebugVariable { |
| name: string; |
| addr: number; |
| type: string; |
| realType: string; |
| kind: GoReflectKind; |
| flags: GoVariableFlags; |
| onlyAddr: boolean; |
| DeclLine: number; |
| value: string; |
| len: number; |
| cap: number; |
| children: DebugVariable[]; |
| unreadable: string; |
| fullyQualifiedName: string; |
| base: number; |
| } |
| |
| interface ListGoroutinesOut { |
| Goroutines: DebugGoroutine[]; |
| } |
| |
| interface DebugGoroutine { |
| id: number; |
| currentLoc: DebugLocation; |
| userCurrentLoc: DebugLocation; |
| goStatementLoc: DebugLocation; |
| } |
| |
| interface DebuggerCommand { |
| name: string; |
| threadID?: number; |
| goroutineID?: number; |
| } |
| |
| interface ListBreakpointsOut { |
| Breakpoints: DebugBreakpoint[]; |
| } |
| |
| interface RestartOut { |
| DiscardedBreakpoints: DiscardedBreakpoint[]; |
| } |
| |
| interface DiscardedBreakpoint { |
| breakpoint: DebugBreakpoint; |
| reason: string; |
| } |
| |
| // This interface should always match the schema found in `package.json`. |
| interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments { |
| request: 'launch'; |
| [key: string]: any; |
| program: string; |
| stopOnEntry?: boolean; |
| args?: string[]; |
| showLog?: boolean; |
| logOutput?: string; |
| cwd?: string; |
| env?: { [key: string]: string }; |
| mode?: 'auto' | 'debug' | 'remote' | 'test' | 'exec'; |
| remotePath?: string; |
| port?: number; |
| host?: string; |
| buildFlags?: string; |
| init?: string; |
| trace?: 'verbose' | 'log' | 'error'; |
| /** Optional path to .env file. */ |
| envFile?: string | string[]; |
| backend?: string; |
| output?: string; |
| /** Delve LoadConfig parameters */ |
| dlvLoadConfig?: LoadConfig; |
| dlvToolPath: string; |
| /** Delve Version */ |
| apiVersion: number; |
| /** Delve maximum stack trace depth */ |
| stackTraceDepth: number; |
| |
| showGlobalVariables?: boolean; |
| packagePathToGoModPathMap: { [key: string]: string }; |
| } |
| |
| interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments { |
| request: 'attach'; |
| processId?: number; |
| stopOnEntry?: boolean; |
| showLog?: boolean; |
| logOutput?: string; |
| cwd?: string; |
| mode?: 'local' | 'remote'; |
| remotePath?: string; |
| port?: number; |
| host?: string; |
| trace?: 'verbose' | 'log' | 'error'; |
| backend?: string; |
| /** Delve LoadConfig parameters */ |
| dlvLoadConfig?: LoadConfig; |
| dlvToolPath: string; |
| /** Delve Version */ |
| apiVersion: number; |
| /** Delve maximum stack trace depth */ |
| stackTraceDepth: number; |
| |
| showGlobalVariables?: boolean; |
| } |
| |
| process.on('uncaughtException', (err: any) => { |
| const errMessage = err && (err.stack || err.message); |
| logger.error(`Unhandled error in debug adapter: ${errMessage}`); |
| throw err; |
| }); |
| |
| function logArgsToString(args: any[]): string { |
| return args |
| .map((arg) => { |
| return typeof arg === 'string' ? arg : JSON.stringify(arg); |
| }) |
| .join(' '); |
| } |
| |
| function log(...args: any[]) { |
| logger.warn(logArgsToString(args)); |
| } |
| |
| function logError(...args: any[]) { |
| logger.error(logArgsToString(args)); |
| } |
| |
| function findPathSeparator(filePath: string) { |
| return filePath.includes('/') ? '/' : '\\'; |
| } |
| |
| export function escapeGoModPath(filePath: string) { |
| return filePath.replace(/[A-Z]/g, (match: string) => `!${match.toLocaleLowerCase()}`); |
| } |
| |
| function normalizePath(filePath: string) { |
| if (process.platform === 'win32') { |
| const pathSeparator = findPathSeparator(filePath); |
| filePath = path.normalize(filePath); |
| // Normalize will replace everything with backslash on Windows. |
| filePath = filePath.replace(/\\/g, pathSeparator); |
| return fixDriveCasingInWindows(filePath); |
| } |
| return filePath; |
| } |
| |
| function getBaseName(filePath: string) { |
| return filePath.includes('/') ? path.basename(filePath) : path.win32.basename(filePath); |
| } |
| |
| export class Delve { |
| public program: string; |
| public remotePath: string; |
| public loadConfig: LoadConfig; |
| public connection: Promise<RPCConnection>; |
| public onstdout: (str: string) => void; |
| public onstderr: (str: string) => void; |
| public onclose: (code: number) => void; |
| public noDebug: boolean; |
| public isApiV1: boolean; |
| public dlvEnv: any; |
| public stackTraceDepth: number; |
| public isRemoteDebugging: boolean; |
| public goroot: string; |
| private localDebugeePath: string | undefined; |
| private debugProcess: ChildProcess; |
| private request: 'attach' | 'launch'; |
| |
| constructor(launchArgs: LaunchRequestArguments | AttachRequestArguments, program: string) { |
| this.request = launchArgs.request; |
| this.program = normalizePath(program); |
| this.remotePath = launchArgs.remotePath; |
| this.isApiV1 = false; |
| if (typeof launchArgs.apiVersion === 'number') { |
| this.isApiV1 = launchArgs.apiVersion === 1; |
| } |
| this.stackTraceDepth = typeof launchArgs.stackTraceDepth === 'number' ? launchArgs.stackTraceDepth : 50; |
| this.connection = new Promise(async (resolve, reject) => { |
| const mode = launchArgs.mode; |
| let dlvCwd = path.dirname(program); |
| let serverRunning = false; |
| const dlvArgs = new Array<string>(); |
| |
| // Get default LoadConfig values according to delve API: |
| // https://github.com/go-delve/delve/blob/c5c41f635244a22d93771def1c31cf1e0e9a2e63/service/rpc1/server.go#L13 |
| // https://github.com/go-delve/delve/blob/c5c41f635244a22d93771def1c31cf1e0e9a2e63/service/rpc2/server.go#L423 |
| this.loadConfig = launchArgs.dlvLoadConfig || { |
| followPointers: true, |
| maxVariableRecurse: 1, |
| maxStringLen: 64, |
| maxArrayValues: 64, |
| maxStructFields: -1 |
| }; |
| |
| if (mode === 'remote') { |
| log(`Start remote debugging: connecting ${launchArgs.host}:${launchArgs.port}`); |
| this.debugProcess = null; |
| this.isRemoteDebugging = true; |
| this.goroot = await queryGOROOT(dlvCwd, process.env); |
| serverRunning = true; // assume server is running when in remote mode |
| connectClient(launchArgs.port, launchArgs.host); |
| return; |
| } |
| this.isRemoteDebugging = false; |
| let env: NodeJS.ProcessEnv; |
| if (launchArgs.request === 'launch') { |
| let isProgramDirectory = false; |
| // Validations on the program |
| if (!program) { |
| return reject('The program attribute is missing in the debug configuration in launch.json'); |
| } |
| try { |
| const pstats = lstatSync(program); |
| if (pstats.isDirectory()) { |
| if (mode === 'exec') { |
| logError(`The program "${program}" must not be a directory in exec mode`); |
| return reject('The program attribute must be an executable in exec mode'); |
| } |
| dlvCwd = program; |
| isProgramDirectory = true; |
| } else if (mode !== 'exec' && path.extname(program) !== '.go') { |
| logError(`The program "${program}" must be a valid go file in debug mode`); |
| return reject('The program attribute must be a directory or .go file in debug mode'); |
| } |
| } catch (e) { |
| logError(`The program "${program}" does not exist: ${e}`); |
| return reject('The program attribute must point to valid directory, .go file or executable.'); |
| } |
| |
| // read env from disk and merge into env variables |
| const fileEnvs = []; |
| try { |
| if (typeof launchArgs.envFile === 'string') { |
| fileEnvs.push(parseEnvFile(launchArgs.envFile)); |
| } |
| if (Array.isArray(launchArgs.envFile)) { |
| launchArgs.envFile.forEach((envFile) => { |
| fileEnvs.push(parseEnvFile(envFile)); |
| }); |
| } |
| } catch (e) { |
| return reject(e); |
| } |
| |
| const launchArgsEnv = launchArgs.env || {}; |
| env = Object.assign({}, process.env, ...fileEnvs, launchArgsEnv); |
| |
| const dirname = isProgramDirectory ? program : path.dirname(program); |
| if (!env['GOPATH'] && (mode === 'debug' || mode === 'test')) { |
| // If no GOPATH is set, then infer it from the file/package path |
| // Not applicable to exec mode in which case `program` need not point to source code under GOPATH |
| env['GOPATH'] = getInferredGopath(dirname) || env['GOPATH']; |
| } |
| this.dlvEnv = env; |
| this.goroot = await queryGOROOT(dlvCwd, env); |
| |
| log(`Using GOPATH: ${env['GOPATH']}`); |
| log(`Using GOROOT: ${this.goroot}`); |
| log(`Using PATH: ${env['PATH']}`); |
| |
| if (!!launchArgs.noDebug) { |
| if (mode === 'debug') { |
| this.noDebug = true; |
| const runArgs = ['run']; |
| const runOptions: { [key: string]: any } = { cwd: dirname, env }; |
| if (launchArgs.buildFlags) { |
| runArgs.push(launchArgs.buildFlags); |
| } |
| if (isProgramDirectory) { |
| runArgs.push('.'); |
| } else { |
| runArgs.push(program); |
| } |
| if (launchArgs.args) { |
| runArgs.push(...launchArgs.args); |
| } |
| |
| const goExe = getBinPathWithPreferredGopathGoroot('go', []); |
| log(`Current working directory: ${dirname}`); |
| log(`Running: ${goExe} ${runArgs.join(' ')}`); |
| |
| this.debugProcess = spawn(goExe, runArgs, runOptions); |
| this.debugProcess.stderr.on('data', (chunk) => { |
| const str = chunk.toString(); |
| if (this.onstderr) { |
| this.onstderr(str); |
| } |
| }); |
| this.debugProcess.stdout.on('data', (chunk) => { |
| const str = chunk.toString(); |
| if (this.onstdout) { |
| this.onstdout(str); |
| } |
| }); |
| this.debugProcess.on('close', (code) => { |
| if (code) { |
| logError(`Process exiting with code: ${code} signal: ${this.debugProcess.killed}`); |
| } else { |
| log(`Process exiting normally ${this.debugProcess.killed}`); |
| } |
| if (this.onclose) { |
| this.onclose(code); |
| } |
| }); |
| this.debugProcess.on('error', (err) => { |
| reject(err); |
| }); |
| resolve(); |
| return; |
| } |
| } |
| this.noDebug = false; |
| |
| if (!existsSync(launchArgs.dlvToolPath)) { |
| log( |
| `Couldn't find dlv at the Go tools path, ${process.env['GOPATH']}${ |
| env['GOPATH'] ? ', ' + env['GOPATH'] : '' |
| } or ${envPath}` |
| ); |
| return reject( |
| `Cannot find Delve debugger. Install from https://github.com/go-delve/delve & ensure it is in your Go tools path, "GOPATH/bin" or "PATH".` |
| ); |
| } |
| |
| const currentGOWorkspace = getCurrentGoWorkspaceFromGOPATH(env['GOPATH'], dirname); |
| dlvArgs.push(mode || 'debug'); |
| if (mode === 'exec' || (mode === 'debug' && !isProgramDirectory)) { |
| dlvArgs.push(program); |
| } else if (currentGOWorkspace && !launchArgs.packagePathToGoModPathMap[dirname]) { |
| dlvArgs.push(dirname.substr(currentGOWorkspace.length + 1)); |
| } |
| dlvArgs.push('--headless=true', `--listen=${launchArgs.host}:${launchArgs.port}`); |
| if (!this.isApiV1) { |
| dlvArgs.push('--api-version=2'); |
| } |
| |
| if (launchArgs.showLog) { |
| dlvArgs.push('--log=' + launchArgs.showLog.toString()); |
| } |
| if (launchArgs.logOutput) { |
| dlvArgs.push('--log-output=' + launchArgs.logOutput); |
| } |
| if (launchArgs.cwd) { |
| dlvArgs.push('--wd=' + launchArgs.cwd); |
| } |
| if (launchArgs.buildFlags) { |
| dlvArgs.push('--build-flags=' + launchArgs.buildFlags); |
| } |
| if (launchArgs.init) { |
| dlvArgs.push('--init=' + launchArgs.init); |
| } |
| if (launchArgs.backend) { |
| dlvArgs.push('--backend=' + launchArgs.backend); |
| } |
| if (launchArgs.output && (mode === 'debug' || mode === 'test')) { |
| dlvArgs.push('--output=' + launchArgs.output); |
| } |
| if (launchArgs.args && launchArgs.args.length > 0) { |
| dlvArgs.push('--', ...launchArgs.args); |
| } |
| this.localDebugeePath = this.getLocalDebugeePath(launchArgs.output); |
| } else if (launchArgs.request === 'attach') { |
| if (!launchArgs.processId) { |
| return reject(`Missing process ID`); |
| } |
| |
| if (!existsSync(launchArgs.dlvToolPath)) { |
| return reject( |
| `Cannot find Delve debugger. Install from https://github.com/go-delve/delve & ensure it is in your Go tools path, "GOPATH/bin" or "PATH".` |
| ); |
| } |
| |
| dlvArgs.push('attach', `${launchArgs.processId}`); |
| dlvArgs.push('--headless=true', '--listen=' + launchArgs.host + ':' + launchArgs.port.toString()); |
| if (!this.isApiV1) { |
| dlvArgs.push('--api-version=2'); |
| } |
| |
| if (launchArgs.showLog) { |
| dlvArgs.push('--log=' + launchArgs.showLog.toString()); |
| } |
| if (launchArgs.logOutput) { |
| dlvArgs.push('--log-output=' + launchArgs.logOutput); |
| } |
| if (launchArgs.cwd) { |
| dlvArgs.push('--wd=' + launchArgs.cwd); |
| } |
| if (launchArgs.backend) { |
| dlvArgs.push('--backend=' + launchArgs.backend); |
| } |
| } |
| |
| log(`Current working directory: ${dlvCwd}`); |
| log(`Running: ${launchArgs.dlvToolPath} ${dlvArgs.join(' ')}`); |
| |
| this.debugProcess = spawn(launchArgs.dlvToolPath, dlvArgs, { |
| cwd: dlvCwd, |
| env |
| }); |
| |
| function connectClient(port: number, host: string) { |
| // Add a slight delay to avoid issues on Linux with |
| // Delve failing calls made shortly after connection. |
| setTimeout(() => { |
| const client = Client.$create(port, host); |
| client.connectSocket((err, conn) => { |
| if (err) { |
| return reject(err); |
| } |
| return resolve(conn); |
| }); |
| client.on('error', reject); |
| }, 200); |
| } |
| |
| this.debugProcess.stderr.on('data', (chunk) => { |
| const str = chunk.toString(); |
| if (this.onstderr) { |
| this.onstderr(str); |
| } |
| }); |
| this.debugProcess.stdout.on('data', (chunk) => { |
| const str = chunk.toString(); |
| if (this.onstdout) { |
| this.onstdout(str); |
| } |
| if (!serverRunning) { |
| serverRunning = true; |
| connectClient(launchArgs.port, launchArgs.host); |
| } |
| }); |
| this.debugProcess.on('close', (code) => { |
| // TODO: Report `dlv` crash to user. |
| logError('Process exiting with code: ' + code); |
| if (this.onclose) { |
| this.onclose(code); |
| } |
| }); |
| this.debugProcess.on('error', (err) => { |
| reject(err); |
| }); |
| }); |
| } |
| |
| public call<T>(command: string, args: any[], callback: (err: Error, results: T) => void) { |
| this.connection.then( |
| (conn) => { |
| conn.call('RPCServer.' + command, args, callback); |
| }, |
| (err) => { |
| callback(err, null); |
| } |
| ); |
| } |
| |
| public callPromise<T>(command: string, args: any[]): Thenable<T> { |
| return new Promise<T>((resolve, reject) => { |
| this.connection.then( |
| (conn) => { |
| conn.call<T>(`RPCServer.${command}`, args, (err, res) => { |
| return err ? reject(err) : resolve(res); |
| }); |
| }, |
| (err) => { |
| reject(err); |
| } |
| ); |
| }); |
| } |
| |
| /** |
| * Returns the current state of the delve debugger. |
| * This method does not block delve and should return immediately. |
| */ |
| public async getDebugState(): Promise<DebuggerState> { |
| // If a program is launched with --continue, the program is running |
| // before we can run attach. So we would need to check the state. |
| // We use NonBlocking so the call would return immediately. |
| const callResult = await this.callPromise<DebuggerState | CommandOut>('State', [{ NonBlocking: true }]); |
| return this.isApiV1 ? <DebuggerState>callResult : (<CommandOut>callResult).State; |
| } |
| |
| /** |
| * Closing a debugging session follows different approaches for launch vs attach debugging. |
| * |
| * For launch without debugging, we kill the process since the extension started the `go run` process. |
| * |
| * For launch debugging, since the extension starts the delve process, the extension should close it as well. |
| * To gracefully clean up the assets created by delve, we send the Detach request with kill option set to true. |
| * |
| * For attach debugging there are two scenarios; attaching to a local process by ID or connecting to a |
| * remote delve server. For attach-local we start the delve process so will also terminate it however we |
| * detach from the debugee without killing it. For attach-remote we only detach from delve. |
| * |
| * The only way to detach from delve when it is running a program is to send a Halt request first. |
| * Since the Halt request might sometimes take too long to complete, we have a timer in place to forcefully kill |
| * the debug process and clean up the assets in case of local debugging |
| */ |
| public async close(): Promise<void> { |
| const forceCleanup = async () => { |
| log(`killing debugee (pid: ${this.debugProcess.pid})...`); |
| await killProcessTree(this.debugProcess); |
| await removeFile(this.localDebugeePath); |
| }; |
| |
| if (this.noDebug) { |
| // delve isn't running so no need to halt |
| await forceCleanup(); |
| return Promise.resolve(); |
| } |
| log('HaltRequest'); |
| const isLocalDebugging: boolean = this.request === 'launch' && !!this.debugProcess; |
| |
| return new Promise(async (resolve) => { |
| // For remote debugging, closing the connection would terminate the |
| // program as well so we just want to disconnect. |
| // See https://www.github.com/go-delve/delve/issues/1587 |
| if (this.isRemoteDebugging) { |
| const rpcConnection = await this.connection; |
| // tslint:disable-next-line no-any |
| (rpcConnection as any)['conn']['end'](); |
| return; |
| } |
| const timeoutToken: NodeJS.Timer = |
| isLocalDebugging && |
| setTimeout(async () => { |
| log('Killing debug process manually as we could not halt delve in time'); |
| await forceCleanup(); |
| resolve(); |
| }, 1000); |
| |
| let haltErrMsg: string; |
| try { |
| await this.callPromise('Command', [{ name: 'halt' }]); |
| } catch (err) { |
| log('HaltResponse'); |
| haltErrMsg = err ? err.toString() : ''; |
| log(`Failed to halt - ${haltErrMsg}`); |
| } |
| clearTimeout(timeoutToken); |
| |
| const targetHasExited: boolean = haltErrMsg && haltErrMsg.endsWith('has exited with status 0'); |
| const shouldDetach: boolean = !haltErrMsg || targetHasExited; |
| let shouldForceClean: boolean = !shouldDetach && isLocalDebugging; |
| if (shouldDetach) { |
| log('DetachRequest'); |
| try { |
| await this.callPromise('Detach', [this.isApiV1 ? true : { Kill: isLocalDebugging }]); |
| } catch (err) { |
| log('DetachResponse'); |
| logError(`Failed to detach - ${err.toString() || ''}`); |
| shouldForceClean = isLocalDebugging; |
| } |
| } |
| if (shouldForceClean) { |
| await forceCleanup(); |
| } |
| return resolve(); |
| }); |
| } |
| |
| private getLocalDebugeePath(output: string | undefined): string { |
| const configOutput = output || 'debug'; |
| return path.isAbsolute(configOutput) ? configOutput : path.resolve(this.program, configOutput); |
| } |
| } |
| |
| export class GoDebugSession extends LoggingDebugSession { |
| private variableHandles: Handles<DebugVariable>; |
| private breakpoints: Map<string, DebugBreakpoint[]>; |
| // Editing breakpoints requires halting delve, skip sending Stop Event to VS Code in such cases |
| private skipStopEventOnce: boolean; |
| private debugState: DebuggerState; |
| private delve: Delve; |
| private localPathSeparator: string; |
| private remotePathSeparator: string; |
| private stackFrameHandles: Handles<[number, number]>; |
| private packageInfo = new Map<string, string>(); |
| private stopOnEntry: boolean; |
| private logLevel: Logger.LogLevel = Logger.LogLevel.Error; |
| private readonly initdone = 'initdone·'; |
| private remoteSourcesAndPackages = new RemoteSourcesAndPackages(); |
| private localToRemotePathMapping = new Map<string, string>(); |
| private remoteToLocalPathMapping = new Map<string, string>(); |
| |
| private showGlobalVariables: boolean = false; |
| |
| private continueEpoch = 0; |
| private continueRequestRunning = false; |
| public constructor( |
| debuggerLinesStartAt1: boolean, |
| isServer: boolean = false, |
| readonly fileSystem = fs) { |
| super('', debuggerLinesStartAt1, isServer); |
| this.variableHandles = new Handles<DebugVariable>(); |
| this.skipStopEventOnce = false; |
| this.stopOnEntry = false; |
| this.debugState = null; |
| this.delve = null; |
| this.breakpoints = new Map<string, DebugBreakpoint[]>(); |
| this.stackFrameHandles = new Handles<[number, number]>(); |
| } |
| |
| protected initializeRequest( |
| response: DebugProtocol.InitializeResponse, |
| args: DebugProtocol.InitializeRequestArguments |
| ): void { |
| log('InitializeRequest'); |
| // This debug adapter implements the configurationDoneRequest. |
| response.body.supportsConfigurationDoneRequest = true; |
| response.body.supportsSetVariable = true; |
| this.sendResponse(response); |
| log('InitializeResponse'); |
| } |
| |
| protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { |
| log('LaunchRequest'); |
| if (!args.program) { |
| this.sendErrorResponse( |
| response, |
| 3000, |
| 'Failed to continue: The program attribute is missing in the debug configuration in launch.json' |
| ); |
| return; |
| } |
| this.initLaunchAttachRequest(response, args); |
| } |
| |
| protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): void { |
| log('AttachRequest'); |
| if (args.mode === 'local' && !args.processId) { |
| this.sendErrorResponse( |
| response, |
| 3000, |
| 'Failed to continue: the processId attribute is missing in the debug configuration in launch.json' |
| ); |
| } else if (args.mode === 'remote' && !args.port) { |
| this.sendErrorResponse( |
| response, |
| 3000, |
| 'Failed to continue: the port attribute is missing in the debug configuration in launch.json' |
| ); |
| } |
| this.initLaunchAttachRequest(response, args); |
| } |
| |
| protected async disconnectRequest( |
| response: DebugProtocol.DisconnectResponse, |
| args: DebugProtocol.DisconnectArguments |
| ): Promise<void> { |
| log('DisconnectRequest'); |
| // For remote process, we have to issue a continue request |
| // before disconnecting. |
| if (this.delve.isRemoteDebugging) { |
| // We don't have to wait for continue call |
| // because we are not doing anything with the result. |
| // Also, DisconnectRequest will return before |
| // we get the result back from delve. |
| this.debugState = await this.delve.getDebugState(); |
| if (!this.debugState.Running) { |
| this.continue(); |
| } |
| } |
| this.delve.close().then(() => { |
| log('DisconnectRequest to parent'); |
| super.disconnectRequest(response, args); |
| log('DisconnectResponse'); |
| }); |
| } |
| |
| protected async configurationDoneRequest( |
| response: DebugProtocol.ConfigurationDoneResponse, |
| args: DebugProtocol.ConfigurationDoneArguments |
| ): Promise<void> { |
| log('ConfigurationDoneRequest'); |
| if (this.stopOnEntry) { |
| this.sendEvent(new StoppedEvent('entry', 1)); |
| log('StoppedEvent("entry")'); |
| this.sendResponse(response); |
| } else { |
| this.debugState = await this.delve.getDebugState(); |
| if (!this.debugState.Running) { |
| this.continueRequest(<DebugProtocol.ContinueResponse>response); |
| } |
| } |
| } |
| |
| /** |
| * Given a potential list of paths in potentialPaths array, we will |
| * find the path that has the longest suffix matching filePath. |
| * For example, if filePath is /usr/local/foo/bar/main.go |
| * and potentialPaths are abc/xyz/main.go, bar/main.go |
| * then bar/main.go will be the result. |
| * NOTE: This function assumes that potentialPaths array only contains |
| * files with the same base names as filePath. |
| */ |
| protected findPathWithBestMatchingSuffix(filePath: string, potentialPaths: string[]): string | undefined { |
| if (!potentialPaths.length) { |
| return; |
| } |
| |
| if (potentialPaths.length === 1) { |
| return potentialPaths[0]; |
| } |
| |
| const filePathSegments = filePath.split(/\/|\\/).reverse(); |
| let bestPathSoFar = potentialPaths[0]; |
| let bestSegmentsCount = 0; |
| for (const potentialPath of potentialPaths) { |
| const potentialPathSegments = potentialPath.split(/\/|\\/).reverse(); |
| let i = 0; |
| for (; i < filePathSegments.length |
| && i < potentialPathSegments.length |
| && filePathSegments[i] === potentialPathSegments[i]; i++) { |
| if (i > bestSegmentsCount) { |
| bestSegmentsCount = i; |
| bestPathSoFar = potentialPath; |
| } |
| } |
| } |
| return bestPathSoFar; |
| } |
| |
| /** |
| * Given a local path, try to find matching file in the remote machine |
| * using remote sources and remote packages info that we get from Delve. |
| * The result would be cached in localToRemotePathMapping. |
| */ |
| protected inferRemotePathFromLocalPath(localPath: string): string | undefined { |
| if (this.localToRemotePathMapping.has(localPath)) { |
| return this.localToRemotePathMapping.get(localPath); |
| } |
| |
| const fileName = getBaseName(localPath); |
| const potentialMatchingRemoteFiles = this.remoteSourcesAndPackages.remoteSourceFilesNameGrouping.get(fileName); |
| const bestMatchingRemoteFile = this.findPathWithBestMatchingSuffix(localPath, potentialMatchingRemoteFiles); |
| if (!bestMatchingRemoteFile) { |
| return; |
| } |
| |
| this.localToRemotePathMapping.set(localPath, bestMatchingRemoteFile); |
| return bestMatchingRemoteFile; |
| } |
| |
| protected async toDebuggerPath(filePath: string): Promise<string> { |
| if (this.delve.remotePath.length === 0) { |
| if (this.delve.isRemoteDebugging) { |
| // The user trusts us to infer the remote path mapping! |
| await this.initializeRemotePackagesAndSources(); |
| const matchedRemoteFile = this.inferRemotePathFromLocalPath(filePath); |
| if (matchedRemoteFile) { |
| return matchedRemoteFile; |
| } |
| } |
| return this.convertClientPathToDebugger(filePath); |
| } |
| |
| // The filePath may have a different path separator than the localPath |
| // So, update it to use the same separator as the remote path to ease |
| // in replacing the local path in it with remote path |
| filePath = filePath.replace(/\/|\\/g, this.remotePathSeparator); |
| return filePath.replace(this.delve.program.replace(/\/|\\/g, this.remotePathSeparator), this.delve.remotePath); |
| } |
| |
| /** |
| * Given a remote path, try to infer the matching local path. |
| * We attempt to find the path in local Go packages as well as workspaceFolder. |
| * Cache the result in remoteToLocalPathMapping. |
| */ |
| protected inferLocalPathFromRemotePath(remotePath: string): string | undefined { |
| if (this.remoteToLocalPathMapping.has(remotePath)) { |
| return this.remoteToLocalPathMapping.get(remotePath); |
| } |
| |
| const convertedLocalPackageFile = this.inferLocalPathFromRemoteGoPackage(remotePath); |
| if (convertedLocalPackageFile) { |
| this.remoteToLocalPathMapping.set(remotePath, convertedLocalPackageFile); |
| return convertedLocalPackageFile; |
| } |
| |
| // If we cannot find the path in packages, most likely it will be in the current directory. |
| const fileName = getBaseName(remotePath); |
| const globSync = glob.sync(fileName, { |
| matchBase: true, |
| cwd: this.delve.program |
| }); |
| const bestMatchingLocalPath = this.findPathWithBestMatchingSuffix(remotePath, globSync); |
| if (bestMatchingLocalPath) { |
| const fullLocalPath = path.join(this.delve.program, bestMatchingLocalPath); |
| this.remoteToLocalPathMapping.set(remotePath, fullLocalPath); |
| return fullLocalPath; |
| } |
| } |
| |
| /** |
| * Given a remote path, we attempt to infer the local path by first checking |
| * if it is in any remote packages. If so, then we attempt to find the matching |
| * local package and find the local path from there. |
| */ |
| protected inferLocalPathFromRemoteGoPackage(remotePath: string): string | undefined { |
| const remotePackage = this.remoteSourcesAndPackages.remotePackagesBuildInfo.find( |
| (buildInfo) => remotePath.startsWith(buildInfo.DirectoryPath)); |
| // Since we know pathToConvert exists in a remote package, we can try to find |
| // that same package in the local client. We can use import path to search for the package. |
| if (!remotePackage) { |
| return; |
| } |
| |
| if (!this.remotePathSeparator) { |
| this.remotePathSeparator = findPathSeparator(remotePackage.DirectoryPath); |
| } |
| |
| // Escaping package path. |
| // It seems like sometimes Delve don't escape the path properly |
| // so we should do it. |
| remotePath = escapeGoModPath(remotePath); |
| const escapedImportPath = escapeGoModPath(remotePackage.ImportPath); |
| |
| // The remotePackage.DirectoryPath should be something like |
| // <gopath|goroot|source>/<import-path>/xyz... |
| // Directory Path can be like "/go/pkg/mod/github.com/google/go-cmp@v0.4.0/cmp" |
| // and Import Path can be like "github.com/google/go-cmp/cmp" |
| // and Remote Path "/go/pkg/mod/github.com/google/go-cmp@v0.4.0/cmp/blah.go" |
| const importPathIndex = remotePath.replace(/@v\d+\.\d+\.\d+[^\/]*/, '') |
| .indexOf(escapedImportPath); |
| if (importPathIndex < 0) { |
| return; |
| } |
| |
| const relativeRemotePath = remotePath |
| .substr(importPathIndex) |
| .split(this.remotePathSeparator) |
| .join(this.localPathSeparator); |
| const pathToConvertWithLocalSeparator = remotePath.split(this.remotePathSeparator).join(this.localPathSeparator); |
| |
| // Scenario 1: The package is inside the current working directory. |
| const localWorkspacePath = path.join(this.delve.program, relativeRemotePath); |
| if (this.fileSystem.existsSync(localWorkspacePath)) { |
| return localWorkspacePath; |
| } |
| |
| // Scenario 2: The package is inside GOPATH. |
| const localGoPathImportPath = this.inferLocalPathInGoPathFromRemoteGoPackage( |
| pathToConvertWithLocalSeparator, relativeRemotePath); |
| if (localGoPathImportPath) { |
| return localGoPathImportPath; |
| } |
| |
| // Scenario 3: The package is inside GOROOT. |
| return this.inferLocalPathInGoRootFromRemoteGoPackage(pathToConvertWithLocalSeparator, relativeRemotePath); |
| } |
| |
| /** |
| * Given a remotePath, check whether the file path exists in $GOROOT/src. |
| * Return the path if it exists. |
| * We are assuming that remotePath is of the form <prefix>/src/<suffix>. |
| */ |
| protected inferLocalPathInGoRootFromRemoteGoPackage( |
| remotePathWithLocalSeparator: string, relativeRemotePath: string): string | undefined { |
| const srcIndex = remotePathWithLocalSeparator.indexOf(`${this.localPathSeparator}src${this.localPathSeparator}`); |
| const goroot = this.getGOROOT(); |
| const localGoRootImportPath = path.join( |
| goroot, |
| srcIndex >= 0 |
| ? remotePathWithLocalSeparator.substr(srcIndex) |
| : path.join('src', relativeRemotePath)); |
| if (this.fileSystem.existsSync(localGoRootImportPath)) { |
| return localGoRootImportPath; |
| } |
| } |
| |
| /** |
| * Given a remotePath, check whether the file path exists in $GOPATH. |
| * This can be either in $GOPATH/pkg/mod or $GOPATH/src. If so, return that path. |
| * remotePath can be something like /usr/local/gopath/src/hello-world/main.go |
| * and relativeRemotePath should be hello-world/main.go. In other words, |
| * relativeRemotePath is a relative version of remotePath starting |
| * from the import path of the module. |
| */ |
| protected inferLocalPathInGoPathFromRemoteGoPackage( |
| remotePathWithLocalSeparator: string, relativeRemotePath: string): string | undefined { |
| // Scenario 1: The package is inside $GOPATH/pkg/mod. |
| const gopath = (process.env['GOPATH'] || '').split(path.delimiter)[0]; |
| |
| const indexGoModCache = remotePathWithLocalSeparator.indexOf( |
| `${this.localPathSeparator}pkg${this.localPathSeparator}mod${this.localPathSeparator}` |
| ); |
| const localGoPathImportPath = path.join( |
| gopath, |
| indexGoModCache >= 0 |
| ? remotePathWithLocalSeparator.substr(indexGoModCache) |
| : path.join('pkg', 'mod', relativeRemotePath)); |
| if (this.fileSystem.existsSync(localGoPathImportPath)) { |
| return localGoPathImportPath; |
| } |
| |
| // Scenario 2: The file is in a package in $GOPATH/src. |
| const localGoPathSrcPath = path.join( |
| gopath, 'src', |
| relativeRemotePath.split(this.remotePathSeparator).join(this.localPathSeparator)); |
| if (this.fileSystem.existsSync(localGoPathSrcPath)) { |
| return localGoPathSrcPath; |
| } |
| } |
| |
| /** |
| * This functions assumes that remote packages and paths information |
| * have been initialized. |
| */ |
| protected toLocalPath(pathToConvert: string): string { |
| if (this.delve.remotePath.length === 0) { |
| // User trusts use to infer the path |
| if (this.delve.isRemoteDebugging) { |
| const inferredPath = this.inferLocalPathFromRemotePath(pathToConvert); |
| if (inferredPath) { |
| return inferredPath; |
| } |
| } |
| return this.convertDebuggerPathToClient(pathToConvert); |
| } |
| |
| // When the pathToConvert is under GOROOT or Go module cache, replace path appropriately |
| if (!pathToConvert.startsWith(this.delve.remotePath)) { |
| // Fix for https://github.com/Microsoft/vscode-go/issues/1178 |
| const index = pathToConvert.indexOf(`${this.remotePathSeparator}src${this.remotePathSeparator}`); |
| const goroot = this.getGOROOT(); |
| if (goroot && index > 0) { |
| return path.join(goroot, pathToConvert.substr(index)); |
| } |
| |
| const indexGoModCache = pathToConvert.indexOf( |
| `${this.remotePathSeparator}pkg${this.remotePathSeparator}mod${this.remotePathSeparator}` |
| ); |
| const gopath = (process.env['GOPATH'] || '').split(path.delimiter)[0]; |
| |
| if (gopath && indexGoModCache > 0) { |
| return path.join( |
| gopath, |
| pathToConvert |
| .substr(indexGoModCache) |
| .split(this.remotePathSeparator) |
| .join(this.localPathSeparator) |
| ); |
| } |
| } |
| return pathToConvert |
| .replace(this.delve.remotePath, this.delve.program) |
| .split(this.remotePathSeparator) |
| .join(this.localPathSeparator); |
| } |
| |
| protected async setBreakPointsRequest( |
| response: DebugProtocol.SetBreakpointsResponse, |
| args: DebugProtocol.SetBreakpointsArguments |
| ): Promise<void> { |
| log('SetBreakPointsRequest'); |
| try { |
| // If a program is launched with --continue, the program is running |
| // before we can run attach. So we would need to check the state. |
| // We use NonBlocking so the call would return immediately. |
| this.debugState = await this.delve.getDebugState(); |
| } catch (error) { |
| this.logDelveError(error, 'Failed to get state'); |
| } |
| |
| if (!this.debugState.Running && !this.continueRequestRunning) { |
| await this.setBreakPoints(response, args); |
| } else { |
| this.skipStopEventOnce = this.continueRequestRunning; |
| this.delve.callPromise('Command', [{ name: 'halt' }]).then( |
| () => { |
| return this.setBreakPoints(response, args).then(() => { |
| return this.continue(true).then(null, (err) => { |
| this.logDelveError(err, 'Failed to continue delve after halting it to set breakpoints'); |
| }); |
| }); |
| }, |
| (err) => { |
| this.skipStopEventOnce = false; |
| this.logDelveError(err, 'Failed to halt delve before attempting to set breakpoint'); |
| return this.sendErrorResponse( |
| response, |
| 2008, |
| 'Failed to halt delve before attempting to set breakpoint: "{e}"', |
| { e: err.toString() } |
| ); |
| } |
| ); |
| } |
| } |
| |
| protected threadsRequest(response: DebugProtocol.ThreadsResponse): void { |
| if (this.continueRequestRunning) { |
| // Thread request to delve is syncronous and will block if a previous async continue request didn't return |
| response.body = { threads: [new Thread(1, 'Dummy')] }; |
| return this.sendResponse(response); |
| } |
| log('ThreadsRequest'); |
| this.delve.call<DebugGoroutine[] | ListGoroutinesOut>('ListGoroutines', [], (err, out) => { |
| if (this.debugState && this.debugState.exited) { |
| // If the program exits very quickly, the initial threadsRequest will complete after it has exited. |
| // A TerminatedEvent has already been sent. Ignore the err returned in this case. |
| response.body = { threads: [] }; |
| return this.sendResponse(response); |
| } |
| |
| if (err) { |
| this.logDelveError(err, 'Failed to get threads'); |
| return this.sendErrorResponse(response, 2003, 'Unable to display threads: "{e}"', { |
| e: err.toString() |
| }); |
| } |
| const goroutines = this.delve.isApiV1 ? <DebugGoroutine[]>out : (<ListGoroutinesOut>out).Goroutines; |
| log('goroutines', goroutines); |
| const threads = goroutines.map( |
| (goroutine) => |
| new Thread( |
| goroutine.id, |
| goroutine.userCurrentLoc.function |
| ? goroutine.userCurrentLoc.function.name |
| : goroutine.userCurrentLoc.file + '@' + goroutine.userCurrentLoc.line |
| ) |
| ); |
| if (threads.length === 0) { |
| threads.push(new Thread(1, 'Dummy')); |
| } |
| response.body = { threads }; |
| this.sendResponse(response); |
| log('ThreadsResponse', threads); |
| }); |
| } |
| |
| protected stackTraceRequest( |
| response: DebugProtocol.StackTraceResponse, |
| args: DebugProtocol.StackTraceArguments |
| ): void { |
| log('StackTraceRequest'); |
| // delve does not support frame paging, so we ask for a large depth |
| const goroutineId = args.threadId; |
| const stackTraceIn = { id: goroutineId, depth: this.delve.stackTraceDepth }; |
| if (!this.delve.isApiV1) { |
| Object.assign(stackTraceIn, { full: false, cfg: this.delve.loadConfig }); |
| } |
| this.delve.call<DebugLocation[] | StacktraceOut>( |
| this.delve.isApiV1 ? 'StacktraceGoroutine' : 'Stacktrace', |
| [stackTraceIn], |
| async (err, out) => { |
| if (err) { |
| this.logDelveError(err, 'Failed to produce stacktrace'); |
| return this.sendErrorResponse(response, 2004, 'Unable to produce stack trace: "{e}"', { |
| e: err.toString() |
| }); |
| } |
| const locations = this.delve.isApiV1 ? <DebugLocation[]>out : (<StacktraceOut>out).Locations; |
| log('locations', locations); |
| |
| if (this.delve.isRemoteDebugging) { |
| await this.initializeRemotePackagesAndSources(); |
| } |
| |
| let stackFrames = locations.map((location, frameId) => { |
| const uniqueStackFrameId = this.stackFrameHandles.create([goroutineId, frameId]); |
| return new StackFrame( |
| uniqueStackFrameId, |
| location.function ? location.function.name : '<unknown>', |
| location.file === '<autogenerated>' |
| ? null |
| : new Source(path.basename(location.file), this.toLocalPath(location.file)), |
| location.line, |
| 0 |
| ); |
| }); |
| if (args.startFrame > 0) { |
| stackFrames = stackFrames.slice(args.startFrame); |
| } |
| if (args.levels > 0) { |
| stackFrames = stackFrames.slice(0, args.levels); |
| } |
| response.body = { stackFrames, totalFrames: locations.length }; |
| this.sendResponse(response); |
| log('StackTraceResponse'); |
| } |
| ); |
| } |
| |
| protected scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void { |
| log('ScopesRequest'); |
| const [goroutineId, frameId] = this.stackFrameHandles.get(args.frameId); |
| const listLocalVarsIn = { goroutineID: goroutineId, frame: frameId }; |
| this.delve.call<DebugVariable[] | ListVarsOut>( |
| 'ListLocalVars', |
| this.delve.isApiV1 ? [listLocalVarsIn] : [{ scope: listLocalVarsIn, cfg: this.delve.loadConfig }], |
| (err, out) => { |
| if (err) { |
| this.logDelveError(err, 'Failed to get list local variables'); |
| return this.sendErrorResponse(response, 2005, 'Unable to list locals: "{e}"', { |
| e: err.toString() |
| }); |
| } |
| const locals = this.delve.isApiV1 ? <DebugVariable[]>out : (<ListVarsOut>out).Variables; |
| log('locals', locals); |
| this.addFullyQualifiedName(locals); |
| const listLocalFunctionArgsIn = { goroutineID: goroutineId, frame: frameId }; |
| this.delve.call<DebugVariable[] | ListFunctionArgsOut>( |
| 'ListFunctionArgs', |
| this.delve.isApiV1 |
| ? [listLocalFunctionArgsIn] |
| : [{ scope: listLocalFunctionArgsIn, cfg: this.delve.loadConfig }], |
| (listFunctionErr, outArgs) => { |
| if (listFunctionErr) { |
| this.logDelveError(listFunctionErr, 'Failed to list function args'); |
| return this.sendErrorResponse(response, 2006, 'Unable to list args: "{e}"', { |
| e: listFunctionErr.toString() |
| }); |
| } |
| const vars = this.delve.isApiV1 |
| ? <DebugVariable[]>outArgs |
| : (<ListFunctionArgsOut>outArgs).Args; |
| log('functionArgs', vars); |
| this.addFullyQualifiedName(vars); |
| vars.push(...locals); |
| // annotate shadowed variables in parentheses |
| const shadowedVars = new Map<string, Array<number>>(); |
| for (let i = 0; i < vars.length; ++i) { |
| if ((vars[i].flags & GoVariableFlags.VariableShadowed) === 0) { |
| continue; |
| } |
| const varName = vars[i].name; |
| if (!shadowedVars.has(varName)) { |
| const indices = new Array<number>(); |
| indices.push(i); |
| shadowedVars.set(varName, indices); |
| } else { |
| shadowedVars.get(varName).push(i); |
| } |
| } |
| for (const svIndices of shadowedVars.values()) { |
| // sort by declared line number in descending order |
| svIndices.sort((lhs: number, rhs: number) => { |
| return vars[rhs].DeclLine - vars[lhs].DeclLine; |
| }); |
| // enclose in parentheses, one pair per scope |
| for (let scope = 0; scope < svIndices.length; ++scope) { |
| const svIndex = svIndices[scope]; |
| // start at -1 so scope of 0 has one pair of parens |
| for (let count = -1; count < scope; ++count) { |
| vars[svIndex].name = `(${vars[svIndex].name})`; |
| } |
| } |
| } |
| const scopes = new Array<Scope>(); |
| const localVariables: DebugVariable = { |
| name: 'Local', |
| addr: 0, |
| type: '', |
| realType: '', |
| kind: 0, |
| flags: 0, |
| onlyAddr: false, |
| DeclLine: 0, |
| value: '', |
| len: 0, |
| cap: 0, |
| children: vars, |
| unreadable: '', |
| fullyQualifiedName: '', |
| base: 0 |
| }; |
| |
| scopes.push(new Scope('Local', this.variableHandles.create(localVariables), false)); |
| response.body = { scopes }; |
| |
| if (!this.showGlobalVariables) { |
| this.sendResponse(response); |
| log('ScopesResponse'); |
| return; |
| } |
| |
| this.getPackageInfo(this.debugState).then((packageName) => { |
| if (!packageName) { |
| this.sendResponse(response); |
| log('ScopesResponse'); |
| return; |
| } |
| const filter = `^${packageName}\\.`; |
| this.delve.call<DebugVariable[] | ListVarsOut>( |
| 'ListPackageVars', |
| this.delve.isApiV1 ? [filter] : [{ filter, cfg: this.delve.loadConfig }], |
| (listPkgVarsErr, listPkgVarsOut) => { |
| if (listPkgVarsErr) { |
| this.logDelveError(listPkgVarsErr, 'Failed to list global vars'); |
| return this.sendErrorResponse( |
| response, |
| 2007, |
| 'Unable to list global vars: "{e}"', |
| { e: listPkgVarsErr.toString() } |
| ); |
| } |
| const globals = this.delve.isApiV1 |
| ? <DebugVariable[]>listPkgVarsOut |
| : (<ListVarsOut>listPkgVarsOut).Variables; |
| let initdoneIndex = -1; |
| for (let i = 0; i < globals.length; i++) { |
| globals[i].name = globals[i].name.substr(packageName.length + 1); |
| if (initdoneIndex === -1 && globals[i].name === this.initdone) { |
| initdoneIndex = i; |
| } |
| } |
| if (initdoneIndex > -1) { |
| globals.splice(initdoneIndex, 1); |
| } |
| log('global vars', globals); |
| |
| const globalVariables: DebugVariable = { |
| name: 'Global', |
| addr: 0, |
| type: '', |
| realType: '', |
| kind: 0, |
| flags: 0, |
| onlyAddr: false, |
| DeclLine: 0, |
| value: '', |
| len: 0, |
| cap: 0, |
| children: globals, |
| unreadable: '', |
| fullyQualifiedName: '', |
| base: 0 |
| }; |
| scopes.push( |
| new Scope('Global', this.variableHandles.create(globalVariables), false) |
| ); |
| this.sendResponse(response); |
| log('ScopesResponse'); |
| } |
| ); |
| }); |
| } |
| ); |
| } |
| ); |
| } |
| |
| protected variablesRequest( |
| response: DebugProtocol.VariablesResponse, |
| args: DebugProtocol.VariablesArguments |
| ): void { |
| log('VariablesRequest'); |
| const vari = this.variableHandles.get(args.variablesReference); |
| let variablesPromise: Promise<DebugProtocol.Variable[]>; |
| const loadChildren = async (exp: string, v: DebugVariable) => { |
| // from https://github.com/go-delve/delve/blob/master/Documentation/api/ClientHowto.md#looking-into-variables |
| if ( |
| (v.kind === GoReflectKind.Struct && v.len > v.children.length) || |
| (v.kind === GoReflectKind.Interface && v.children.length > 0 && v.children[0].onlyAddr === true) |
| ) { |
| await this.evaluateRequestImpl({ expression: exp }).then( |
| (result) => { |
| const variable = this.delve.isApiV1 ? <DebugVariable>result : (<EvalOut>result).Variable; |
| v.children = variable.children; |
| }, |
| (err) => this.logDelveError(err, 'Failed to evaluate expression') |
| ); |
| } |
| }; |
| // expressions passed to loadChildren defined per |
| // https://github.com/go-delve/delve/blob/master/Documentation/api/ClientHowto.md#loading-more-of-a-variable |
| if (vari.kind === GoReflectKind.Array || vari.kind === GoReflectKind.Slice) { |
| variablesPromise = Promise.all( |
| vari.children.map((v, i) => { |
| return loadChildren(`*(*"${v.type}")(${v.addr})`, v).then( |
| (): DebugProtocol.Variable => { |
| const { result, variablesReference } = this.convertDebugVariableToProtocolVariable(v); |
| return { |
| name: '[' + i + ']', |
| value: result, |
| evaluateName: vari.fullyQualifiedName + '[' + i + ']', |
| variablesReference |
| }; |
| } |
| ); |
| }) |
| ); |
| } else if (vari.kind === GoReflectKind.Map) { |
| variablesPromise = Promise.all( |
| vari.children.map((_, i) => { |
| // even indices are map keys, odd indices are values |
| if (i % 2 === 0 && i + 1 < vari.children.length) { |
| const mapKey = this.convertDebugVariableToProtocolVariable(vari.children[i]); |
| return loadChildren( |
| `${vari.fullyQualifiedName}.${vari.name}[${mapKey.result}]`, |
| vari.children[i + 1] |
| ).then(() => { |
| const mapValue = this.convertDebugVariableToProtocolVariable(vari.children[i + 1]); |
| return { |
| name: mapKey.result, |
| value: mapValue.result, |
| evaluateName: vari.fullyQualifiedName + '[' + mapKey.result + ']', |
| variablesReference: mapValue.variablesReference |
| }; |
| }); |
| } |
| }) |
| ); |
| } else { |
| variablesPromise = Promise.all( |
| vari.children.map((v) => { |
| return loadChildren(`*(*"${v.type}")(${v.addr})`, v).then( |
| (): DebugProtocol.Variable => { |
| const { result, variablesReference } = this.convertDebugVariableToProtocolVariable(v); |
| |
| return { |
| name: v.name, |
| value: result, |
| evaluateName: v.fullyQualifiedName, |
| variablesReference |
| }; |
| } |
| ); |
| }) |
| ); |
| } |
| variablesPromise.then((variables) => { |
| response.body = { variables }; |
| this.sendResponse(response); |
| log('VariablesResponse', JSON.stringify(variables, null, ' ')); |
| }); |
| } |
| |
| protected continueRequest(response: DebugProtocol.ContinueResponse): void { |
| log('ContinueRequest'); |
| this.continue(); |
| this.sendResponse(response); |
| log('ContinueResponse'); |
| } |
| |
| protected nextRequest(response: DebugProtocol.NextResponse): void { |
| log('NextRequest'); |
| this.delve.call<DebuggerState | CommandOut>('Command', [{ name: 'next' }], (err, out) => { |
| if (err) { |
| this.logDelveError(err, 'Failed to next'); |
| } |
| const state = this.delve.isApiV1 ? <DebuggerState>out : (<CommandOut>out).State; |
| log('next state', state); |
| this.debugState = state; |
| this.handleReenterDebug('step'); |
| }); |
| this.sendResponse(response); |
| log('NextResponse'); |
| } |
| |
| protected stepInRequest(response: DebugProtocol.StepInResponse): void { |
| log('StepInRequest'); |
| this.delve.call<DebuggerState | CommandOut>('Command', [{ name: 'step' }], (err, out) => { |
| if (err) { |
| this.logDelveError(err, 'Failed to step in'); |
| } |
| const state = this.delve.isApiV1 ? <DebuggerState>out : (<CommandOut>out).State; |
| log('stop state', state); |
| this.debugState = state; |
| this.handleReenterDebug('step'); |
| }); |
| this.sendResponse(response); |
| log('StepInResponse'); |
| } |
| |
| protected stepOutRequest(response: DebugProtocol.StepOutResponse): void { |
| log('StepOutRequest'); |
| this.delve.call<DebuggerState | CommandOut>('Command', [{ name: 'stepOut' }], (err, out) => { |
| if (err) { |
| this.logDelveError(err, 'Failed to step out'); |
| } |
| const state = this.delve.isApiV1 ? <DebuggerState>out : (<CommandOut>out).State; |
| log('stepout state', state); |
| this.debugState = state; |
| this.handleReenterDebug('step'); |
| }); |
| this.sendResponse(response); |
| log('StepOutResponse'); |
| } |
| |
| protected pauseRequest(response: DebugProtocol.PauseResponse): void { |
| log('PauseRequest'); |
| this.delve.call<DebuggerState | CommandOut>('Command', [{ name: 'halt' }], (err, out) => { |
| if (err) { |
| this.logDelveError(err, 'Failed to halt'); |
| return this.sendErrorResponse(response, 2010, 'Unable to halt execution: "{e}"', { |
| e: err.toString() |
| }); |
| } |
| const state = this.delve.isApiV1 ? <DebuggerState>out : (<CommandOut>out).State; |
| log('pause state', state); |
| this.debugState = state; |
| this.handleReenterDebug('pause'); |
| }); |
| this.sendResponse(response); |
| log('PauseResponse'); |
| } |
| |
| protected evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): void { |
| log('EvaluateRequest'); |
| this.evaluateRequestImpl(args).then( |
| (out) => { |
| const variable = this.delve.isApiV1 ? <DebugVariable>out : (<EvalOut>out).Variable; |
| // #2326: Set the fully qualified name for variable mapping |
| variable.fullyQualifiedName = variable.name; |
| response.body = this.convertDebugVariableToProtocolVariable(variable); |
| this.sendResponse(response); |
| log('EvaluateResponse'); |
| }, |
| (err) => { |
| let dest: ErrorDestination; |
| // No need to repeatedly show the error pop-up when expressions |
| // are continiously reevaluated in the Watch panel, which |
| // already displays errors. |
| if (args.context === 'watch') { |
| dest = null; |
| } else { |
| dest = ErrorDestination.User; |
| } |
| this.sendErrorResponse(response, 2009, 'Unable to eval expression: "{e}"', { |
| e: err.toString() |
| }, dest); |
| } |
| ); |
| } |
| |
| protected setVariableRequest( |
| response: DebugProtocol.SetVariableResponse, |
| args: DebugProtocol.SetVariableArguments |
| ): void { |
| log('SetVariableRequest'); |
| const scope = { |
| goroutineID: this.debugState.currentGoroutine.id |
| }; |
| const setSymbolArgs = { |
| Scope: scope, |
| Symbol: args.name, |
| Value: args.value |
| }; |
| this.delve.call(this.delve.isApiV1 ? 'SetSymbol' : 'Set', [setSymbolArgs], (err) => { |
| if (err) { |
| const errMessage = `Failed to set variable: ${err.toString()}`; |
| this.logDelveError(err, 'Failed to set variable'); |
| return this.sendErrorResponse(response, 2010, errMessage); |
| } |
| response.body = { value: args.value }; |
| this.sendResponse(response); |
| log('SetVariableResponse'); |
| }); |
| } |
| |
| private getGOROOT(): string { |
| if (this.delve && this.delve.goroot) { |
| return this.delve.goroot; |
| } |
| return process.env['GOROOT'] || ''; |
| // this is a workaround to keep the tests in integration/goDebug.test.ts running. |
| // The tests synthesize a bogus Delve instance. |
| } |
| |
| // contains common code for launch and attach debugging initialization |
| private initLaunchAttachRequest( |
| response: DebugProtocol.LaunchResponse, |
| args: LaunchRequestArguments | AttachRequestArguments |
| ) { |
| this.logLevel = |
| args.trace === 'verbose' |
| ? Logger.LogLevel.Verbose |
| : args.trace === 'log' |
| ? Logger.LogLevel.Log |
| : Logger.LogLevel.Error; |
| const logPath = |
| this.logLevel !== Logger.LogLevel.Error ? path.join(os.tmpdir(), 'vscode-go-debug.txt') : undefined; |
| logger.setup(this.logLevel, logPath); |
| |
| if (typeof args.showGlobalVariables === 'boolean') { |
| this.showGlobalVariables = args.showGlobalVariables; |
| } |
| if (args.stopOnEntry) { |
| this.stopOnEntry = args.stopOnEntry; |
| } |
| if (!args.port) { |
| args.port = random(2000, 50000); |
| } |
| if (!args.host) { |
| args.host = '127.0.0.1'; |
| } |
| |
| let localPath: string; |
| if (args.request === 'attach') { |
| localPath = args.cwd; |
| } else if (args.request === 'launch') { |
| localPath = args.program; |
| } |
| if (!args.remotePath) { |
| // too much code relies on remotePath never being null |
| args.remotePath = ''; |
| } |
| |
| this.localPathSeparator = findPathSeparator(localPath); |
| if (args.remotePath.length > 0) { |
| this.remotePathSeparator = findPathSeparator(args.remotePath); |
| |
| const llist = localPath.split(/\/|\\/).reverse(); |
| const rlist = args.remotePath.split(/\/|\\/).reverse(); |
| let i = 0; |
| for (; i < llist.length; i++) { |
| if (llist[i] !== rlist[i] || llist[i] === 'src') { |
| break; |
| } |
| } |
| |
| if (i) { |
| localPath = llist.reverse().slice(0, -i).join(this.localPathSeparator) + this.localPathSeparator; |
| args.remotePath = |
| rlist.reverse().slice(0, -i).join(this.remotePathSeparator) + this.remotePathSeparator; |
| } else if ( |
| args.remotePath.length > 1 && |
| (args.remotePath.endsWith('\\') || args.remotePath.endsWith('/')) |
| ) { |
| args.remotePath = args.remotePath.substring(0, args.remotePath.length - 1); |
| } |
| } |
| |
| // Launch the Delve debugger on the program |
| this.delve = new Delve(args, localPath); |
| this.delve.onstdout = (str: string) => { |
| this.sendEvent(new OutputEvent(str, 'stdout')); |
| }; |
| this.delve.onstderr = (str: string) => { |
| this.sendEvent(new OutputEvent(str, 'stderr')); |
| }; |
| this.delve.onclose = (code) => { |
| if (code !== 0) { |
| this.sendErrorResponse(response, 3000, 'Failed to continue: Check the debug console for details.'); |
| } |
| log('Sending TerminatedEvent as delve is closed'); |
| this.sendEvent(new TerminatedEvent()); |
| }; |
| |
| this.delve.connection.then( |
| () => { |
| if (!this.delve.noDebug) { |
| this.delve.call<GetVersionOut>('GetVersion', [], (err, out) => { |
| if (err) { |
| logError(err); |
| return this.sendErrorResponse( |
| response, |
| 2001, |
| 'Failed to get remote server version: "{e}"', |
| { e: err.toString() } |
| ); |
| } |
| const clientVersion = this.delve.isApiV1 ? 1 : 2; |
| if (out.APIVersion !== clientVersion) { |
| const errorMessage = `The remote server is running on delve v${out.APIVersion} API and the client is running v${clientVersion} API. Change the version used on the client by using the property "apiVersion" in your launch.json file.`; |
| logError(errorMessage); |
| return this.sendErrorResponse(response, 3000, errorMessage); |
| } |
| }); |
| |
| this.sendEvent(new InitializedEvent()); |
| log('InitializeEvent'); |
| } |
| this.sendResponse(response); |
| }, |
| (err) => { |
| this.sendErrorResponse(response, 3000, 'Failed to continue: "{e}"', { |
| e: err.toString() |
| }); |
| log('ContinueResponse'); |
| } |
| ); |
| } |
| |
| /** |
| * Initializing remote packages and sources. |
| * We use event model to prevent race conditions. |
| */ |
| private async initializeRemotePackagesAndSources(): Promise<void> { |
| if (this.remoteSourcesAndPackages.initializedRemoteSourceFiles) { |
| return; |
| } |
| |
| if (!this.remoteSourcesAndPackages.initializingRemoteSourceFiles) { |
| try { |
| await this.remoteSourcesAndPackages.initializeRemotePackagesAndSources(this.delve); |
| } catch (error) { |
| log(`Failing to initialize remote sources: ${error}`); |
| } |
| return; |
| } |
| |
| if (this.remoteSourcesAndPackages.initializingRemoteSourceFiles) { |
| try { |
| await new Promise((resolve) => { |
| this.remoteSourcesAndPackages.on(RemoteSourcesAndPackages.INITIALIZED, () => { |
| resolve(); |
| }); |
| }); |
| } catch (error) { |
| log(`Failing to initialize remote sources: ${error}`); |
| } |
| } |
| } |
| |
| private async setBreakPoints( |
| response: DebugProtocol.SetBreakpointsResponse, |
| args: DebugProtocol.SetBreakpointsArguments |
| ): Promise<void> { |
| const file = normalizePath(args.source.path); |
| if (!this.breakpoints.get(file)) { |
| this.breakpoints.set(file, []); |
| } |
| const remoteFile = await this.toDebuggerPath(file); |
| |
| return Promise.all( |
| this.breakpoints.get(file).map((existingBP) => { |
| log('Clearing: ' + existingBP.id); |
| return this.delve.callPromise('ClearBreakpoint', [ |
| this.delve.isApiV1 ? existingBP.id : { Id: existingBP.id } |
| ]); |
| }) |
| ) |
| .then(() => { |
| log('All cleared'); |
| let existingBreakpoints: DebugBreakpoint[] | undefined; |
| return Promise.all( |
| args.breakpoints.map((breakpoint) => { |
| if (this.delve.remotePath.length === 0) { |
| log('Creating on: ' + file + ':' + breakpoint.line); |
| } else { |
| log('Creating on: ' + file + ' (' + remoteFile + ') :' + breakpoint.line); |
| } |
| const breakpointIn = <DebugBreakpoint>{}; |
| breakpointIn.file = remoteFile; |
| breakpointIn.line = breakpoint.line; |
| breakpointIn.loadArgs = this.delve.loadConfig; |
| breakpointIn.loadLocals = this.delve.loadConfig; |
| breakpointIn.cond = breakpoint.condition; |
| return this.delve |
| .callPromise('CreateBreakpoint', [ |
| this.delve.isApiV1 ? breakpointIn : { Breakpoint: breakpointIn } |
| ]) |
| .then(null, async (err) => { |
| // Delve does not seem to support error code at this time. |
| // TODO(quoct): Follow up with delve team. |
| if (err.toString().startsWith('Breakpoint exists at')) { |
| log('Encounter existing breakpoint: ' + breakpointIn); |
| // We need to call listbreakpoints to find the ID. |
| // Otherwise, we would not be able to clear the breakpoints. |
| if (!existingBreakpoints) { |
| try { |
| const listBreakpointsResponse = await this.delve.callPromise< |
| ListBreakpointsOut | DebugBreakpoint[] |
| >('ListBreakpoints', this.delve.isApiV1 ? [] : [{}]); |
| existingBreakpoints = this.delve.isApiV1 |
| ? (listBreakpointsResponse as DebugBreakpoint[]) |
| : (listBreakpointsResponse as ListBreakpointsOut).Breakpoints; |
| } catch (error) { |
| log('Error listing breakpoints: ' + error.toString()); |
| return null; |
| } |
| } |
| const matchedBreakpoint = existingBreakpoints.find( |
| (existingBreakpoint) => |
| existingBreakpoint.line === breakpointIn.line && |
| existingBreakpoint.file === breakpointIn.file |
| ); |
| if (!matchedBreakpoint) { |
| log(`Cannot match breakpoint ${breakpointIn} with existing breakpoints.`); |
| return null; |
| } |
| return this.delve.isApiV1 ? matchedBreakpoint : { Breakpoint: matchedBreakpoint }; |
| } |
| log('Error on CreateBreakpoint: ' + err.toString()); |
| return null; |
| }); |
| }) |
| ); |
| }) |
| .then((newBreakpoints) => { |
| let convertedBreakpoints: DebugBreakpoint[]; |
| if (!this.delve.isApiV1) { |
| // Unwrap breakpoints from v2 apicall |
| convertedBreakpoints = newBreakpoints.map((bp, i) => { |
| return bp ? (bp as CreateBreakpointOut).Breakpoint : null; |
| }); |
| } else { |
| convertedBreakpoints = newBreakpoints as DebugBreakpoint[]; |
| } |
| |
| log('All set:' + JSON.stringify(newBreakpoints)); |
| const breakpoints = convertedBreakpoints.map((bp, i) => { |
| if (bp) { |
| return { verified: true, line: bp.line }; |
| } else { |
| return { verified: false, line: args.lines[i] }; |
| } |
| }); |
| this.breakpoints.set( |
| file, |
| convertedBreakpoints.filter((x) => !!x) |
| ); |
| return breakpoints; |
| }) |
| .then( |
| (breakpoints) => { |
| response.body = { breakpoints }; |
| this.sendResponse(response); |
| log('SetBreakPointsResponse'); |
| }, |
| (err) => { |
| this.sendErrorResponse(response, 2002, 'Failed to set breakpoint: "{e}"', { |
| e: err.toString() |
| }); |
| logError(err); |
| } |
| ); |
| } |
| |
| private async getPackageInfo(debugState: DebuggerState): Promise<string> { |
| if (!debugState.currentThread || !debugState.currentThread.file) { |
| return Promise.resolve(null); |
| } |
| if (this.delve.isRemoteDebugging) { |
| await this.initializeRemotePackagesAndSources(); |
| } |
| const dir = path.dirname( |
| this.delve.remotePath.length || this.delve.isRemoteDebugging |
| ? this.toLocalPath(debugState.currentThread.file) |
| : debugState.currentThread.file |
| ); |
| if (this.packageInfo.has(dir)) { |
| return Promise.resolve(this.packageInfo.get(dir)); |
| } |
| return new Promise((resolve) => { |
| execFile( |
| getBinPathWithPreferredGopathGoroot('go', []), |
| ['list', '-f', '{{.Name}} {{.ImportPath}}'], |
| { cwd: dir, env: this.delve.dlvEnv }, |
| (err, stdout, stderr) => { |
| if (err || stderr || !stdout) { |
| logError(`go list failed on ${dir}: ${stderr || err}`); |
| return resolve(); |
| } |
| if (stdout.split('\n').length !== 2) { |
| logError(`Cannot determine package for ${dir}`); |
| return resolve(); |
| } |
| const spaceIndex = stdout.indexOf(' '); |
| const result = stdout.substr(0, spaceIndex) === 'main' ? 'main' : stdout.substr(spaceIndex).trim(); |
| this.packageInfo.set(dir, result); |
| resolve(result); |
| } |
| ); |
| }); |
| } |
| |
| private convertDebugVariableToProtocolVariable(v: DebugVariable): { result: string; variablesReference: number } { |
| if (v.kind === GoReflectKind.UnsafePointer) { |
| return { |
| result: `unsafe.Pointer(0x${v.children[0].addr.toString(16)})`, |
| variablesReference: 0 |
| }; |
| } else if (v.kind === GoReflectKind.Ptr) { |
| if (v.children[0].addr === 0) { |
| return { |
| result: 'nil <' + v.type + '>', |
| variablesReference: 0 |
| }; |
| } else if (v.children[0].type === 'void') { |
| return { |
| result: 'void', |
| variablesReference: 0 |
| }; |
| } else { |
| if (v.children[0].children.length > 0) { |
| // Generate correct fullyQualified names for variable expressions |
| v.children[0].fullyQualifiedName = v.fullyQualifiedName; |
| v.children[0].children.forEach((child) => { |
| child.fullyQualifiedName = v.fullyQualifiedName + '.' + child.name; |
| }); |
| } |
| return { |
| result: `<${v.type}>(0x${v.children[0].addr.toString(16)})`, |
| variablesReference: v.children.length > 0 ? this.variableHandles.create(v) : 0 |
| }; |
| } |
| } else if (v.kind === GoReflectKind.Slice) { |
| if (v.base === 0) { |
| return { |
| result: 'nil <' + v.type + '>', |
| variablesReference: 0 |
| }; |
| } |
| return { |
| result: '<' + v.type + '> (length: ' + v.len + ', cap: ' + v.cap + ')', |
| variablesReference: this.variableHandles.create(v) |
| }; |
| } else if (v.kind === GoReflectKind.Map) { |
| if (v.base === 0) { |
| return { |
| result: 'nil <' + v.type + '>', |
| variablesReference: 0 |
| }; |
| } |
| return { |
| result: '<' + v.type + '> (length: ' + v.len + ')', |
| variablesReference: this.variableHandles.create(v) |
| }; |
| } else if (v.kind === GoReflectKind.Array) { |
| return { |
| result: '<' + v.type + '>', |
| variablesReference: this.variableHandles.create(v) |
| }; |
| } else if (v.kind === GoReflectKind.String) { |
| let val = v.value; |
| const byteLength = Buffer.byteLength(val || ''); |
| if (v.value && byteLength < v.len) { |
| val += `...+${v.len - byteLength} more`; |
| } |
| return { |
| result: v.unreadable ? '<' + v.unreadable + '>' : '"' + val + '"', |
| variablesReference: 0 |
| }; |
| } else { |
| // Default case - structs |
| if (v.children.length > 0) { |
| // Generate correct fullyQualified names for variable expressions |
| v.children.forEach((child) => { |
| child.fullyQualifiedName = v.fullyQualifiedName + '.' + child.name; |
| }); |
| } |
| return { |
| result: v.value || '<' + v.type + '>', |
| variablesReference: v.children.length > 0 ? this.variableHandles.create(v) : 0 |
| }; |
| } |
| } |
| |
| private cleanupHandles(): void { |
| this.variableHandles.reset(); |
| this.stackFrameHandles.reset(); |
| } |
| |
| private handleReenterDebug(reason: string): void { |
| this.cleanupHandles(); |
| |
| if (this.debugState.exited) { |
| this.sendEvent(new TerminatedEvent()); |
| log('TerminatedEvent'); |
| } else { |
| // Delve blocks on continue and does not support events, so there is no way to |
| // refresh the list of goroutines while the program is running. And when the program is |
| // stopped, the development tool will issue a threads request and update the list of |
| // threads in the UI even without the optional thread events. Therefore, instead of |
| // analyzing all goroutines here, only retrieve the current one. |
| // TODO(polina): validate the assumption in this code that the first goroutine |
| // is the current one. So far it appears to me that this is always the main goroutine |
| // with id 1. |
| this.delve.call<DebugGoroutine[] | ListGoroutinesOut>('ListGoroutines', [{ count: 1 }], (err, out) => { |
| if (err) { |
| this.logDelveError(err, 'Failed to get threads'); |
| } |
| const goroutines = this.delve.isApiV1 ? <DebugGoroutine[]>out : (<ListGoroutinesOut>out).Goroutines; |
| if (!this.debugState.currentGoroutine && goroutines.length > 0) { |
| this.debugState.currentGoroutine = goroutines[0]; |
| } |
| |
| if (this.skipStopEventOnce) { |
| this.skipStopEventOnce = false; |
| return; |
| } |
| |
| const stoppedEvent = new StoppedEvent(reason, this.debugState.currentGoroutine.id); |
| (<any>stoppedEvent.body).allThreadsStopped = true; |
| this.sendEvent(stoppedEvent); |
| log('StoppedEvent("' + reason + '")'); |
| }); |
| } |
| } |
| private continue(calledWhenSettingBreakpoint?: boolean): Thenable<void> { |
| this.continueEpoch++; |
| const closureEpoch = this.continueEpoch; |
| this.continueRequestRunning = true; |
| |
| const callback = (out: any) => { |
| if (closureEpoch === this.continueEpoch) { |
| this.continueRequestRunning = false; |
| } |
| const state = this.delve.isApiV1 ? <DebuggerState>out : (<CommandOut>out).State; |
| log('continue state', state); |
| this.debugState = state; |
| this.handleReenterDebug('breakpoint'); |
| }; |
| |
| // If called when setting breakpoint internally, we want the error to bubble up. |
| let errorCallback = null; |
| if (!calledWhenSettingBreakpoint) { |
| errorCallback = (err: any) => { |
| if (err) { |
| this.logDelveError(err, 'Failed to continue'); |
| } |
| this.handleReenterDebug('breakpoint'); |
| throw err; |
| }; |
| } |
| |
| return this.delve.callPromise('Command', [{ name: 'continue' }]).then(callback, errorCallback); |
| } |
| |
| private evaluateRequestImpl(args: DebugProtocol.EvaluateArguments): Thenable<EvalOut | DebugVariable> { |
| // default to the topmost stack frame of the current goroutine |
| let goroutineId = -1; |
| let frameId = 0; |
| // args.frameId won't be specified when evaluating global vars |
| if (args.frameId) { |
| [goroutineId, frameId] = this.stackFrameHandles.get(args.frameId); |
| } |
| const scope = { |
| goroutineID: goroutineId, |
| frame: frameId |
| }; |
| const apiV1Args = { |
| symbol: args.expression, |
| scope |
| }; |
| const apiV2Args = { |
| Expr: args.expression, |
| Scope: scope, |
| Cfg: this.delve.loadConfig |
| }; |
| const evalSymbolArgs = this.delve.isApiV1 ? apiV1Args : apiV2Args; |
| const returnValue = this.delve |
| .callPromise<EvalOut | DebugVariable>(this.delve.isApiV1 ? 'EvalSymbol' : 'Eval', [evalSymbolArgs]) |
| .then( |
| (val) => val, |
| (err) => { |
| log( |
| 'Failed to eval expression: ', |
| JSON.stringify(evalSymbolArgs, null, ' '), |
| '\n\rEval error:', |
| err.toString() |
| ); |
| return Promise.reject(err); |
| } |
| ); |
| return returnValue; |
| } |
| |
| private addFullyQualifiedName(variables: DebugVariable[]) { |
| variables.forEach((local) => { |
| local.fullyQualifiedName = local.name; |
| local.children.forEach((child) => { |
| child.fullyQualifiedName = local.name; |
| }); |
| }); |
| } |
| |
| private logDelveError(err: any, message: string) { |
| if (err === undefined) { |
| return; |
| } |
| |
| let errorMessage = err.toString(); |
| // Use a more user friendly message for an unpropagated SIGSEGV (EXC_BAD_ACCESS) |
| // signal that delve is unable to send back to the target process to be |
| // handled as a panic. |
| // https://github.com/microsoft/vscode-go/issues/1903#issuecomment-460126884 |
| // https://github.com/go-delve/delve/issues/852 |
| // This affects macOS only although we're agnostic of the OS at this stage. |
| if (errorMessage === 'bad access') { |
| // Reuse the panic message from the Go runtime. |
| errorMessage = |
| `runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation]\nUnable to propogate EXC_BAD_ACCESS signal to target process and panic (see https://github.com/go-delve/delve/issues/852)`; |
| } |
| |
| logError(message + ' - ' + errorMessage); |
| this.dumpStacktrace(); |
| } |
| |
| private async dumpStacktrace() { |
| // Get current goroutine |
| // Debugger may be stopped at this point but we still can (and need) to obtain state and stacktrace |
| let goroutineId = 0; |
| try { |
| const stateCallResult = await this.delve.getDebugState(); |
| // In some fault scenarios there may not be a currentGoroutine available from the debugger state |
| // Use the current thread |
| if (!stateCallResult.currentGoroutine) { |
| goroutineId = stateCallResult.currentThread.goroutineID; |
| } else { |
| goroutineId = stateCallResult.currentGoroutine.id; |
| } |
| } catch (error) { |
| logError('dumpStacktrace - Failed to get debugger state ' + error); |
| } |
| |
| // Get goroutine stacktrace |
| const stackTraceIn = { id: goroutineId, depth: this.delve.stackTraceDepth }; |
| if (!this.delve.isApiV1) { |
| Object.assign(stackTraceIn, { full: false, cfg: this.delve.loadConfig }); |
| } |
| this.delve.call<DebugLocation[] | StacktraceOut>( |
| this.delve.isApiV1 ? 'StacktraceGoroutine' : 'Stacktrace', |
| [stackTraceIn], |
| (err, out) => { |
| if (err) { |
| logError('dumpStacktrace: Failed to produce stack trace' + err); |
| return; |
| } |
| const locations = this.delve.isApiV1 ? <DebugLocation[]>out : (<StacktraceOut>out).Locations; |
| log('locations', locations); |
| const stackFrames = locations.map((location, frameId) => { |
| const uniqueStackFrameId = this.stackFrameHandles.create([goroutineId, frameId]); |
| return new StackFrame( |
| uniqueStackFrameId, |
| location.function ? location.function.name : '<unknown>', |
| location.file === '<autogenerated>' |
| ? null |
| : new Source(path.basename(location.file), this.toLocalPath(location.file)), |
| location.line, |
| 0 |
| ); |
| }); |
| |
| // Dump stacktrace into error logger |
| logError(`Last known immediate stacktrace (goroutine id ${goroutineId}):`); |
| let output = ''; |
| stackFrames.forEach((stackFrame) => { |
| output = output.concat(`\t${stackFrame.source.path}:${stackFrame.line}\n`); |
| if (stackFrame.name) { |
| output = output.concat(`\t\t${stackFrame.name}\n`); |
| } |
| }); |
| logError(output); |
| } |
| ); |
| } |
| } |
| |
| // Class for fetching remote sources and packages |
| // in the remote program using Delve. |
| // tslint:disable-next-line:max-classes-per-file |
| export class RemoteSourcesAndPackages extends EventEmitter { |
| public static readonly INITIALIZED = 'INITIALIZED'; |
| |
| public initializingRemoteSourceFiles = false; |
| public initializedRemoteSourceFiles = false; |
| |
| public remotePackagesBuildInfo: PackageBuildInfo[] = []; |
| public remoteSourceFiles: string[] = []; |
| public remoteSourceFilesNameGrouping = new Map<string, string[]>(); |
| |
| /** |
| * Initialize and fill out remote packages build info and remote source files. |
| * Emits the INITIALIZED event once initialization is complete. |
| */ |
| public async initializeRemotePackagesAndSources(delve: Delve): Promise<void> { |
| this.initializingRemoteSourceFiles = true; |
| |
| try { |
| // ListPackagesBuildInfo is not available on V1. |
| if (!delve.isApiV1 && this.remotePackagesBuildInfo.length === 0) { |
| const packagesBuildInfoResponse: ListPackagesBuildInfoOut = await delve.callPromise( |
| 'ListPackagesBuildInfo', [{ IncludeFiles: true }] |
| ); |
| if (packagesBuildInfoResponse && packagesBuildInfoResponse.List) { |
| this.remotePackagesBuildInfo = packagesBuildInfoResponse.List; |
| } |
| } |
| |
| // List sources will return all the source files used by Delve. |
| if (delve.isApiV1) { |
| this.remoteSourceFiles = await delve.callPromise('ListSources', []); |
| } else { |
| const listSourcesResponse: ListSourcesOut = await delve.callPromise('ListSources', [{}]); |
| if (listSourcesResponse && listSourcesResponse.Sources) { |
| this.remoteSourceFiles = listSourcesResponse.Sources; |
| } |
| } |
| |
| // Group the source files by name for easy searching later. |
| this.remoteSourceFiles = this.remoteSourceFiles.filter((sourceFile) => !sourceFile.startsWith('<')); |
| this.remoteSourceFiles.forEach((sourceFile) => { |
| const fileName = getBaseName(sourceFile); |
| if (!this.remoteSourceFilesNameGrouping.has(fileName)) { |
| this.remoteSourceFilesNameGrouping.set(fileName, []); |
| } |
| this.remoteSourceFilesNameGrouping.get(fileName).push(sourceFile); |
| }); |
| } catch (error) { |
| logError(`Failed to initialize remote sources and packages: ${error && error.message}`); |
| } finally { |
| this.emit(RemoteSourcesAndPackages.INITIALIZED); |
| this.initializedRemoteSourceFiles = true; |
| } |
| } |
| } |
| |
| function random(low: number, high: number): number { |
| return Math.floor(Math.random() * (high - low) + low); |
| } |
| |
| async function removeFile(filePath: string): Promise<void> { |
| try { |
| const fileExists = await fsAccess(filePath) |
| .then(() => true) |
| .catch(() => false); |
| if (filePath && fileExists) { |
| await fsUnlink(filePath); |
| } |
| } catch (e) { |
| logError(`Potentially failed remove file: ${filePath} - ${e.toString() || ''}`); |
| } |
| } |
| |
| function killProcessTree(p: ChildProcess): Promise<void> { |
| if (!p || !p.pid) { |
| log(`no process to kill`); |
| return Promise.resolve(); |
| } |
| return new Promise((resolve) => { |
| kill(p.pid, (err) => { |
| if (err) { |
| logError(`Error killing process ${p.pid}: ${err}`); |
| } else { |
| log(`killed process ${p.pid}`); |
| } |
| resolve(); |
| }); |
| }); |
| } |
| |
| // queryGOROOT returns `go env GOROOT`. |
| function queryGOROOT(cwd: any, env: any): Promise<string> { |
| return new Promise<string>((resolve) => { |
| execFile( |
| getBinPathWithPreferredGopathGoroot('go', []), |
| ['env', 'GOROOT'], |
| { cwd, env }, |
| (err, stdout, stderr) => { |
| if (err) { |
| return resolve(''); |
| } |
| return resolve(stdout.trim()); |
| }); |
| }); |
| } |
| |
| DebugSession.run(GoDebugSession); |