src/debugAdapter/goDebug.ts (2,319 lines of code) (raw):

/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable no-case-declarations */ /* eslint-disable eqeqeq */ /* eslint-disable no-useless-escape */ /* eslint-disable no-async-promise-executor */ /* eslint-disable @typescript-eslint/no-explicit-any */ /*--------------------------------------------------------- * 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, spawnSync } 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 * as util from 'util'; import { ContinuedEvent, DebugSession, ErrorDestination, Handles, InitializedEvent, logger, Logger, LoggingDebugSession, OutputEvent, Scope, Source, StackFrame, StoppedEvent, TerminatedEvent, Thread } from 'vscode-debugadapter'; import { DebugProtocol } from 'vscode-debugprotocol'; import { parseEnvFiles } from '../utils/envUtils'; import { correctBinname, getEnvPath, expandFilePathInOutput, fixDriveCasingInWindows, getBinPathWithPreferredGopathGoroot, getCurrentGoWorkspaceFromGOPATH, getInferredGopath } from '../utils/pathUtils'; import { killProcessTree } from '../utils/processUtils'; 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; currentThread: DebugThread; currentGoroutine: DebugGoroutine; Running: boolean; Threads: DebugThread[]; NextInProgress: 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; breakPoint: DebugBreakpoint; breakPointInfo: {}; function?: DebugFunction; ReturnValues: DebugVariable[]; } 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, VariableFakeAddress = 32 } interface DebugVariable { // DebugVariable corresponds to api.Variable in Delve API. // https://github.com/go-delve/delve/blob/328cf87808822693dc611591519689dcd42696a3/service/api/types.go#L239-L284 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; } // Unrecovered panic and fatal throw breakpoint IDs taken from delve: // https://github.com/go-delve/delve/blob/f90134eb4db1c423e24fddfbc6eff41b288e6297/pkg/proc/breakpoints.go#L11-L21 // UnrecoveredPanic is the name given to the unrecovered panic breakpoint. const unrecoveredPanicID = -1; // FatalThrow is the name given to the breakpoint triggered when the target // process dies because of a fatal runtime error. const fatalThrowID = -2; // 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; dlvFlags?: string[]; 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, info, warn are to match goLogging. // In practice, this adapter handles only verbose, log, and error // verbose === trace, // log === info === warn, // error trace?: 'verbose' | 'trace' | 'info' | 'log' | 'warn' | 'error'; backend?: string; output?: string; substitutePath?: { from: string; to: 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 }; /** Optional path to .env file. */ // TODO: deprecate .env file processing from DA. // We expect the extension processes .env files // and send the information to DA using the 'env' property. envFile?: string | string[]; } interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments { request: 'attach'; processId?: number; stopOnEntry?: boolean; dlvFlags?: string[]; showLog?: boolean; logOutput?: string; cwd?: string; mode?: 'local' | 'remote'; remotePath?: string; port?: number; host?: string; trace?: 'verbose' | 'trace' | 'info' | 'log' | 'warn' | 'error'; backend?: string; substitutePath?: { from: string; to: 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)); } export function findPathSeparator(filePath: string | undefined) { return filePath && filePath.includes('\\') ? '\\' : '/'; } // Comparing two different file paths while ignoring any different path separators. function compareFilePathIgnoreSeparator(firstFilePath: string, secondFilePath: string): boolean { const firstSeparator = findPathSeparator(firstFilePath); const secondSeparator = findPathSeparator(secondFilePath); if (firstSeparator === secondSeparator) { return firstFilePath === secondFilePath; } return firstFilePath === secondFilePath.split(secondSeparator).join(firstSeparator); } 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; } // normalizeSeparators will prepare the filepath for comparison in mapping from // local to debugger path and from debugger path to local path. All separators are // replaced with '/', and the drive name is capitalized for windows paths. // Exported for testing export function normalizeSeparators(filePath: string): string { // Although the current machine may not be running windows, // the remote machine may be and we need to fix the drive // casing. // This is a workaround for issue in https://github.com/Microsoft/vscode/issues/9448#issuecomment-244804026 if (filePath.indexOf(':') === 1) { filePath = filePath.substr(0, 1).toUpperCase() + filePath.substr(1); } return filePath.replace(/\/|\\/g, '/'); } 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 | null>; // null if connection isn't necessary (e.g. noDebug mode) 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; public delveConnectionClosed = false; 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 = undefined; this.isRemoteDebugging = true; this.goroot = await queryGOROOT(dlvCwd, process.env); serverRunning = true; // assume server is running when in remote mode if (!launchArgs.port || !launchArgs.host) { return reject('Unable to connect, missing host or port from launchArgs.'); } connectClient(launchArgs.port, launchArgs.host, this.onclose); return; } this.isRemoteDebugging = false; let env: NodeJS.ProcessEnv | undefined; 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 try { const fileEnvs = parseEnvFiles(launchArgs.envFile); const launchArgsEnv = launchArgs.env || {}; env = Object.assign({}, process.env, fileEnvs, launchArgsEnv); } catch (e) { return reject(`failed to process 'envFile' and 'env' settings: ${e}`); } 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 build = ['build']; const output = path.join(os.tmpdir(), correctBinname('out')); build.push(`-o=${output}`); const buildOptions: { [key: string]: any } = { cwd: dirname, env }; if (launchArgs.buildFlags) { build.push(launchArgs.buildFlags); } if (isProgramDirectory) { build.push('.'); } else { build.push(program); } const goExe = getBinPathWithPreferredGopathGoroot('go', []); log(`Current working directory: ${dirname}`); log(`Building: ${goExe} ${build.join(' ')}`); // Use spawnSync to ensure that the binary exists before running it. const buffer = spawnSync(goExe, build, buildOptions); if (buffer.stderr && buffer.stderr.length > 0) { const str = buffer.stderr.toString(); if (this.onstderr) { this.onstderr(str); } } if (buffer.stdout && buffer.stdout.length > 0) { const str = buffer.stdout.toString(); if (this.onstdout) { this.onstdout(str); } } if (buffer.status) { logError(`Build process exiting with code: ${buffer.status} signal: ${buffer.signal}`); if (this.onclose) { this.onclose(buffer.status); } } else { log(`Build process exiting normally ${buffer.signal}`); } if (buffer.error) { reject(buffer.error); } // Run the built binary let wd = dirname; if (launchArgs.cwd) { wd = launchArgs.cwd; } const runOptions: { [key: string]: any } = { cwd: wd, env }; const run = []; if (launchArgs.args) { run.push(...launchArgs.args); } log(`Current working directory: ${wd}`); log(`Running: ${output} ${run.join(' ')}`); this.debugProcess = spawn(output, run, 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(null); 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 ${getEnvPath()}` ); 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); if (!launchArgs.packagePathToGoModPathMap) { launchArgs.packagePathToGoModPathMap = {}; } 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)); } // add user-specified dlv flags first. When duplicate flags are specified, // dlv doesn't mind but accepts the last flag value. if (launchArgs.dlvFlags && launchArgs.dlvFlags.length > 0) { dlvArgs.push(...launchArgs.dlvFlags); } 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()); // Only add the log output flag if we have already added the log flag. // Otherwise, delve complains. 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.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}`); // add user-specified dlv flags first. When duplicate flags are specified, // dlv doesn't mind but accepts the last flag value. if (launchArgs.dlvFlags && launchArgs.dlvFlags.length > 0) { dlvArgs.push(...launchArgs.dlvFlags); } 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, onClose?: Delve['onclose']) { // Add a slight delay to avoid issues on Linux with // Delve failing calls made shortly after connection. setTimeout(() => { const conn = Client.$create(port, host).connectSocket(); conn.on('connect', () => resolve(conn)) .on('error', reject) .on('close', (hadError) => { logError('Socket connection to remote was closed'); onClose?.(hadError ? 1 : 0); }); }, 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; if (!launchArgs.port || !launchArgs.host) { return reject('Unable to connect, missing host or port from launchArgs.'); } connectClient(launchArgs.port, launchArgs.host, this.onclose); } }); 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); } ); } 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 close the client connection, * but do not terminate the remote server. * * For local debugging, 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})...`); if (this.debugProcess) { await killProcessTree(this.debugProcess, log); } if (this.localDebugeePath) { await removeFile(this.localDebugeePath); } }; if (this.noDebug) { // delve isn't running so no need to halt await forceCleanup(); return Promise.resolve(); } const isLocalDebugging: boolean = this.request === 'launch' && !!this.debugProcess; return new Promise(async (resolve) => { this.delveConnectionClosed = true; // For remote debugging, we want to leave the remote dlv server running, // so instead of killing it via halt+detach, we just close the network connection. // See https://www.github.com/go-delve/delve/issues/1587 if (this.isRemoteDebugging) { log('Remote Debugging: close dlv connection.'); const rpcConnection = await this.connection; // tslint:disable-next-line no-any (rpcConnection as any)['conn']['end'](); return resolve(); } const timeoutToken = isLocalDebugging && setTimeout(async () => { log('Killing debug process manually as we could not halt delve in time'); await forceCleanup(); resolve(); }, 1000); let haltErrMsg: string | undefined; try { log('HaltRequest'); await this.callPromise('Command', [{ name: 'halt' }]); } catch (err) { log('HaltResponse'); log(`Failed to halt - ${err}`); } if (timeoutToken) { clearTimeout(timeoutToken); } const targetHasExited = !!haltErrMsg && haltErrMsg.endsWith('has exited with status 0'); const shouldDetach = !haltErrMsg || targetHasExited; let shouldForceClean = !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}`); 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 overrideStopReason: string; 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>(); // TODO(suzmue): Use delve's implementation of substitute-path. private substitutePath?: { from: string; to: string }[]; private showGlobalVariables = false; private continueEpoch = 0; private continueRequestRunning = false; private nextEpoch = 0; private nextRequestRunning = false; public constructor(debuggerLinesStartAt1: boolean, isServer = false, readonly fileSystem = fs) { super('', debuggerLinesStartAt1, isServer); this.variableHandles = new Handles<DebugVariable>(); this.skipStopEventOnce = false; this.overrideStopReason = ''; this.stopOnEntry = false; this.breakpoints = new Map<string, DebugBreakpoint[]>(); this.stackFrameHandles = new Handles<[number, number]>(); } protected initializeRequest( response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments ): void { log('InitializeRequest'); // Set the capabilities that this debug adapter supports. response.body = response.body ?? {}; response.body.supportsConditionalBreakpoints = true; 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'); if (this.delve) { // Since users want to reset when they issue a disconnect request, // we should have a timeout in case disconnectRequestHelper hangs. await Promise.race([ this.disconnectRequestHelper(response, args), new Promise<void>((resolve) => setTimeout(() => { log('DisconnectRequestHelper timed out after 5s.'); resolve(); }, 5_000) ) ]); } this.shutdownProtocolServer(response, args); log('DisconnectResponse'); } protected async disconnectRequestHelper( response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments ): Promise<void> { // There is a chance that a second disconnectRequest can come through // if users click detach multiple times. In that case, we want to // guard against talking to the closed Delve connection. // Note: this does not completely guard against users attempting to // disconnect multiple times when a disconnect request is still running. // The order of the execution may results in strange states that don't allow // the delve connection to fully disconnect. if (this.delve?.delveConnectionClosed) { log("Skip disconnectRequestHelper as Delve's connection is already closed."); return; } // For remote process, we have to issue a continue request // before disconnecting. if (this.delve?.isRemoteDebugging) { if (!(await this.isDebuggeeRunning())) { log("Issuing a continue command before closing Delve's connection as the debuggee is not running."); this.continue(); } } log('Closing Delve.'); await this.delve?.close(); } 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")'); } else if (!(await this.isDebuggeeRunning())) { log('Changing DebugState from Halted to Running'); this.continue(); } this.sendResponse(response); log('ConfigurationDoneResponse', 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.substitutePath?.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 for ease in path replacement. filePath = normalizeSeparators(filePath); let substitutedPath = filePath; let substituteRule: { from: string; to: string }; this.substitutePath?.forEach((value) => { if (filePath.startsWith(value.from)) { if (substituteRule) { log( `Substitutition rule ${value.from}:${value.to} applies to local path ${filePath} but it was already mapped to debugger path using rule ${substituteRule.from}:${substituteRule.to}` ); return; } substitutedPath = filePath.replace(value.from, value.to); substituteRule = { from: value.from, to: value.to }; } }); filePath = substitutedPath; return (filePath = filePath.replace(/\/|\\/g, this.remotePathSeparator ?? '')); } /** * 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 { // Don't try to infer a path for a file that does not exist if (remotePath === '') { return remotePath; } 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.substitutePath?.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); } // If there is a substitutePath mapping, then we replace the path. pathToConvert = normalizeSeparators(pathToConvert); let substitutedPath = pathToConvert; let substituteRule: { from: string; to: string } | undefined; this.substitutePath?.forEach((value) => { if (pathToConvert.startsWith(value.to)) { if (substituteRule) { log( `Substitutition rule ${value.from}:${value.to} applies to debugger path ${pathToConvert} but it was already mapped to local path using rule ${substituteRule.from}:${substituteRule.to}` ); return; } substitutedPath = pathToConvert.replace(value.to, value.from); substituteRule = { from: value.from, to: value.to }; } }); pathToConvert = substitutedPath; // When the pathToConvert is under GOROOT or Go module cache, replace path appropriately if (!substituteRule) { // 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.split(this.remotePathSeparator ?? '').join(this.localPathSeparator); } protected async setBreakPointsRequest( response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments ): Promise<void> { log('SetBreakPointsRequest'); if (!(await this.isDebuggeeRunning())) { log('Debuggee is not running. Setting breakpoints without halting.'); await this.setBreakPoints(response, args); } else { // Skip stop event if a continue request is running. this.skipStopEventOnce = this.continueRequestRunning; const haltedDuringNext = this.nextRequestRunning; if (haltedDuringNext) { this.overrideStopReason = 'next cancelled'; } log(`Halting before setting breakpoints. SkipStopEventOnce is ${this.skipStopEventOnce}.`); this.delve?.callPromise('Command', [{ name: 'halt' }]).then( () => { return this.setBreakPoints(response, args).then(() => { // We do not want to continue if it was running a next request, since the // request was automatically cancelled. if (haltedDuringNext) { // Send an output event containing a warning that next was cancelled. const warning = "Setting breakpoints during 'next', 'step in' or 'step out' halted delve and cancelled the next request"; this.sendEvent(new OutputEvent(warning, 'stderr')); return; } return this.continue(true).then(undefined, (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 async threadsRequest(response: DebugProtocol.ThreadsResponse): Promise<void> { if (await this.isDebuggeeRunning()) { // Thread request to delve is synchronous and will block if a previous async continue request didn't return response.body = { threads: [new Thread(1, 'Dummy')] }; return this.sendResponse(response); } else 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. d response.body = { threads: [] }; 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 async stackTraceRequest( response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments ): Promise<void> { log('StackTraceRequest'); // For normal VSCode, this request doesn't get invoked when we send a Dummy thread // in the scenario where the debuggee is running. // For Theia, however, this does get invoked and so we should just send an error // response that we cannot get the stack trace at this point since the debugggee is running. if (await this.isDebuggeeRunning()) { this.sendErrorResponse(response, 2004, 'Unable to produce stack trace as the debugger is running'); return; } // 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() }, // Disable showUser pop-up since errors already show up under the CALL STACK pane undefined ); } 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>' ? undefined : new Source(path.basename(location.file), this.toLocalPath(location.file)), location.line, 0 ); }); const { startFrame = 0, levels = 0 } = args; if (startFrame > 0) { stackFrames = stackFrames.slice(startFrame); } if (levels > 0) { stackFrames = stackFrames.slice(0, levels); } response.body = { stackFrames, totalFrames: locations.length }; this.sendResponse(response); log('StackTraceResponse'); } ); } protected scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void { log('ScopesRequest'); // TODO(polinasok): this.stackFrameHandles.get should succeed as long as DA // clients behaves well. Find the documentation around stack frame management // and in case of a failure caused by misbehavior, consider to indicate it // in the error response. 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[]> | undefined; 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 } as DebugProtocol.Variable; }); } }) .filter((v): v is Promise<DebugProtocol.Variable> => !!v) // remove the null values created by combining keys and values ); } 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 { this.nextEpoch++; const closureEpoch = this.nextEpoch; this.nextRequestRunning = true; log('NextRequest'); this.delve?.call<DebuggerState | CommandOut>('Command', [{ name: 'next' }], (err, out) => { if (closureEpoch === this.continueEpoch) { this.nextRequestRunning = false; } 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'); }); // All threads are resumed on a next request this.sendEvent(new ContinuedEvent(1, true)); this.sendResponse(response); log('NextResponse'); } protected stepInRequest(response: DebugProtocol.StepInResponse): void { this.nextEpoch++; const closureEpoch = this.nextEpoch; this.nextRequestRunning = true; log('StepInRequest'); this.delve?.call<DebuggerState | CommandOut>('Command', [{ name: 'step' }], (err, out) => { if (closureEpoch === this.continueEpoch) { this.nextRequestRunning = false; } 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'); }); // All threads are resumed on a step in request this.sendEvent(new ContinuedEvent(1, true)); this.sendResponse(response); log('StepInResponse'); } protected stepOutRequest(response: DebugProtocol.StepOutResponse): void { this.nextEpoch++; const closureEpoch = this.nextEpoch; this.nextRequestRunning = true; log('StepOutRequest'); this.delve?.call<DebuggerState | CommandOut>('Command', [{ name: 'stepOut' }], (err, out) => { if (closureEpoch === this.continueEpoch) { this.nextRequestRunning = false; } 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'); }); // All threads are resumed on a step out request this.sendEvent(new ContinuedEvent(1, true)); 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'); } // evaluateRequest is used both for the traditional expression evaluation // (https://github.com/go-delve/delve/blob/master/Documentation/cli/expr.md) and // for the 'call' command support. // If the args.expression starts with the 'call' keyword followed by an expression that looks // like a function call, the request is interpreted as a 'call' command request, // and otherwise, interpreted as `print` command equivalent with RPCServer.Eval. protected evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): void { log('EvaluateRequest'); // Captures pattern that looks like the expression that starts with `call<space>` // command call. This is supported only with APIv2. const isCallCommand = args.expression.match(/^\s*call\s+\S+/); if (!this.delve?.isApiV1 && isCallCommand) { this.evaluateCallImpl(args).then( (out) => { const state = (<CommandOut>out).State; const returnValues = state?.currentThread?.ReturnValues ?? []; switch (returnValues.length) { case 0: response.body = { result: '', variablesReference: 0 }; break; case 1: response.body = this.convertDebugVariableToProtocolVariable(returnValues[0]); break; default: // Go function can return multiple return values while // DAP EvaluateResponse assumes a single result with possibly // multiple children. So, create a fake DebugVariable // that has all the results as children. const returnResults = this.wrapReturnVars(returnValues); response.body = this.convertDebugVariableToProtocolVariable(returnResults); break; } this.sendResponse(response); log('EvaluateCallResponse'); }, (err) => { this.sendErrorResponse( response, 2009, 'Unable to complete call: "{e}"', { e: err.toString() }, args.context === 'watch' ? undefined : ErrorDestination.User ); } ); return; } // Now handle it as a conventional 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) => { // No need to repeatedly show the error pop-up when expressions // are continuously reevaluated in the Watch panel, which // already displays errors. this.sendErrorResponse( response, 2009, 'Unable to eval expression: "{e}"', { e: err.toString() }, args.context === 'watch' ? undefined : ErrorDestination.User ); } ); } 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' || args.trace === 'trace' ? Logger.LogLevel.Verbose : args.trace === 'log' || args.trace === 'info' || args.trace === 'warn' ? 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 = ''; 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); this.substitutePath = []; 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 (; llist && 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); } // Make the remotePath mapping the first one in substitutePath // so that it will take precedence over the other mappings. this.substitutePath.push({ from: normalizeSeparators(localPath), to: normalizeSeparators(args.remotePath) }); } if (args.substitutePath) { args.substitutePath.forEach((value) => { if (!this.remotePathSeparator) { this.remotePathSeparator = findPathSeparator(value.to); } this.substitutePath?.push({ from: normalizeSeparators(value.from), to: normalizeSeparators(value.to) }); }); } // 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) => { if (localPath.length > 0) { str = expandFilePathInOutput(str, localPath); } 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) { if (!this.delve) { return; } 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<void>((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(undefined, 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); return null; } } // Make sure that we compare the file names with the same separators. const matchedBreakpoint = existingBreakpoints.find( (existingBreakpoint) => existingBreakpoint.line === breakpointIn.line && compareFilePathIgnoreSeparator(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 | null)[]; 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 is DebugBreakpoint => !!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 | undefined): Promise<string | void> { if (!debugState || !debugState.currentThread || !debugState.currentThread.file) { return Promise.resolve(); } 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); } ); }); } // Go might return more than one result while DAP and VS Code do not support // such scenario but assume one single result. So, wrap all return variables // in one made-up, nameless, invalid variable. This is similar to how scopes // are represented. This assumes the vars are the ordered list of return // values from a function call. private wrapReturnVars(vars: DebugVariable[]): DebugVariable { // VS Code uses the value property of the DebugVariable // when displaying it. So let's formulate it in a user friendly way // as if they look like a list of multiple values. // Note: we use only convertDebugVariableToProtocolVariable's result, // which means we will leak the variable references until the handle // map is cleared. Assuming the number of return parameters is handful, // this waste shouldn't be significant. const values = vars.map((v) => this.convertDebugVariableToProtocolVariable(v).result) || []; return { value: values.join(', '), kind: GoReflectKind.Invalid, flags: GoVariableFlags.VariableFakeAddress | GoVariableFlags.VariableReturnArgument, children: vars, // DebugVariable requires the following fields. name: '', addr: 0, type: '', realType: '', onlyAddr: false, DeclLine: 0, len: 0, cap: 0, unreadable: '', base: 0, fullyQualifiedName: '' }; } 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]) { return { result: 'unknown <' + v.type + '>', variablesReference: 0 }; } else 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 if (v.kind === GoReflectKind.Interface) { if (v.addr === 0) { // an escaped interface variable that points to nil, this shouldn't // happen in normal code but can happen if the variable is out of scope. return { result: 'nil', variablesReference: 0 }; } if (v.children.length === 0) { // Shouldn't happen, but to be safe. return { result: 'nil', variablesReference: 0 }; } const child = v.children[0]; if (child.kind === GoReflectKind.Invalid && child.addr === 0) { return { result: `nil <${v.type}>`, variablesReference: 0 }; } return { // TODO(hyangah): v.value will be useless. consider displaying more info from the child. // https://github.com/go-delve/delve/blob/930fa3b/service/api/prettyprint.go#L106-L124 result: v.value || `<${v.type}(${child.type})>)`, variablesReference: v.children?.length > 0 ? this.variableHandles.create(v) : 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 { log(`handleReenterDebug(${reason}).`); 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 && !this.debugState?.currentGoroutine && goroutines.length > 0) { this.debugState.currentGoroutine = goroutines[0]; } if (this.skipStopEventOnce) { log( `Skipping stop event for ${reason}. The current Go routines is ${this.debugState?.currentGoroutine}.` ); this.skipStopEventOnce = false; return; } if (this.overrideStopReason?.length > 0) { reason = this.overrideStopReason; this.overrideStopReason = ''; } const stoppedEvent = new StoppedEvent(reason, this.debugState?.currentGoroutine.id); (<any>stoppedEvent.body).allThreadsStopped = true; this.sendEvent(stoppedEvent); log('StoppedEvent("' + reason + '")'); }); } } // Returns true if the debuggee is running. // The call getDebugState is non-blocking so it should return // almost instantaneously. However, if we run into some errors, // we will fall back to the internal tracking of the debug state. // TODO: If Delve is not in multi-client state, we can simply // track the running state with continueRequestRunning internally // instead of issuing a getDebugState call to Delve. Perhaps we want to // do that to improve performance in the future. private async isDebuggeeRunning(): Promise<boolean> { if (this.debugState && this.debugState.exited) { return false; } try { this.debugState = await this.delve?.getDebugState(); return !!this.debugState?.Running; } catch (error) { this.logDelveError(error, 'Failed to get state'); // Fall back to the internal tracking. return this.continueRequestRunning || this.nextRequestRunning; } } private shutdownProtocolServer( response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments ): void { log('DisconnectRequest to parent to shut down protocol server.'); super.disconnectRequest(response, args); } 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; let reason = 'breakpoint'; // Check if the current thread was stopped on 'panic' or 'fatal error'. if (!!state.currentThread && !!state.currentThread.breakPoint) { const bp = state.currentThread.breakPoint; if (bp.id === unrecoveredPanicID) { // If the breakpoint is actually caused by a panic, // we want to return on "panic". reason = 'panic'; } else if (bp.id === fatalThrowID) { // If the breakpoint is actually caused by a fatal throw, // we want to return on "fatal error". reason = 'fatal error'; } } this.handleReenterDebug(reason); }; // If called when setting breakpoint internally, we want the error to bubble up. let errorCallback = (_: unknown): any => void 0; if (!calledWhenSettingBreakpoint) { errorCallback = (err: any) => { if (err) { this.logDelveError(err, 'Failed to continue'); } this.handleReenterDebug('breakpoint'); throw err; }; } if (this.delve) { return this.delve.callPromise('Command', [{ name: 'continue' }]).then(callback, errorCallback); } return Promise.resolve(); } // evaluateCallImpl expects args.expression starts with the 'call ' command. private evaluateCallImpl(args: DebugProtocol.EvaluateArguments): Thenable<DebuggerState | CommandOut | void> { const callExpr = args.expression.trimLeft().slice('call '.length); // if args.frameID is 'not specified', expression is evaluated in the global scope, according to DAP. // default to the topmost stack frame of the current goroutine let goroutineId = -1; let frameId = 0; if (args.frameId) { [goroutineId, frameId] = this.stackFrameHandles.get(args.frameId, [goroutineId, frameId]); } // See https://github.com/go-delve/delve/blob/328cf87808822693dc611591519689dcd42696a3/service/api/types.go#L321-L350 // for the command args for function call. const returnValue = this.delve ?.callPromise<DebuggerState | CommandOut>('Command', [ { name: 'call', goroutineID: goroutineId, returnInfoLoadConfig: this.delve?.loadConfig, expr: callExpr, unsafe: false } ]) .then( (val) => val, (err) => { logError( 'Failed to call function: ', JSON.stringify(callExpr, null, ' '), '\n\rCall error:', err.toString() ); return Promise.reject(err); } ); return returnValue ?? Promise.resolve(); } private evaluateRequestImpl(args: DebugProtocol.EvaluateArguments): Thenable<EvalOut | DebugVariable | void> { // 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, [goroutineId, 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 ?? Promise.resolve(); } 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 propagate 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 { this.debugState = await this.delve?.getDebugState(); // In some fault scenarios there may not be a currentGoroutine available from the debugger state // Use the current thread if (!this.debugState?.currentGoroutine) { goroutineId = this.debugState?.currentThread.goroutineID ?? 0; } else { goroutineId = this.debugState?.currentGoroutine.id ?? 0; } } 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>' ? undefined : 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 as 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}`); } } // 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);