diff --git a/src/debugAdapter/goDebug.ts b/src/debugAdapter/goDebug.ts index 35447a552..811cf4a67 100644 --- a/src/debugAdapter/goDebug.ts +++ b/src/debugAdapter/goDebug.ts @@ -132,6 +132,7 @@ interface DebugThread { id: number; line: number; pc: number; + goroutineID: number; function?: DebugFunction; } @@ -153,6 +154,7 @@ interface DebugFunction { goType: number; args: DebugVariable[]; locals: DebugVariable[]; + optimized: boolean; } interface ListVarsOut { @@ -700,7 +702,7 @@ class Delve { await this.callPromise('Detach', [this.isApiV1 ? true : { Kill: isLocalDebugging }]); } catch (err) { log('DetachResponse'); - logError(`Failed to detach - ${err.toString() || ''}`); + logError(err, 'Failed to detach'); shouldForceClean = isLocalDebugging; } } @@ -874,7 +876,7 @@ class GoDebugSession extends LoggingDebugSession { // We use NonBlocking so the call would return immediately. this.debugState = await this.delve.getDebugState(); } catch (error) { - logError(`Failed to get state ${String(error)}`); + this.logDelveError(error, 'Failed to get state'); } if (!this.debugState.Running && !this.continueRequestRunning) { @@ -885,15 +887,13 @@ class GoDebugSession extends LoggingDebugSession { () => { return this.setBreakPoints(response, args).then(() => { return this.continue(true).then(null, (err) => { - logError( - `Failed to continue delve after halting it to set breakpoints: "${err.toString()}"` - ); + this.logDelveError(err, 'Failed to continue delve after halting it to set breakpoints'); }); }); }, (err) => { this.skipStopEventOnce = false; - logError(err); + this.logDelveError(err, 'Failed to halt delve before attempting to set breakpoint'); return this.sendErrorResponse( response, 2008, @@ -921,7 +921,7 @@ class GoDebugSession extends LoggingDebugSession { } if (err) { - logError('Failed to get threads - ' + err.toString()); + this.logDelveError(err, 'Failed to get threads'); return this.sendErrorResponse(response, 2003, 'Unable to display threads: "{e}"', { e: err.toString() }); @@ -963,7 +963,7 @@ class GoDebugSession extends LoggingDebugSession { [stackTraceIn], (err, out) => { if (err) { - logError('Failed to produce stack trace!'); + this.logDelveError(err, 'Failed to produce stacktrace'); return this.sendErrorResponse(response, 2004, 'Unable to produce stack trace: "{e}"', { e: err.toString() }); @@ -1004,7 +1004,7 @@ class GoDebugSession extends LoggingDebugSession { this.delve.isApiV1 ? [listLocalVarsIn] : [{ scope: listLocalVarsIn, cfg: this.delve.loadConfig }], (err, out) => { if (err) { - logError('Failed to list local variables - ' + err.toString()); + this.logDelveError(err, 'Failed to get list local variables'); return this.sendErrorResponse(response, 2005, 'Unable to list locals: "{e}"', { e: err.toString() }); @@ -1020,7 +1020,7 @@ class GoDebugSession extends LoggingDebugSession { : [{ scope: listLocalFunctionArgsIn, cfg: this.delve.loadConfig }], (listFunctionErr, outArgs) => { if (listFunctionErr) { - logError('Failed to list function args - ' + listFunctionErr.toString()); + this.logDelveError(listFunctionErr, 'Failed to list function args'); return this.sendErrorResponse(response, 2006, 'Unable to list args: "{e}"', { e: listFunctionErr.toString() }); @@ -1100,7 +1100,7 @@ class GoDebugSession extends LoggingDebugSession { this.delve.isApiV1 ? [filter] : [{ filter, cfg: this.delve.loadConfig }], (listPkgVarsErr, listPkgVarsOut) => { if (listPkgVarsErr) { - logError('Failed to list global vars - ' + listPkgVarsErr.toString()); + this.logDelveError(listPkgVarsErr, 'Failed to list global vars'); return this.sendErrorResponse( response, 2007, @@ -1172,7 +1172,7 @@ class GoDebugSession extends LoggingDebugSession { const variable = this.delve.isApiV1 ? result : (result).Variable; v.children = variable.children; }, - (err) => logError('Failed to evaluate expression - ' + err.toString()) + (err) => this.logDelveError(err, 'Failed to evaluate expression') ); } }; @@ -1251,7 +1251,7 @@ class GoDebugSession extends LoggingDebugSession { log('NextRequest'); this.delve.call('Command', [{ name: 'next' }], (err, out) => { if (err) { - logError('Failed to next - ' + err.toString()); + this.logDelveError(err, 'Failed to next'); } const state = this.delve.isApiV1 ? out : (out).State; log('next state', state); @@ -1266,7 +1266,7 @@ class GoDebugSession extends LoggingDebugSession { log('StepInRequest'); this.delve.call('Command', [{ name: 'step' }], (err, out) => { if (err) { - logError('Failed to step - ' + err.toString()); + this.logDelveError(err, 'Failed to step in'); } const state = this.delve.isApiV1 ? out : (out).State; log('stop state', state); @@ -1281,7 +1281,7 @@ class GoDebugSession extends LoggingDebugSession { log('StepOutRequest'); this.delve.call('Command', [{ name: 'stepOut' }], (err, out) => { if (err) { - logError('Failed to stepout - ' + err.toString()); + this.logDelveError(err, 'Failed to step out'); } const state = this.delve.isApiV1 ? out : (out).State; log('stepout state', state); @@ -1296,7 +1296,7 @@ class GoDebugSession extends LoggingDebugSession { log('PauseRequest'); this.delve.call('Command', [{ name: 'halt' }], (err, out) => { if (err) { - logError('Failed to halt - ' + err.toString()); + this.logDelveError(err, 'Failed to halt'); return this.sendErrorResponse(response, 2010, 'Unable to halt execution: "{e}"', { e: err.toString() }); @@ -1345,7 +1345,7 @@ class GoDebugSession extends LoggingDebugSession { this.delve.call(this.delve.isApiV1 ? 'SetSymbol' : 'Set', [setSymbolArgs], (err) => { if (err) { const errMessage = `Failed to set variable: ${err.toString()}`; - logError(errMessage); + this.logDelveError(err, 'Failed to set variable'); return this.sendErrorResponse(response, 2010, errMessage); } response.body = { value: args.value }; @@ -1743,7 +1743,7 @@ class GoDebugSession extends LoggingDebugSession { // [TODO] Can we avoid doing this? https://github.com/Microsoft/vscode/issues/40#issuecomment-161999881 this.delve.call('ListGoroutines', [], (err, out) => { if (err) { - logError('Failed to get threads - ' + err.toString()); + this.logDelveError(err, 'Failed to get threads'); } const goroutines = this.delve.isApiV1 ? out : (out).Goroutines; this.updateGoroutinesList(goroutines); @@ -1783,7 +1783,7 @@ class GoDebugSession extends LoggingDebugSession { if (!calledWhenSettingBreakpoint) { errorCallback = (err: any) => { if (err) { - logError('Failed to continue - ' + err.toString()); + this.logDelveError(err, 'Failed to continue'); } this.handleReenterDebug('breakpoint'); throw err; @@ -1840,6 +1840,87 @@ class GoDebugSession extends LoggingDebugSession { }); }); } + + private logDelveError(err: any, message: string) { + if (err === undefined) { + return; + } + + let errorMessage = err.toString(); + // Handle unpropagated fatalpanic errors with a more user friendly message: + // 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, only handle the error + if (errorMessage === 'bad access') { + errorMessage = 'unpropagated fatalpanic: signal SIGSEGV (EXC_BAD_ACCESS). This fatalpanic is not traceable on macOS, see https://github.com/go-delve/delve/issues/852'; + } + + logError(message + ' - ' + errorMessage); + + if (errorMessage === 'bad access') { + logError('WARNING: this stack might not be from the expected active goroutine'); + } + + 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( + this.delve.isApiV1 ? + 'StacktraceGoroutine' : 'Stacktrace', [stackTraceIn], (err, out) => { + if (err) { + logError('dumpStacktrace: Failed to produce stack trace' + err); + return; + } + const locations = this.delve.isApiV1 ? out : (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 : '', + location.file === '' ? 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); + }); + } } function random(low: number, high: number): number { diff --git a/src/goLanguageServer.ts b/src/goLanguageServer.ts index 06793132a..357ecf6df 100644 --- a/src/goLanguageServer.ts +++ b/src/goLanguageServer.ts @@ -12,6 +12,7 @@ import semver = require('semver'); import util = require('util'); import vscode = require('vscode'); import { + Command, FormattingOptions, HandleDiagnosticsSignature, LanguageClient, @@ -132,6 +133,55 @@ export async function registerLanguageFeatures(ctx: vscode.ExtensionContext) { return null; } return next(document, token); + }, + provideCompletionItem: ( + document: vscode.TextDocument, + position: vscode.Position, + context: vscode.CompletionContext, + token: vscode.CancellationToken, + next: ProvideCompletionItemsSignature + ) => { + // TODO(hyangah): when v1.42+ api is available, we can simplify + // language-specific configuration lookup using the new + // ConfigurationScope. + // const paramHintsEnabled = vscode.workspace.getConfiguration( + // 'editor.parameterHints', + // { languageId: 'go', uri: document.uri }); + + const editorParamHintsEnabled = vscode.workspace.getConfiguration( + 'editor.parameterHints', document.uri)['enabled']; + const goParamHintsEnabled = vscode.workspace.getConfiguration( + '[go]', document.uri)['editor.parameterHints.enabled']; + + let paramHintsEnabled: boolean = false; + if (typeof goParamHintsEnabled === 'undefined') { + paramHintsEnabled = editorParamHintsEnabled; + } else { + paramHintsEnabled = goParamHintsEnabled; + } + let cmd: Command; + if (paramHintsEnabled) { + cmd = { title: 'triggerParameterHints', command: 'editor.action.triggerParameterHints' }; + } + + function configureCommands( + r: vscode.CompletionItem[] | vscode.CompletionList | null | undefined): + vscode.CompletionItem[] | vscode.CompletionList | null | undefined { + if (r) { + (Array.isArray(r) ? r : r.items).forEach((i: vscode.CompletionItem) => { + i.command = cmd; + }); + } + return r; + } + const ret = next(document, position, context, token); + + const isThenable = (obj: vscode.ProviderResult): obj is Thenable => obj && (obj)['then']; + if (isThenable(ret)) { + return ret.then(configureCommands); + } + return configureCommands(ret); + } } }