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