diff --git a/src/commands/startLanguageServer.ts b/src/commands/startLanguageServer.ts index 6c672fb5b9..f01d1f97c4 100644 --- a/src/commands/startLanguageServer.ts +++ b/src/commands/startLanguageServer.ts @@ -30,7 +30,7 @@ const languageServerStartMutex = new Mutex(); export const startLanguageServer: CommandFactory = (ctx, goCtx) => { return async (reason: RestartReason = RestartReason.MANUAL) => { const goConfig = getGoConfig(); - const cfg = buildLanguageServerConfig(goConfig); + const cfg = await buildLanguageServerConfig(goConfig); if (typeof reason === 'string') { updateRestartHistory(goCtx, reason, cfg.enabled); @@ -42,6 +42,7 @@ export const startLanguageServer: CommandFactory = (ctx, goCtx) => { if (reason === RestartReason.MANUAL) { await suggestGoplsIssueReport( goCtx, + cfg, "Looks like you're about to manually restart the language server.", errorKind.manualRestart ); diff --git a/src/goCheck.ts b/src/goCheck.ts index ff8113677f..c71574b820 100644 --- a/src/goCheck.ts +++ b/src/goCheck.ts @@ -11,7 +11,6 @@ import path = require('path'); import vscode = require('vscode'); import { getGoplsConfig } from './config'; import { goBuild } from './goBuild'; -import { buildLanguageServerConfig } from './language/goLanguageServer'; import { goLint } from './goLint'; import { isModSupported } from './goModules'; import { diagnosticsStatusBarItem, outputChannel } from './goStatus'; @@ -68,8 +67,7 @@ export function check( // If a user has enabled diagnostics via a language server, // then we disable running build or vet to avoid duplicate errors and warnings. - const lspConfig = buildLanguageServerConfig(goConfig); - const disableBuildAndVet = lspConfig.enabled; + const disableBuildAndVet = goConfig.get('useLanguageServer'); let testPromise: Thenable; const testConfig: TestConfig = { diff --git a/src/goStatus.ts b/src/goStatus.ts index 01b12240f3..6ce338b514 100644 --- a/src/goStatus.ts +++ b/src/goStatus.ts @@ -11,7 +11,6 @@ import vscode = require('vscode'); import vscodeUri = require('vscode-uri'); import { getGoConfig } from './config'; import { formatGoVersion, GoEnvironmentOption, terminalCreationListener } from './goEnvironmentStatus'; -import { buildLanguageServerConfig, getLocalGoplsVersion } from './language/goLanguageServer'; import { isGoFile } from './goMode'; import { isModSupported, runGoEnv } from './goModules'; import { allToolsInformation } from './goToolsInformation'; @@ -61,14 +60,16 @@ export const expandGoStatusBar: CommandFactory = (ctx, goCtx) => async () => { { label: 'Choose Go Environment' } ]; - // Get the gopls configuration + const cfg = goCtx.latestConfig; + // Get the gopls configuration. const goConfig = getGoConfig(); - const cfg = buildLanguageServerConfig(goConfig); - if (languageServerIsRunning && cfg.serverName === 'gopls') { - const goplsVersion = await getLocalGoplsVersion(cfg); + const goplsIsRunning = languageServerIsRunning && cfg && cfg.serverName === 'gopls'; + if (goplsIsRunning) { + const goplsVersion = cfg.version; options.push({ label: `${languageServerIcon}Open 'gopls' trace`, description: `${goplsVersion?.version}` }); } - if (!languageServerIsRunning && !cfg.serverName && goConfig['useLanguageServer'] === true) { + // In case gopls still need to be installed, cfg.serverName will be empty. + if (!goplsIsRunning && goConfig.get('useLanguageServer') === true && cfg?.serverName === '') { options.push({ label: 'Install Go Language Server', description: `${languageServerErrorIcon}'gopls' is required but missing` diff --git a/src/language/goLanguageServer.ts b/src/language/goLanguageServer.ts index 60ee6ed27b..ea2c3c2b16 100644 --- a/src/language/goLanguageServer.ts +++ b/src/language/goLanguageServer.ts @@ -158,9 +158,6 @@ export function scheduleGoplsSuggestions(goCtx: GoExtensionContext) { return; } // Some helper functions. - const usingGopls = (cfg: LanguageServerConfig): boolean => { - return cfg.enabled && cfg.serverName === 'gopls'; - }; const usingGo = (): boolean => { return vscode.workspace.textDocuments.some((doc) => doc.languageId === 'go'); }; @@ -187,20 +184,11 @@ export function scheduleGoplsSuggestions(goCtx: GoExtensionContext) { }; const update = async () => { setTimeout(update, timeDay); - - let cfg = buildLanguageServerConfig(getGoConfig()); - if (!usingGopls(cfg)) { - // This shouldn't happen, but if the user has a non-gopls language - // server enabled, we shouldn't prompt them to change. - if (cfg.serverName !== '' && cfg.serverName !== 'gopls') { - return; - } - // Check if the language server has now been enabled, and if so, - // it will be installed below. - cfg = buildLanguageServerConfig(getGoConfig()); - if (!cfg.enabled) { - return; - } + const cfg = goCtx.latestConfig; + // trigger periodic update check only if the user is already using gopls. + // Otherwise, let's check again tomorrow. + if (!cfg || !cfg.enabled || cfg.serverName !== 'gopls') { + return; } await installGopls(cfg); }; @@ -300,14 +288,6 @@ export const flushGoplsOptOutConfig = (cfg: GoplsOptOutConfig, workspace: boolea updateGlobalState(goplsOptOutConfigKey, JSON.stringify(cfg)); }; -const race = function (promise: Promise, timeoutInMilliseconds: number) { - let token: NodeJS.Timeout; - const timeout = new Promise((resolve, reject) => { - token = setTimeout(() => reject('timeout'), timeoutInMilliseconds); - }); - return Promise.race([promise, timeout]).then(() => clearTimeout(token)); -}; - // exported for testing. export async function stopLanguageClient(goCtx: GoExtensionContext) { const c = goCtx.languageClient; @@ -322,10 +302,8 @@ export async function stopLanguageClient(goCtx: GoExtensionContext) { // LanguageClient.stop may hang if the language server // crashes during shutdown before responding to the // shutdown request. Enforce client-side timeout. - // TODO(hyangah): replace with the new LSP client API that supports timeout - // and remove this. try { - await race(c.stop(), 2000); + c.stop(2000); } catch (e) { c.outputChannel?.appendLine(`Failed to stop client: ${e}`); } @@ -416,6 +394,7 @@ export async function buildLanguageClient( goCtx: GoExtensionContext, cfg: BuildLanguageClientOption ): Promise { + await getLocalGoplsVersion(cfg); // populate and cache cfg.version const goplsWorkspaceConfig = await adjustGoplsWorkspaceConfiguration(cfg, getGoplsConfig(), 'gopls', undefined); const documentSelector = [ @@ -435,7 +414,7 @@ export async function buildLanguageClient( const pendingVulncheckProgressToken = new Map(); const onDidChangeVulncheckResultEmitter = new vscode.EventEmitter(); - + // cfg is captured by closures for later use during error report. const c = new GoLanguageClient( 'go', // id cfg.serverName, // name e.g. gopls @@ -470,6 +449,7 @@ export async function buildLanguageClient( }; } return { + message: '', // suppresses error popups action: ErrorAction.Shutdown }; }, @@ -477,6 +457,7 @@ export async function buildLanguageClient( if (initializationError !== undefined) { suggestGoplsIssueReport( goCtx, + cfg, 'The gopls server failed to initialize.', errorKind.initializationFailure, initializationError @@ -484,7 +465,7 @@ export async function buildLanguageClient( initializationError = undefined; // In case of initialization failure, do not try to restart. return { - message: 'The gopls server failed to initialize.', + message: '', // suppresses error popups - there will be other popups. :-( action: CloseAction.DoNotRestart }; } @@ -500,11 +481,13 @@ export async function buildLanguageClient( } suggestGoplsIssueReport( goCtx, + cfg, 'The connection to gopls has been closed. The gopls server may have crashed.', errorKind.crash ); updateLanguageServerIconGoStatusBar(false, true); return { + message: '', // suppresses error popups - there will be other popups. action: CloseAction.DoNotRestart }; } @@ -968,16 +951,16 @@ export async function watchLanguageServerConfiguration(goCtx: GoExtensionContext } } -export function buildLanguageServerConfig(goConfig: vscode.WorkspaceConfiguration): LanguageServerConfig { +export async function buildLanguageServerConfig( + goConfig: vscode.WorkspaceConfiguration +): Promise { let formatter: GoDocumentFormattingEditProvider | undefined; if (usingCustomFormatTool(goConfig)) { formatter = new GoDocumentFormattingEditProvider(); } const cfg: LanguageServerConfig = { - serverName: '', + serverName: '', // remain empty if gopls binary can't be found. path: '', - version: undefined, // compute version lazily - modtime: undefined, enabled: goConfig['useLanguageServer'] === true, flags: goConfig['languageServerFlags'] || [], features: { @@ -1015,7 +998,7 @@ Please try reinstalling it.`); return cfg; } cfg.modtime = stats.mtime; - + cfg.version = await getLocalGoplsVersion(cfg); return cfg; } @@ -1208,13 +1191,13 @@ interface GoplsVersionOutput { // If this command has already been executed, it returns the saved result. export const getLocalGoplsVersion = async (cfg?: LanguageServerConfig) => { if (!cfg) { - return null; + return; } if (cfg.version) { return cfg.version; } if (cfg.path === '') { - return null; + return; } const env = toolExecutionEnvironment(); const cwd = getWorkspaceFolderPath(); @@ -1240,7 +1223,7 @@ export const getLocalGoplsVersion = async (cfg?: LanguageServerConfig) => { } catch (e) { // The "gopls version" command is not supported, or something else went wrong. // TODO: Should we propagate this error? - return null; + return; } const lines = output.trim().split('\n'); @@ -1248,17 +1231,17 @@ export const getLocalGoplsVersion = async (cfg?: LanguageServerConfig) => { case 0: // No results, should update. // Worth doing anything here? - return null; + return; case 1: // Built in $GOPATH mode. Should update. // TODO: Should we check the Go version here? // Do we even allow users to enable gopls if their Go version is too low? - return null; + return; case 2: // We might actually have a parseable version. break; default: - return null; + return; } // The second line should be the sum line. @@ -1277,7 +1260,7 @@ export const getLocalGoplsVersion = async (cfg?: LanguageServerConfig) => { // const split = moduleVersion.trim().split('@'); if (split.length < 2) { - return null; + return; } // The version comes after the @ symbol: // @@ -1326,15 +1309,26 @@ export enum errorKind { // suggestGoplsIssueReport prompts users to file an issue with gopls. export async function suggestGoplsIssueReport( goCtx: GoExtensionContext, + cfg: LanguageServerConfig, // config used when starting this gopls. msg: string, reason: errorKind, initializationError?: WebRequest.ResponseError ) { + const issueTime = new Date(); + // Don't prompt users who manually restart to file issues until gopls/v1.0. if (reason === errorKind.manualRestart) { return; } + // cfg is the config used when starting this crashed gopls instance, while + // goCtx.latestConfig is the config used by the latest gopls instance. + // They may be different if gopls upgrade occurred in between. + // Let's not report issue yet if they don't match. + if (JSON.stringify(goCtx.latestConfig?.version) !== JSON.stringify(cfg.version)) { + return; + } + // The user may have an outdated version of gopls, in which case we should // just prompt them to update, not file an issue. const tool = getTool('gopls'); @@ -1378,15 +1372,16 @@ export async function suggestGoplsIssueReport( if (failureReason === GoplsFailureModes.INCORRECT_COMMAND_USAGE) { const languageServerFlags = getGoConfig()['languageServerFlags'] as string[]; if (languageServerFlags && languageServerFlags.length > 0) { - selected = await vscode.window.showInformationMessage( + selected = await vscode.window.showErrorMessage( `The extension was unable to start the language server. You may have an invalid value in your "go.languageServerFlags" setting. -It is currently set to [${languageServerFlags}]. Please correct the setting by navigating to Preferences -> Settings.`, - 'Open settings', +It is currently set to [${languageServerFlags}]. +Please correct the setting.`, + 'Open Settings', 'I need more help.' ); switch (selected) { - case 'Open settings': + case 'Open Settings': await vscode.commands.executeCommand('workbench.action.openSettings', 'go.languageServerFlags'); return; case 'I need more help': @@ -1395,7 +1390,8 @@ It is currently set to [${languageServerFlags}]. Please correct the setting by n } } } - selected = await vscode.window.showInformationMessage( + const showMessage = sanitizedLog ? vscode.window.showWarningMessage : vscode.window.showInformationMessage; + selected = await showMessage( `${msg} Would you like to report a gopls issue on GitHub? You will be asked to provide additional information and logs, so PLEASE READ THE CONTENT IN YOUR BROWSER.`, 'Yes', @@ -1415,11 +1411,9 @@ You will be asked to provide additional information and logs, so PLEASE READ THE errKind = 'initialization'; break; } - // Get the user's version in case the update prompt above failed. - const usersGoplsVersion = await getLocalGoplsVersion(goCtx.latestConfig); - const goVersion = await getGoVersion(); const settings = goCtx.latestConfig.flags.join(' '); const title = `gopls: automated issue report (${errKind})`; + const goplsStats = await getGoplsStats(goCtx.latestConfig?.path); const goplsLog = sanitizedLog ? `
${sanitizedLog}
` : `Please attach the stack trace from the crash. @@ -1430,17 +1424,15 @@ Please copy the stack trace and error messages from that window and paste it in Failed to auto-collect gopls trace: ${failureReason}. `; - const now = new Date(); const body = ` -gopls version: ${usersGoplsVersion?.version} (${usersGoplsVersion?.goVersion}) +gopls version: ${cfg.version?.version}/${cfg.version?.goVersion} gopls flags: ${settings} -update flags: ${goCtx.latestConfig.checkForUpdates} +update flags: ${cfg.checkForUpdates} extension version: ${extensionInfo.version} -go version: ${goVersion?.format(true)} environment: ${extensionInfo.appName} ${process.platform} initialization error: ${initializationError} -issue timestamp: ${now.toUTCString()} +issue timestamp: ${issueTime.toUTCString()} restart history: ${formatRestartHistory(goCtx)} @@ -1452,6 +1444,10 @@ Describe what you observed. ${goplsLog} +
gopls stats -anon +${goplsStats} +
+ OPTIONAL: If you would like to share more information, you can attach your complete gopls logs. NOTE: THESE MAY CONTAIN SENSITIVE INFORMATION ABOUT YOUR CODEBASE. @@ -1459,7 +1455,7 @@ DO NOT SHARE LOGS IF YOU ARE WORKING IN A PRIVATE REPOSITORY. `; - const url = `https://github.com/golang/vscode-go/issues/new?title=${title}&labels=upstream-tools&body=${body}`; + const url = `https://github.com/golang/vscode-go/issues/new?title=${title}&labels=automatedReport&body=${body}`; await vscode.env.openExternal(vscode.Uri.parse(url)); } break; @@ -1525,12 +1521,10 @@ async function collectGoplsLog(goCtx: GoExtensionContext): Promise<{ sanitizedLo if (doc.isDirty || doc.isClosed) { continue; } - // The document's name should look like 'extension-output-#X'. - if (doc.fileName.indexOf('extension-output-') === -1) { - continue; + if (doc.fileName.indexOf('gopls (server)') > -1) { + logs = doc.getText(); + break; } - logs = doc.getText(); - break; } if (logs) { break; @@ -1538,7 +1532,6 @@ async function collectGoplsLog(goCtx: GoExtensionContext): Promise<{ sanitizedLo // sleep a bit before the next try. The choice of the sleep time is arbitrary. await sleep((i + 1) * 100); } - return sanitizeGoplsTrace(logs); } @@ -1588,7 +1581,7 @@ export function sanitizeGoplsTrace(logs?: string): { sanitizedLog?: string; fail } return { failureReason: GoplsFailureModes.INCOMPLETE_PANIC_TRACE }; } - const initFailMsgBegin = logs.lastIndexOf('Starting client failed'); + const initFailMsgBegin = logs.lastIndexOf('gopls client:'); if (initFailMsgBegin > -1) { // client start failed. Capture up to the 'Code:' line. const initFailMsgEnd = logs.indexOf('Code: ', initFailMsgBegin); @@ -1602,9 +1595,16 @@ export function sanitizeGoplsTrace(logs?: string): { sanitizedLog?: string; fail }; } } - if (logs.lastIndexOf('Usage: gopls') > -1) { + if (logs.lastIndexOf('Usage:') > -1) { return { failureReason: GoplsFailureModes.INCORRECT_COMMAND_USAGE }; } + // Capture Fatal + // foo.go:1: the last message (caveat - we capture only the first log line) + const m = logs.match(/(^\S+\.go:\d+:.*$)/gm); + if (m && m.length > 0) { + return { sanitizedLog: m[0].toString() }; + } + return { failureReason: GoplsFailureModes.UNRECOGNIZED_CRASH_PATTERN }; } @@ -1656,3 +1656,22 @@ export function maybePromptForTelemetry(goCtx: GoExtensionContext) { }; callback(); } + +async function getGoplsStats(binpath?: string) { + if (!binpath) { + return 'gopls path unknown'; + } + const env = toolExecutionEnvironment(); + const cwd = getWorkspaceFolderPath(); + const start = new Date(); + const execFile = util.promisify(cp.execFile); + try { + const timeout = 60 * 1000; // 60sec; + const { stdout } = await execFile(binpath, ['stats', '-anon'], { env, cwd, timeout }); + return stdout; + } catch (e) { + const duration = new Date().getTime() - start.getTime(); + console.log(`gopls stats -anon failed: ${JSON.stringify(e)}`); + return `gopls stats -anon failed after running for ${duration}ms`; // e may contain user information. don't include in the report. + } +} diff --git a/src/utils/pathUtils.ts b/src/utils/pathUtils.ts index ead04ac328..ad4509db48 100644 --- a/src/utils/pathUtils.ts +++ b/src/utils/pathUtils.ts @@ -247,7 +247,7 @@ export function fixDriveCasingInWindows(pathToFix: string): string { } /** - * Returns the tool name from the given path to the tool + * Returns the tool name (executable's basename) from the given path to the tool * @param toolPath */ export function getToolFromToolPath(toolPath: string): string | undefined { diff --git a/test/gopls/goplsTestEnv.utils.ts b/test/gopls/goplsTestEnv.utils.ts index a6bc9996f6..95d2d4ef04 100644 --- a/test/gopls/goplsTestEnv.utils.ts +++ b/test/gopls/goplsTestEnv.utils.ts @@ -108,7 +108,7 @@ export class Env { if (!goConfig) { goConfig = getGoConfig(); } - const cfg: BuildLanguageClientOption = buildLanguageServerConfig( + const cfg: BuildLanguageClientOption = await buildLanguageServerConfig( Object.create(goConfig, { useLanguageServer: { value: true }, languageServerFlags: { value: ['-rpc.trace'] } // enable rpc tracing to monitor progress reports diff --git a/test/gopls/report.test.ts b/test/gopls/report.test.ts index dd121e71e6..4cabab95d4 100644 --- a/test/gopls/report.test.ts +++ b/test/gopls/report.test.ts @@ -22,8 +22,8 @@ suite('gopls issue report tests', () => { }, { name: 'initialization error message', - in: traceFromIssueVSCodeGo572, - want: sanitizedTraceFromIssuVSCodeGo572 + in: traceFromIssueVSCodeGo572LSP317, + want: sanitizedTraceFromIssueVSCodeGo572LSP317 }, { name: 'incomplete panic trace', @@ -40,8 +40,8 @@ suite('gopls issue report tests', () => { testCases.map((tc: TestCase) => { const { sanitizedLog, failureReason } = sanitizeGoplsTrace(tc.in); assert.strictEqual( - sanitizedLog, - tc.want, + JSON.stringify(sanitizedLog), + JSON.stringify(tc.want), `sanitizeGoplsTrace(${tc.name}) returned unexpected sanitizedLog result` ); assert.strictEqual( @@ -317,15 +317,21 @@ created by golang.org/x/tools/internal/jsonrpc2.AsyncHandler.func1 handler.go:100 +0x171 [Info - 12:50:26 PM] `; -const traceFromIssueVSCodeGo572 = ` - -[Error - 下午9:23:45] Starting client failed -Message: unsupported URI scheme: (gopls only supports file URIs) -Code: 0 -[Info - 下午9:23:45] 2020/08/25 21:23:45 server shutdown without initialization - +const traceFromIssueVSCodeGo572LSP317 = ` +[Error - 12:20:35 PM] Stopping server failed +Error: Client is not running and can't be stopped. It's current state is: startFailed + at GoLanguageClient.shutdown (/Users/hakim/projects/vscode-go/dist/goMain.js:21702:17) + at GoLanguageClient.stop (/Users/hakim/projects/vscode-go/dist/goMain.js:21679:21) + at GoLanguageClient.stop (/Users/hakim/projects/vscode-go/dist/goMain.js:23486:22) + at GoLanguageClient.handleConnectionError (/Users/hakim/projects/vscode-go/dist/goMain.js:21920:16) + at process.processTicksAndRejections (node:internal/process/task_queues:95:5) +[Error - 12:20:35 PM] +[Error - 12:20:35 PM] gopls client: couldn't create connection to server. + Message: Socket closed before the connection was established + Code: -32099 +Error starting language server: Error: Socket closed before the connection was established <-- this will be included in the initialization error field. `; -const sanitizedTraceFromIssuVSCodeGo572 = `Starting client failed -Message: unsupported URI scheme: (gopls only supports file URIs) -Code: 0`; +const sanitizedTraceFromIssueVSCodeGo572LSP317 = `gopls client: couldn't create connection to server. + Message: Socket closed before the connection was established + Code: -32099 `; diff --git a/test/integration/extension.test.ts b/test/integration/extension.test.ts index b6aa5f72af..ca76d4d4ac 100644 --- a/test/integration/extension.test.ts +++ b/test/integration/extension.test.ts @@ -210,7 +210,7 @@ const testAll = (isModuleMode: boolean) => { ]; // If a user has enabled diagnostics via a language server, // then we disable running build or vet to avoid duplicate errors and warnings. - const lspConfig = buildLanguageServerConfig(getGoConfig()); + const lspConfig = await buildLanguageServerConfig(getGoConfig()); const expectedBuildVetErrors = lspConfig.enabled ? [] : [{ line: 11, severity: 'error', msg: 'undefined: prin' }]; @@ -489,7 +489,7 @@ const testAll = (isModuleMode: boolean) => { }); test('Build Tags checking', async () => { - const goplsConfig = buildLanguageServerConfig(getGoConfig()); + const goplsConfig = await buildLanguageServerConfig(getGoConfig()); if (goplsConfig.enabled) { // Skip this test if gopls is enabled. Build/Vet checks this test depend on are // disabled when the language server is enabled, and gopls is not handling tags yet.