From 70186bb8641ca5ff47f089ee4752a356cd45991a Mon Sep 17 00:00:00 2001 From: Joel Hendrix Date: Mon, 18 Mar 2019 14:03:57 -0700 Subject: [PATCH 1/6] refactor attach feature Added template for attaching to process. Fixed template for connecting to a server. Removed unnecessary fields from AttachRequestArguments Cleaned up Delve.constructor to remove unnecessary parameters. Removed duplicated code from attachRequest(). Cleaned up handling of legacy 'useApiV1' setting. Ensure that property 'cwd' is set in debug configuration. --- package.json | 161 +++++++++++++- src/debugAdapter/goDebug.ts | 410 ++++++++++++++++++++++-------------- src/goDebugConfiguration.ts | 9 +- 3 files changed, 409 insertions(+), 171 deletions(-) diff --git a/package.json b/package.json index 486c2b701..f81bdca03 100644 --- a/package.json +++ b/package.json @@ -370,20 +370,28 @@ ] } }, + { + "label": "Go: Attach to process", + "description": "Attach to an existing process by process ID", + "body": { + "name": "${1:Attach to Process}", + "type": "go", + "request": "attach", + "mode": "attach", + "processId": 0 + } + }, { "label": "Go: Connect to server", "description": "Connect to a remote headless debug server", "body": { - "name": "${3:Connect to server}", + "name": "${1:Connect to server}", "type": "go", - "request": "launch", - "mode": "remote", - "remotePath": "^\"\\${workspaceFolder}${1:}\"", + "request": "attach", + "mode": "connect", + "remotePath": "^\"\\${workspaceFolder}\"", "port": 2345, - "host": "127.0.0.1", - "program": "^\"\\${workspaceFolder}${1:}\"", - "env": {}, - "args": [] + "host": "127.0.0.1" } } ], @@ -559,6 +567,143 @@ "description": "Boolean value to indicate whether global package variables should be shown in the variables pane or not." } } + }, + "attach": { + "required": [], + "properties": { + "processId": { + "type": "number", + "description": "The ID of the process to be debugged." + }, + "mode": { + "enum": [ + "attach", + "connect" + ], + "description": "One of 'auto', 'debug', 'remote', 'test', 'exec'.", + "default": "auto" + }, + "stopOnEntry": { + "type": "boolean", + "description": "Automatically stop program after launch.", + "default": false + }, + "showLog": { + "type": "boolean", + "description": "Show log output from the delve debugger.", + "default": false + }, + "cwd": { + "type": "string", + "description": "Workspace relative or absolute path to the working directory of the program being debugged. Default is the current workspace.", + "default": "${workspaceFolder}" + }, + "remotePath": { + "type": "string", + "description": "If remote debugging, the path to the source code on the remote machine, if different from the local machine.", + "default": "" + }, + "port": { + "type": "number", + "description": "The port that the delve debugger will be listening on.", + "default": 2345 + }, + "host": { + "type": "string", + "description": "The host name of the machine the delve debugger will be listening on.", + "default": "127.0.0.1" + }, + "trace": { + "type": [ + "boolean", + "string" + ], + "enum": [ + "verbose", + true + ], + "default": true, + "description": "When 'true', the extension will log diagnostic info to a file. When 'verbose', it will also show logs in the console." + }, + "backend": { + "type": "string", + "enum": [ + "default", + "native", + "lldb" + ], + "description": "Backend used by delve. Only available in delve version 0.12.2 and above." + }, + "logOutput": { + "type": "string", + "enum": [ + "debugger", + "gdbwire", + "lldbout", + "debuglineerr", + "rpc" + ], + "description": "Comma separated list of components that should produce debug output.", + "default": "debugger" + }, + "dlvLoadConfig": { + "type": "object", + "properties": { + "followPointers": { + "type": "boolean", + "description": "FollowPointers requests pointers to be automatically dereferenced", + "default": true + }, + "maxVariableRecurse": { + "type": "number", + "description": "MaxVariableRecurse is how far to recurse when evaluating nested types", + "default": 1 + }, + "maxStringLen": { + "type": "number", + "description": "MaxStringLen is the maximum number of bytes read from a string", + "default": 64 + }, + "maxArrayValues": { + "type": "number", + "description": "MaxArrayValues is the maximum number of elements read from an array, a slice or a map", + "default": 64 + }, + "maxStructFields": { + "type": "number", + "description": "MaxStructFields is the maximum number of fields read from a struct, -1 will read all fields", + "default": -1 + } + }, + "description": "LoadConfig describes to delve, how to load values from target's memory", + "default": { + "followPointers": true, + "maxVariableRecurse": 1, + "maxStringLen": 64, + "maxArrayValues": 64, + "maxStructFields": -1 + } + }, + "apiVersion": { + "type": "number", + "enum": [ + 1, + 2 + ], + "description": "Delve Api Version to use. Default value is 2.", + "default": 2 + }, + "stackTraceDepth": { + "type": "number", + "description": "Maximum depth of stack trace collected from Delve", + "default": 50 + }, + "showGlobalVariables": { + "type": "boolean", + "default": true, + "description": "Boolean value to indicate whether global package variables should be shown in the variables pane or not." + } + } } } } diff --git a/src/debugAdapter/goDebug.ts b/src/debugAdapter/goDebug.ts index 2352dff85..498893a16 100644 --- a/src/debugAdapter/goDebug.ts +++ b/src/debugAdapter/goDebug.ts @@ -201,6 +201,7 @@ interface DiscardedBreakpoint { // This interface should always match the schema found in `package.json`. interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments { + kind: 'launch'; [key: string]: any; program: string; stopOnEntry?: boolean; @@ -232,6 +233,31 @@ interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments { currentFile: string; } +interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments { + kind: 'attach'; + processId?: number; + stopOnEntry?: boolean; + showLog?: boolean; + logOutput?: string; + cwd?: string; + mode?: string; + 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; + currentFile: string; +} + process.on('uncaughtException', (err: any) => { const errMessage = err && (err.stack || err.message); logger.error(`Unhandled error in debug adapter: ${errMessage}`); @@ -276,162 +302,193 @@ class Delve { isApiV1: boolean; dlvEnv: any; stackTraceDepth: number; + kind: 'attach' | 'launch'; - constructor(remotePath: string, port: number, host: string, program: string, launchArgs: LaunchRequestArguments) { + constructor(launchArgs: LaunchRequestArguments | AttachRequestArguments, program: string) { + this.kind = launchArgs.kind; this.program = normalizePath(program); - this.remotePath = remotePath; + this.remotePath = launchArgs.remotePath; this.isApiV1 = false; if (typeof launchArgs.apiVersion === 'number') { this.isApiV1 = launchArgs.apiVersion === 1; - } else if (typeof launchArgs['useApiV1'] === 'boolean') { - this.isApiV1 = launchArgs['useApiV1']; } this.stackTraceDepth = typeof launchArgs.stackTraceDepth === 'number' ? launchArgs.stackTraceDepth : 50; - const mode = launchArgs.mode; - let dlvCwd = dirname(program); - let isProgramDirectory = false; - const launchArgsEnv = launchArgs.env || {}; this.connection = new Promise((resolve, reject) => { - // Validations on the program - if (!program) { - return reject('The program attribute is missing in the debug configuration in launch.json'); + const mode = launchArgs.mode; + let dlvCwd = dirname(program); + let serverRunning = false; + const dlvArgs = new Array(); + if (mode === 'remote' || mode === 'connect') { + this.debugProcess = null; + serverRunning = true; // assume server is running when in remote mode + connectClient(launchArgs.port, launchArgs.host); + return; } - 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'); + let env: NodeJS.ProcessEnv; + if (launchArgs.kind === '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' && 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'); } - dlvCwd = program; - isProgramDirectory = true; - } else if (mode !== 'exec' && 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.'); } - } 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 - let fileEnv = {}; - try { - fileEnv = parseEnvFile(launchArgs.envFile); - } catch (e) { - return reject(e); - } - - const env = Object.assign({}, process.env, fileEnv, launchArgsEnv); + // read env from disk and merge into env variables + let fileEnv = {}; + try { + fileEnv = parseEnvFile(launchArgs.envFile); + } catch (e) { + return reject(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; - log(`Using GOPATH: ${env['GOPATH']}`); - - if (!!launchArgs.noDebug) { - if (mode === 'debug') { - if (isProgramDirectory && launchArgs.currentFile) { - program = launchArgs.currentFile; - isProgramDirectory = false; - } + const launchArgsEnv = launchArgs.env || {}; + env = Object.assign({}, process.env, fileEnv, launchArgsEnv); - if (!isProgramDirectory) { - this.noDebug = true; - const runArgs = ['run']; - if (launchArgs.buildFlags) { - runArgs.push(launchArgs.buildFlags); + 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; + log(`Using GOPATH: ${env['GOPATH']}`); + + if (!!launchArgs.noDebug) { + if (mode === 'debug') { + if (isProgramDirectory && launchArgs.currentFile) { + program = launchArgs.currentFile; + isProgramDirectory = false; } - runArgs.push(program); - if (launchArgs.args) { - runArgs.push(...launchArgs.args); + + if (!isProgramDirectory) { + this.noDebug = true; + const runArgs = ['run']; + if (launchArgs.buildFlags) { + runArgs.push(launchArgs.buildFlags); + } + runArgs.push(program); + if (launchArgs.args) { + runArgs.push(...launchArgs.args); + } + this.debugProcess = spawn(getBinPathWithPreferredGopath('go', []), runArgs, { env }); + 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) => { + logError('Process exiting with code: ' + code); + if (this.onclose) { this.onclose(code); } + }); + this.debugProcess.on('error', (err) => { + reject(err); + }); + resolve(); + return; } - this.debugProcess = spawn(getBinPathWithPreferredGopath('go', []), runArgs, { env }); - 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) => { - logError('Process exiting with code: ' + code); - if (this.onclose) { this.onclose(code); } - }); - this.debugProcess.on('error', (err) => { - reject(err); - }); - resolve(); - return; } } - } - this.noDebug = false; - let serverRunning = false; + this.noDebug = false; + + // Get default LoadConfig values according to delve API: + // https://github.com/derekparker/delve/blob/c5c41f635244a22d93771def1c31cf1e0e9a2e63/service/rpc1/server.go#L13 + // https://github.com/derekparker/delve/blob/c5c41f635244a22d93771def1c31cf1e0e9a2e63/service/rpc2/server.go#L423 + this.loadConfig = launchArgs.dlvLoadConfig || { + followPointers: true, + maxVariableRecurse: 1, + maxStringLen: 64, + maxArrayValues: 64, + maxStructFields: -1 + }; - // Get default LoadConfig values according to delve API: - // https://github.com/derekparker/delve/blob/c5c41f635244a22d93771def1c31cf1e0e9a2e63/service/rpc1/server.go#L13 - // https://github.com/derekparker/delve/blob/c5c41f635244a22d93771def1c31cf1e0e9a2e63/service/rpc2/server.go#L423 - this.loadConfig = launchArgs.dlvLoadConfig || { - followPointers: true, - maxVariableRecurse: 1, - maxStringLen: 64, - maxArrayValues: 64, - maxStructFields: -1 - }; + 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/derekparker/delve & ensure it is in your Go tools path, "GOPATH/bin" or "PATH".`); + } - if (mode === 'remote') { - this.debugProcess = null; - serverRunning = true; // assume server is running when in remote mode - connectClient(port, host); - return; - } + const currentGOWorkspace = getCurrentGoWorkspaceFromGOPATH(env['GOPATH'], dirname); + dlvArgs.push(mode || 'debug'); + if (mode === 'exec') { + dlvArgs.push(program); + } else if (currentGOWorkspace && env['GO111MODULE'] !== 'on') { + dlvArgs.push(dirname.substr(currentGOWorkspace.length + 1)); + } + dlvArgs.push('--headless=true', '--listen=' + launchArgs.host + ':' + launchArgs.port.toString()); + if (!this.isApiV1) { + dlvArgs.push('--api-version=2'); + } - 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/derekparker/delve & ensure it is in your Go tools path, "GOPATH/bin" or "PATH".`); - } + 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') { + 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.kind === 'attach') { + if (!launchArgs.processId) { + return reject(`Missing process ID`); + } - const currentGOWorkspace = getCurrentGoWorkspaceFromGOPATH(env['GOPATH'], dirname); - let dlvArgs = [mode || 'debug']; - if (mode === 'exec') { - dlvArgs = dlvArgs.concat([program]); - } else if (currentGOWorkspace && env['GO111MODULE'] !== 'on') { - dlvArgs = dlvArgs.concat([dirname.substr(currentGOWorkspace.length + 1)]); - } - dlvArgs = dlvArgs.concat(['--headless=true', '--listen=' + host + ':' + port.toString()]); - if (!this.isApiV1) { - dlvArgs.push('--api-version=2'); - } + if (!existsSync(launchArgs.dlvToolPath)) { + return reject(`Cannot find Delve debugger. Install from https://github.com/derekparker/delve & ensure it is in your Go tools path, "GOPATH/bin" or "PATH".`); + } - if (launchArgs.showLog) { - dlvArgs = dlvArgs.concat(['--log=' + launchArgs.showLog.toString()]); - } - if (launchArgs.logOutput) { - dlvArgs = dlvArgs.concat(['--log-output=' + launchArgs.logOutput]); - } - if (launchArgs.cwd) { - dlvArgs = dlvArgs.concat(['--wd=' + launchArgs.cwd]); - } - if (launchArgs.buildFlags) { - dlvArgs = dlvArgs.concat(['--build-flags=' + launchArgs.buildFlags]); - } - if (launchArgs.init) { - dlvArgs = dlvArgs.concat(['--init=' + launchArgs.init]); - } - if (launchArgs.backend) { - dlvArgs = dlvArgs.concat(['--backend=' + launchArgs.backend]); - } - if (launchArgs.output && mode === 'debug') { - dlvArgs = dlvArgs.concat(['--output=' + launchArgs.output]); - } - if (launchArgs.args && (launchArgs.args.length > 0)) { - dlvArgs = dlvArgs.concat(['--', ...launchArgs.args]); + 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}`); @@ -442,7 +499,6 @@ class Delve { env, }); - this.localDebugeePath = this.getLocalDebugeePath(launchArgs.output); function connectClient(port: number, host: string) { // Add a slight delay to avoid issues on Linux with // Delve failing calls made shortly after connection. @@ -464,7 +520,7 @@ class Delve { if (this.onstdout) { this.onstdout(str); } if (!serverRunning) { serverRunning = true; - connectClient(port, host); + connectClient(launchArgs.port, launchArgs.host); } }); this.debugProcess.on('close', (code) => { @@ -478,7 +534,7 @@ class Delve { }); } - call(command: string, args: any[], callback: (err: Error, results: T) => void) { + public call(command: string, args: any[], callback: (err: Error, results: T) => void) { this.connection.then(conn => { conn.call('RPCServer.' + command, args, callback); }, err => { @@ -499,12 +555,14 @@ class Delve { } /** - * Closing a debugging session follows different approaches for local vs remote delve process. + * Closing a debugging session follows different approaches for launch vs attach debugging. * - * For local process, since the extension starts the delve process, the extension should close it as well. + * 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 remote process, since the extension doesnt start the delve process, we only detach from it without killing it. + * For attach debugging there are two scenarios; attaching to an existing process by ID or connecting to an + * existing delve server. For debug-attach we start the delve process so will also terminate it however we + * detach from the debugee without killing it. For debug-connect 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 @@ -517,7 +575,7 @@ class Delve { } log('HaltRequest'); - const isLocalDebugging: boolean = !!this.debugProcess; + const isLocalDebugging: boolean = this.kind === 'launch' && !!this.debugProcess; const forceCleanup = async () => { killTree(this.debugProcess.pid); await removeFile(this.localDebugeePath); @@ -529,7 +587,7 @@ class Delve { resolve(); }, 1000); - let haltErrMsg; + let haltErrMsg: string; try { await this.callPromise('Command', [{ name: 'halt' }]); } catch (err) { @@ -579,7 +637,7 @@ class GoDebugSession extends LoggingDebugSession { private remotePathSeparator: string; private stackFrameHandles: Handles<[number, number]>; private packageInfo = new Map(); - private launchArgs: LaunchRequestArguments; + private stopOnEntry: boolean; private logLevel: Logger.LogLevel = Logger.LogLevel.Error; private readonly initdone = 'initdoneĀ·'; @@ -588,6 +646,7 @@ class GoDebugSession extends LoggingDebugSession { super('', debuggerLinesStartAt1, isServer); this.variableHandles = new Handles(); this.skipStopEventOnce = false; + this.stopOnEntry = false; this.goroutines = new Set(); this.debugState = null; this.delve = null; @@ -609,8 +668,8 @@ class GoDebugSession extends LoggingDebugSession { return path.includes('/') ? '/' : '\\'; } - protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { - this.launchArgs = args; + // 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 : @@ -621,36 +680,46 @@ class GoDebugSession extends LoggingDebugSession { if (typeof (args.showGlobalVariables) === 'boolean') { this.showGlobalVariables = args.showGlobalVariables; } - - if (!args.program) { - this.sendErrorResponse(response, 3000, 'Failed to continue: The program attribute is missing in the debug configuration in launch.json'); - return; + if (args.stopOnEntry) { + this.stopOnEntry = args.stopOnEntry; + } + if (!args.port) { + args.port = random(2000, 50000); + } + if (!args.host) { + args.host = '127.0.0.1'; } - // Launch the Delve debugger on the program - let localPath = args.program; - let remotePath = args.remotePath || ''; - const port = args.port || random(2000, 50000); - const host = args.host || '127.0.0.1'; + let localPath: string; + if (args.kind === 'attach') { + localPath = args.cwd; + } else if (args.kind === 'launch') { + localPath = args.program; + } + if (!args.remotePath) { + // too much code relies on remotePath never being null + args.remotePath = ''; + } - if (remotePath.length > 0) { + if (args.remotePath.length > 0) { this.localPathSeparator = this.findPathSeperator(localPath); - this.remotePathSeparator = this.findPathSeperator(remotePath); + this.remotePathSeparator = this.findPathSeperator(args.remotePath); const llist = localPath.split(/\/|\\/).reverse(); - const rlist = remotePath.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; - remotePath = rlist.reverse().slice(0, -i).join(this.remotePathSeparator) + this.remotePathSeparator; - } else if ((remotePath.endsWith('\\')) || (remotePath.endsWith('/'))) { - remotePath = remotePath.substring(0, remotePath.length - 1); + args.remotePath = rlist.reverse().slice(0, -i).join(this.remotePathSeparator) + this.remotePathSeparator; + } else if ((args.remotePath.endsWith('\\')) || (args.remotePath.endsWith('/'))) { + args.remotePath = args.remotePath.substring(0, args.remotePath.length - 1); } } - this.delve = new Delve(remotePath, port, host, localPath, args); + // Launch the Delve debugger on the program + this.delve = new Delve(args, localPath); this.delve.onstdout = (str: string) => { this.sendEvent(new OutputEvent(str, 'stdout')); }; @@ -692,6 +761,25 @@ class GoDebugSession extends LoggingDebugSession { }); } + protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { + args.kind = 'launch'; + 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 { + args.kind = 'attach'; + if (args.mode === 'attach' && !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 === 'connect' && !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 disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments): void { log('DisconnectRequest'); this.delve.close().then(() => { @@ -704,7 +792,7 @@ class GoDebugSession extends LoggingDebugSession { protected configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse, args: DebugProtocol.ConfigurationDoneArguments): void { log('ConfigurationDoneRequest'); - if (this.launchArgs.stopOnEntry) { + if (this.stopOnEntry) { this.sendEvent(new StoppedEvent('breakpoint', 0)); log('StoppedEvent("breakpoint")'); this.sendResponse(response); diff --git a/src/goDebugConfiguration.ts b/src/goDebugConfiguration.ts index d79e74636..fb82f26db 100644 --- a/src/goDebugConfiguration.ts +++ b/src/goDebugConfiguration.ts @@ -34,7 +34,7 @@ export class GoDebugConfigurationProvider implements vscode.DebugConfigurationPr sendTelemetryEvent('debugConfiguration', { request: debugConfiguration.request, mode: debugConfiguration.mode, - useApiV1: debugConfiguration.useApiV1, + apiVersion: debugConfiguration.apiVersion, stopOnEntry: debugConfiguration.stopOnEntry }); } @@ -71,7 +71,9 @@ export class GoDebugConfigurationProvider implements vscode.DebugConfigurationPr const dlvConfig: { [key: string]: any } = goConfig.get('delveConfig'); if (!debugConfiguration.hasOwnProperty('useApiV1') && dlvConfig.hasOwnProperty('useApiV1')) { - debugConfiguration['useApiV1'] = dlvConfig['useApiV1']; + if (typeof dlvConfig['useApiV1'] === 'boolean' && dlvConfig['useApiV1'] === true) { + debugConfiguration['apiVersion'] = 1; + } } if (!debugConfiguration.hasOwnProperty('apiVersion') && dlvConfig.hasOwnProperty('apiVersion')) { debugConfiguration['apiVersion'] = dlvConfig['apiVersion']; @@ -82,6 +84,9 @@ export class GoDebugConfigurationProvider implements vscode.DebugConfigurationPr if (!debugConfiguration.hasOwnProperty('showGlobalVariables') && dlvConfig.hasOwnProperty('showGlobalVariables')) { debugConfiguration['showGlobalVariables'] = dlvConfig['showGlobalVariables']; } + if (!debugConfiguration['cwd']) { + debugConfiguration['cwd'] = '${workspaceFolder}'; + } debugConfiguration['dlvToolPath'] = getBinPath('dlv'); if (!path.isAbsolute(debugConfiguration['dlvToolPath'])) { From 70a33e8690abea0228ff1101ec95a153a4ea4e72 Mon Sep 17 00:00:00 2001 From: Joel Hendrix Date: Wed, 27 Mar 2019 09:49:28 -0700 Subject: [PATCH 2/6] refactor based on feedback --- src/debugAdapter/goDebug.ts | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/debugAdapter/goDebug.ts b/src/debugAdapter/goDebug.ts index 498893a16..ef6261e6b 100644 --- a/src/debugAdapter/goDebug.ts +++ b/src/debugAdapter/goDebug.ts @@ -51,7 +51,7 @@ enum GoReflectKind { } // These types should stay in sync with: -// https://github.com/derekparker/delve/blob/master/service/api/types.go +// https://github.com/go-delve/delve/blob/master/service/api/types.go interface CommandOut { State: DebuggerState; @@ -201,7 +201,7 @@ interface DiscardedBreakpoint { // This interface should always match the schema found in `package.json`. interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments { - kind: 'launch'; + request: 'launch'; [key: string]: any; program: string; stopOnEntry?: boolean; @@ -234,7 +234,7 @@ interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments { } interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments { - kind: 'attach'; + request: 'attach'; processId?: number; stopOnEntry?: boolean; showLog?: boolean; @@ -302,10 +302,10 @@ class Delve { isApiV1: boolean; dlvEnv: any; stackTraceDepth: number; - kind: 'attach' | 'launch'; + request: 'attach' | 'launch'; constructor(launchArgs: LaunchRequestArguments | AttachRequestArguments, program: string) { - this.kind = launchArgs.kind; + this.request = launchArgs.request; this.program = normalizePath(program); this.remotePath = launchArgs.remotePath; this.isApiV1 = false; @@ -325,7 +325,7 @@ class Delve { return; } let env: NodeJS.ProcessEnv; - if (launchArgs.kind === 'launch') { + if (launchArgs.request === 'launch') { let isProgramDirectory = false; // Validations on the program if (!program) { @@ -410,8 +410,8 @@ class Delve { this.noDebug = false; // Get default LoadConfig values according to delve API: - // https://github.com/derekparker/delve/blob/c5c41f635244a22d93771def1c31cf1e0e9a2e63/service/rpc1/server.go#L13 - // https://github.com/derekparker/delve/blob/c5c41f635244a22d93771def1c31cf1e0e9a2e63/service/rpc2/server.go#L423 + // 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, @@ -432,7 +432,7 @@ class Delve { } else if (currentGOWorkspace && env['GO111MODULE'] !== 'on') { dlvArgs.push(dirname.substr(currentGOWorkspace.length + 1)); } - dlvArgs.push('--headless=true', '--listen=' + launchArgs.host + ':' + launchArgs.port.toString()); + dlvArgs.push('--headless=true', `--listen=${launchArgs.host}:${launchArgs.port}`); if (!this.isApiV1) { dlvArgs.push('--api-version=2'); } @@ -462,13 +462,13 @@ class Delve { dlvArgs.push('--', ...launchArgs.args); } this.localDebugeePath = this.getLocalDebugeePath(launchArgs.output); - } else if (launchArgs.kind === 'attach') { + } 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/derekparker/delve & ensure it is in your Go tools path, "GOPATH/bin" or "PATH".`); + 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}`); @@ -575,7 +575,7 @@ class Delve { } log('HaltRequest'); - const isLocalDebugging: boolean = this.kind === 'launch' && !!this.debugProcess; + const isLocalDebugging: boolean = this.request === 'launch' && !!this.debugProcess; const forceCleanup = async () => { killTree(this.debugProcess.pid); await removeFile(this.localDebugeePath); @@ -691,9 +691,9 @@ class GoDebugSession extends LoggingDebugSession { } let localPath: string; - if (args.kind === 'attach') { + if (args.request === 'attach') { localPath = args.cwd; - } else if (args.kind === 'launch') { + } else if (args.request === 'launch') { localPath = args.program; } if (!args.remotePath) { @@ -762,7 +762,6 @@ class GoDebugSession extends LoggingDebugSession { } protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { - args.kind = 'launch'; if (!args.program) { this.sendErrorResponse(response, 3000, 'Failed to continue: The program attribute is missing in the debug configuration in launch.json'); return; @@ -771,7 +770,6 @@ class GoDebugSession extends LoggingDebugSession { } protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): void { - args.kind = 'attach'; if (args.mode === 'attach' && !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 === 'connect' && !args.port) { @@ -1233,7 +1231,7 @@ class GoDebugSession extends LoggingDebugSession { }, err => logError('Failed to evaluate expression - ' + err.toString())); } }; - // expressions passed to loadChildren defined per https://github.com/derekparker/delve/blob/master/Documentation/api/ClientHowto.md#loading-more-of-a-variable + // 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 => { From e1fc4f8739d9c7caa854aea074216fdf2700bbcf Mon Sep 17 00:00:00 2001 From: Joel Hendrix Date: Fri, 29 Mar 2019 15:31:18 -0700 Subject: [PATCH 3/6] clean up handling of legacy useApiV1 per feedback --- src/goDebugConfiguration.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/goDebugConfiguration.ts b/src/goDebugConfiguration.ts index fb82f26db..5596f6097 100644 --- a/src/goDebugConfiguration.ts +++ b/src/goDebugConfiguration.ts @@ -70,10 +70,14 @@ export class GoDebugConfigurationProvider implements vscode.DebugConfigurationPr }); const dlvConfig: { [key: string]: any } = goConfig.get('delveConfig'); - if (!debugConfiguration.hasOwnProperty('useApiV1') && dlvConfig.hasOwnProperty('useApiV1')) { - if (typeof dlvConfig['useApiV1'] === 'boolean' && dlvConfig['useApiV1'] === true) { - debugConfiguration['apiVersion'] = 1; - } + let useApiV1 = false; + if (debugConfiguration.hasOwnProperty('useApiV1')) { + useApiV1 = debugConfiguration['useApiV1'] === true; + } else if (dlvConfig.hasOwnProperty('useApiV1')) { + useApiV1 = dlvConfig['useApiV1'] === true; + } + if (useApiV1) { + debugConfiguration['apiVersion'] = 1; } if (!debugConfiguration.hasOwnProperty('apiVersion') && dlvConfig.hasOwnProperty('apiVersion')) { debugConfiguration['apiVersion'] = dlvConfig['apiVersion']; From ed835845edf21411a1feb2e95af01687fc15d051 Mon Sep 17 00:00:00 2001 From: Joel Hendrix Date: Wed, 12 Jun 2019 15:27:22 -0700 Subject: [PATCH 4/6] renamed attach to local, connect to remote --- package.json | 14 +++++++------- src/debugAdapter/goDebug.ts | 16 ++++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index f81bdca03..c24488669 100644 --- a/package.json +++ b/package.json @@ -371,13 +371,13 @@ } }, { - "label": "Go: Attach to process", + "label": "Go: Attach to local process", "description": "Attach to an existing process by process ID", "body": { "name": "${1:Attach to Process}", "type": "go", "request": "attach", - "mode": "attach", + "mode": "local", "processId": 0 } }, @@ -388,7 +388,7 @@ "name": "${1:Connect to server}", "type": "go", "request": "attach", - "mode": "connect", + "mode": "remote", "remotePath": "^\"\\${workspaceFolder}\"", "port": 2345, "host": "127.0.0.1" @@ -577,11 +577,11 @@ }, "mode": { "enum": [ - "attach", - "connect" + "local", + "remote" ], - "description": "One of 'auto', 'debug', 'remote', 'test', 'exec'.", - "default": "auto" + "description": "Indicates local or remote debugging. Local maps to the dlv 'attach' command, remote maps to 'connect'.", + "default": "local" }, "stopOnEntry": { "type": "boolean", diff --git a/src/debugAdapter/goDebug.ts b/src/debugAdapter/goDebug.ts index ef6261e6b..a7dee31d8 100644 --- a/src/debugAdapter/goDebug.ts +++ b/src/debugAdapter/goDebug.ts @@ -210,7 +210,7 @@ interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments { logOutput?: string; cwd?: string; env?: { [key: string]: string; }; - mode?: string; + mode?: 'auto' | 'debug' | 'remote' | 'test' | 'exec'; remotePath?: string; port?: number; host?: string; @@ -240,7 +240,7 @@ interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments { showLog?: boolean; logOutput?: string; cwd?: string; - mode?: string; + mode?: 'local' | 'remote'; remotePath?: string; port?: number; host?: string; @@ -318,7 +318,7 @@ class Delve { let dlvCwd = dirname(program); let serverRunning = false; const dlvArgs = new Array(); - if (mode === 'remote' || mode === 'connect') { + if (mode === 'remote') { this.debugProcess = null; serverRunning = true; // assume server is running when in remote mode connectClient(launchArgs.port, launchArgs.host); @@ -560,9 +560,9 @@ class Delve { * 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 an existing process by ID or connecting to an - * existing delve server. For debug-attach we start the delve process so will also terminate it however we - * detach from the debugee without killing it. For debug-connect we only detach from delve. + * 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 @@ -770,9 +770,9 @@ class GoDebugSession extends LoggingDebugSession { } protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): void { - if (args.mode === 'attach' && !args.processId) { + 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 === 'connect' && !args.port) { + } 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); From 5402218538a8bb6a29916c859505b7913d5203cc Mon Sep 17 00:00:00 2001 From: Joel Hendrix Date: Wed, 12 Jun 2019 15:53:54 -0700 Subject: [PATCH 5/6] show warning when using deprecated launch-remote --- src/goDebugConfiguration.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/goDebugConfiguration.ts b/src/goDebugConfiguration.ts index 5596f6097..b4d05e22d 100644 --- a/src/goDebugConfiguration.ts +++ b/src/goDebugConfiguration.ts @@ -4,6 +4,7 @@ import vscode = require('vscode'); import path = require('path'); import { getCurrentGoPath, getToolsEnvVars, sendTelemetryEvent, getBinPath } from './util'; import { promptForMissingTool } from './goInstallTools'; +import { getFromGlobalState, updateGlobalState } from './stateUtils'; export class GoDebugConfigurationProvider implements vscode.DebugConfigurationProvider { @@ -103,6 +104,16 @@ export class GoDebugConfigurationProvider implements vscode.DebugConfigurationPr } debugConfiguration['currentFile'] = activeEditor && activeEditor.document.languageId === 'go' && activeEditor.document.fileName; + const neverAgain = { title: 'Don\'t Show Again' }; + const ignoreWarningKey = 'ignoreDebugLaunchRemoteWarning'; + const ignoreWarning = getFromGlobalState(ignoreWarningKey); + if (ignoreWarning !== true && debugConfiguration.request === 'launch' && debugConfiguration['mode'] === 'remote') { + vscode.window.showWarningMessage('Request type of \'launch\' with mode \'remote\' is deprecated, please use request type \'attach\' with mode \'remote\' instead.', neverAgain).then(result => { + if (result === neverAgain) { + updateGlobalState(ignoreWarningKey, true); + } + }); + } return debugConfiguration; } From a954f7fdf80b8af0eff9e99cf44ee35c15e1b29e Mon Sep 17 00:00:00 2001 From: Joel Hendrix Date: Thu, 13 Jun 2019 06:43:48 -0700 Subject: [PATCH 6/6] only set cwd for attach requests --- src/goDebugConfiguration.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/goDebugConfiguration.ts b/src/goDebugConfiguration.ts index b4d05e22d..3d6f6d7be 100644 --- a/src/goDebugConfiguration.ts +++ b/src/goDebugConfiguration.ts @@ -5,6 +5,7 @@ import path = require('path'); import { getCurrentGoPath, getToolsEnvVars, sendTelemetryEvent, getBinPath } from './util'; import { promptForMissingTool } from './goInstallTools'; import { getFromGlobalState, updateGlobalState } from './stateUtils'; +import { debug } from 'util'; export class GoDebugConfigurationProvider implements vscode.DebugConfigurationProvider { @@ -89,7 +90,7 @@ export class GoDebugConfigurationProvider implements vscode.DebugConfigurationPr if (!debugConfiguration.hasOwnProperty('showGlobalVariables') && dlvConfig.hasOwnProperty('showGlobalVariables')) { debugConfiguration['showGlobalVariables'] = dlvConfig['showGlobalVariables']; } - if (!debugConfiguration['cwd']) { + if (debugConfiguration.request === 'attach' && !debugConfiguration['cwd']) { debugConfiguration['cwd'] = '${workspaceFolder}'; }