diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 27e7d2c6a..e93a22bac 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -108,8 +108,9 @@ "Detected change in telemetry settings. These will not take effect until the language server is restarted, would you like to restart?": "Detected change in telemetry settings. These will not take effect until the language server is restarted, would you like to restart?", "Go to output": "Go to output", "Report Issue": "Report Issue", - "The C# language server has crashed. Restart extensions to re-enable C# functionality.": "The C# language server has crashed. Restart extensions to re-enable C# functionality.", "Restart extensions": "Restart extensions", + "The C# language server was terminated externally. Restart extensions to re-enable C# functionality.": "The C# language server was terminated externally. Restart extensions to re-enable C# functionality.", + "The C# language server has crashed. Restart extensions to re-enable C# functionality.": "The C# language server has crashed. Restart extensions to re-enable C# functionality.", "Restore {0}": "Restore {0}", "Restore already in progress": "Restore already in progress", "Select context": "Select context", diff --git a/package-lock.json b/package-lock.json index 09e0d6762..3ac059de3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "rxjs": "6.6.7", "semver": "7.5.4", "vscode-html-languageservice": "^5.3.1", - "vscode-languageclient": "10.0.0-next.20", + "vscode-languageclient": "10.1.1", "yauzl": "3.2.1" }, "devDependencies": { @@ -1482,31 +1482,6 @@ "copilot-language-server": "dist/language-server.js" } }, - "node_modules/@github/copilot-language-server/node_modules/vscode-jsonrpc": { - "version": "8.2.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", - "integrity": "sha1-9D36NftR52PRfNlNzKDJRY81q/k=", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@github/copilot-language-server/node_modules/vscode-languageserver-protocol": { - "version": "3.17.5", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", - "integrity": "sha1-hkqLjzkINVcvThO9n4MT0OOsS+o=", - "license": "MIT", - "dependencies": { - "vscode-jsonrpc": "8.2.0", - "vscode-languageserver-types": "3.17.5" - } - }, - "node_modules/@github/copilot-language-server/node_modules/vscode-languageserver-types": { - "version": "3.17.5", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", - "integrity": "sha1-MnNnbwzy6rQLP0TQhay7fwijnYo=", - "license": "MIT" - }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@humanfs/core/-/core-0.19.2.tgz", @@ -11379,30 +11354,25 @@ "vscode-uri": "^3.0.8" } }, - "node_modules/vscode-html-languageservice/node_modules/vscode-languageserver-types": { - "version": "3.17.5", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", - "integrity": "sha1-MnNnbwzy6rQLP0TQhay7fwijnYo=", - "license": "MIT" - }, "node_modules/vscode-jsonrpc": { - "version": "9.0.0-next.11", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.11.tgz", - "integrity": "sha1-6Sxurk6NCVDTD6hy9RX76cr18Pg=", + "version": "9.0.2", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-jsonrpc/-/vscode-jsonrpc-9.0.2.tgz", + "integrity": "sha1-CGt2xPlPGwdD3urE4LV8nEpV6B0=", "license": "MIT", "engines": { "node": ">=14.0.0" } }, "node_modules/vscode-languageclient": { - "version": "10.0.0-next.20", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-languageclient/-/vscode-languageclient-10.0.0-next.20.tgz", - "integrity": "sha1-6Mm7cctE4+gImdRLPv/C1T+und0=", + "version": "10.1.1", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-languageclient/-/vscode-languageclient-10.1.1.tgz", + "integrity": "sha1-zVBsX5AQ8D5GAEiH8WR8lEVMtiI=", "license": "MIT", "dependencies": { - "minimatch": "^10.1.2", - "semver": "^7.7.1", - "vscode-languageserver-protocol": "3.17.6-next.16" + "minimatch": "^10.2.6", + "semver": "^7.8.1", + "vscode-languageserver-protocol": "3.18.3", + "vscode-languageserver-textdocument": "1.0.14" }, "engines": { "vscode": "^1.91.0" @@ -11430,12 +11400,12 @@ } }, "node_modules/vscode-languageclient/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha1-Rls6zL0CGLgoH1MB4nztxpf5b94=", + "version": "10.2.6", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha1-/ZVrvgt3JB6fFaxdzLHGOAYJaO8=", "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.8" }, "engines": { "node": "18 || 20 || >=22" @@ -11445,9 +11415,9 @@ } }, "node_modules/vscode-languageclient/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/semver/-/semver-7.7.1.tgz", - "integrity": "sha1-q9UJjYKxjGyB9gdP8mR/0+ciDJ8=", + "version": "7.8.5", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/semver/-/semver-7.8.5.tgz", + "integrity": "sha1-ObZGA33VDBT7RR5+TKxY7YuGP2k=", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -11456,26 +11426,26 @@ "node": ">=10" } }, - "node_modules/vscode-languageclient/node_modules/vscode-languageserver-protocol": { - "version": "3.17.6-next.16", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.16.tgz", - "integrity": "sha1-Sr22BlL3wUkWQgdieXxKtp460+Y=", + "node_modules/vscode-languageserver-protocol": { + "version": "3.18.3", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.18.3.tgz", + "integrity": "sha1-Y5OggGD7lPVSkkP6O+BWxnY5ghE=", "license": "MIT", "dependencies": { - "vscode-jsonrpc": "9.0.0-next.11", - "vscode-languageserver-types": "3.17.6-next.6" + "vscode-jsonrpc": "9.0.2", + "vscode-languageserver-types": "3.18.3" } }, "node_modules/vscode-languageserver-textdocument": { - "version": "1.0.12", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", - "integrity": "sha1-RX7gQnGrOJmKCTxowjQvU/bkpjE=", + "version": "1.0.14", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.14.tgz", + "integrity": "sha1-dzG4CLoakH41YNXkMXvNFLr4Gc4=", "license": "MIT" }, "node_modules/vscode-languageserver-types": { - "version": "3.17.6-next.6", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.6.tgz", - "integrity": "sha1-ZD7DWlJ+qbMmxnB1UfZtj+qz/q8=", + "version": "3.18.3", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-languageserver-types/-/vscode-languageserver-types-3.18.3.tgz", + "integrity": "sha1-6ylsQv1lk3WoKVG5zDPnZyup7+c=", "license": "MIT" }, "node_modules/vscode-oniguruma": { @@ -12648,27 +12618,6 @@ "integrity": "sha1-TK953sM6hRbXD7IR2JioKvPMlgI=", "requires": { "vscode-languageserver-protocol": "^3.17.5" - }, - "dependencies": { - "vscode-jsonrpc": { - "version": "8.2.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", - "integrity": "sha1-9D36NftR52PRfNlNzKDJRY81q/k=" - }, - "vscode-languageserver-protocol": { - "version": "3.17.5", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", - "integrity": "sha1-hkqLjzkINVcvThO9n4MT0OOsS+o=", - "requires": { - "vscode-jsonrpc": "8.2.0", - "vscode-languageserver-types": "3.17.5" - } - }, - "vscode-languageserver-types": { - "version": "3.17.5", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", - "integrity": "sha1-MnNnbwzy6rQLP0TQhay7fwijnYo=" - } } }, "@humanfs/core": { @@ -19676,28 +19625,22 @@ "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "^3.17.5", "vscode-uri": "^3.0.8" - }, - "dependencies": { - "vscode-languageserver-types": { - "version": "3.17.5", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", - "integrity": "sha1-MnNnbwzy6rQLP0TQhay7fwijnYo=" - } } }, "vscode-jsonrpc": { - "version": "9.0.0-next.11", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.11.tgz", - "integrity": "sha1-6Sxurk6NCVDTD6hy9RX76cr18Pg=" + "version": "9.0.2", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-jsonrpc/-/vscode-jsonrpc-9.0.2.tgz", + "integrity": "sha1-CGt2xPlPGwdD3urE4LV8nEpV6B0=" }, "vscode-languageclient": { - "version": "10.0.0-next.20", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-languageclient/-/vscode-languageclient-10.0.0-next.20.tgz", - "integrity": "sha1-6Mm7cctE4+gImdRLPv/C1T+und0=", + "version": "10.1.1", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-languageclient/-/vscode-languageclient-10.1.1.tgz", + "integrity": "sha1-zVBsX5AQ8D5GAEiH8WR8lEVMtiI=", "requires": { - "minimatch": "^10.1.2", - "semver": "^7.7.1", - "vscode-languageserver-protocol": "3.17.6-next.16" + "minimatch": "^10.2.6", + "semver": "^7.8.1", + "vscode-languageserver-protocol": "3.18.3", + "vscode-languageserver-textdocument": "1.0.14" }, "dependencies": { "balanced-match": { @@ -19714,38 +19657,38 @@ } }, "minimatch": { - "version": "10.2.4", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha1-Rls6zL0CGLgoH1MB4nztxpf5b94=", + "version": "10.2.6", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha1-/ZVrvgt3JB6fFaxdzLHGOAYJaO8=", "requires": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.8" } }, "semver": { - "version": "7.7.1", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/semver/-/semver-7.7.1.tgz", - "integrity": "sha1-q9UJjYKxjGyB9gdP8mR/0+ciDJ8=" - }, - "vscode-languageserver-protocol": { - "version": "3.17.6-next.16", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.16.tgz", - "integrity": "sha1-Sr22BlL3wUkWQgdieXxKtp460+Y=", - "requires": { - "vscode-jsonrpc": "9.0.0-next.11", - "vscode-languageserver-types": "3.17.6-next.6" - } + "version": "7.8.5", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/semver/-/semver-7.8.5.tgz", + "integrity": "sha1-ObZGA33VDBT7RR5+TKxY7YuGP2k=" } } }, + "vscode-languageserver-protocol": { + "version": "3.18.3", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.18.3.tgz", + "integrity": "sha1-Y5OggGD7lPVSkkP6O+BWxnY5ghE=", + "requires": { + "vscode-jsonrpc": "9.0.2", + "vscode-languageserver-types": "3.18.3" + } + }, "vscode-languageserver-textdocument": { - "version": "1.0.12", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", - "integrity": "sha1-RX7gQnGrOJmKCTxowjQvU/bkpjE=" + "version": "1.0.14", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.14.tgz", + "integrity": "sha1-dzG4CLoakH41YNXkMXvNFLr4Gc4=" }, "vscode-languageserver-types": { - "version": "3.17.6-next.6", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.6.tgz", - "integrity": "sha1-ZD7DWlJ+qbMmxnB1UfZtj+qz/q8=" + "version": "3.18.3", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-languageserver-types/-/vscode-languageserver-types-3.18.3.tgz", + "integrity": "sha1-6ylsQv1lk3WoKVG5zDPnZyup7+c=" }, "vscode-oniguruma": { "version": "1.7.0", diff --git a/package.json b/package.json index 1ad14b647..fd0754c38 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "rxjs": "6.6.7", "semver": "7.5.4", "vscode-html-languageservice": "^5.3.1", - "vscode-languageclient": "10.0.0-next.20", + "vscode-languageclient": "10.1.1", "yauzl": "3.2.1" }, "devDependencies": { @@ -5780,4 +5780,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/lsptoolshost/server/roslynLanguageClient.ts b/src/lsptoolshost/server/roslynLanguageClient.ts index d063636fe..c39cb2aa8 100644 --- a/src/lsptoolshost/server/roslynLanguageClient.ts +++ b/src/lsptoolshost/server/roslynLanguageClient.ts @@ -4,8 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { ChildProcess } from 'child_process'; import { LanguageClient, ServerOptions } from 'vscode-languageclient/node'; -import { CancellationToken, ErrorHandler, LanguageClientOptions, MessageSignature, State } from 'vscode-languageclient'; +import { + CancellationToken, + ErrorHandler, + LanguageClientOptions, + MessageSignature, + MessageTransports, + State, +} from 'vscode-languageclient'; import CompositeDisposable from '../../compositeDisposable'; import { IDisposable } from '../../disposable'; import { languageServerOptions } from '../../shared/options'; @@ -14,6 +22,11 @@ import { showErrorMessageWithOptions } from '../../shared/observers/utils/showMe import { ITelemetryReporter } from '../../shared/telemetryReporter'; import { TelemetryEventNames } from '../../shared/telemetryEventNames'; +/** + * How long to wait for the server process to be reaped after the connection closes (which can happen milliseconds before the process actually exits) + */ +const serverExitTimeoutMs = 1000; + /** * Implementation of the base LanguageClient type that allows for additional items to be disposed of * when the base LanguageClient instance is disposed. @@ -30,6 +43,11 @@ export class RoslynLanguageClient extends LanguageClient { */ private _hasShownConnectionClose = false; + /** + * Resolves when the process exits, true when the server process was terminated externally. + */ + private _serverExit: Promise | undefined; + constructor( id: string, name: string, @@ -61,6 +79,12 @@ export class RoslynLanguageClient extends LanguageClient { return super.dispose(timeout); } + protected override async createMessageTransports(encoding: string): Promise { + const transports = await super.createMessageTransports(encoding); + this._serverExit = waitForExternalTermination(this.serverProcess); + return transports; + } + override handleFailedRequest( type: MessageSignature, token: CancellationToken | undefined, @@ -161,28 +185,97 @@ export class RoslynLanguageClient extends LanguageClient { return; } + // Set the guard before awaiting so the error and closed handlers cannot both get past it. this._hasShownConnectionClose = true; - this._telemetryReporter.sendTelemetryEvent(TelemetryEventNames.ServerCrash); - this.showCrashNotificationCore(); + void this.showCrashNotificationAsync(); } - private showCrashNotificationCore() { - showErrorMessageWithOptions( - vscode, - vscode.l10n.t('The C# language server has crashed. Restart extensions to re-enable C# functionality.'), - { modal: false }, - { - title: vscode.l10n.t('Restart extensions'), - command: 'workbench.action.restartExtensionHost', - }, - { - title: vscode.l10n.t('Report Issue'), - action: async () => { - vscode.commands.executeCommand('csharp.reportIssue'); - // Re-show the notification so the user can still restart extensions after reporting. - this.showCrashNotificationCore(); - }, - } - ); + /** + * Waits for the server process to exit, racing with a timeout. + * Reports false if the process outlives the connection. + */ + private async waitForProcessExit(): Promise { + if (this._serverExit === undefined) { + return false; + } + + let timeout: NodeJS.Timeout | undefined; + try { + return await Promise.race([ + this._serverExit, + new Promise((resolve) => { + timeout = setTimeout(() => resolve(false), serverExitTimeoutMs); + }), + ]); + } finally { + clearTimeout(timeout); + } + } + + private async showCrashNotificationAsync(): Promise { + const externallyTerminated = await this.waitForProcessExit(); + + this._telemetryReporter.sendTelemetryEvent(TelemetryEventNames.ServerCrash, { + externallyTerminated: externallyTerminated.toString(), + }); + + this.showCrashNotificationCore(externallyTerminated); + } + + private showCrashNotificationCore(externallyTerminated: boolean) { + const restartCommand = { + title: vscode.l10n.t('Restart extensions'), + command: 'workbench.action.restartExtensionHost', + }; + if (externallyTerminated) { + // Show a notification without a report issue command - there's nothing we can if the server + // was terminated by some external process. + showErrorMessageWithOptions( + vscode, + vscode.l10n.t( + 'The C# language server was terminated externally. Restart extensions to re-enable C# functionality.' + ), + { modal: false }, + restartCommand + ); + } else { + showErrorMessageWithOptions( + vscode, + vscode.l10n.t('The C# language server has crashed. Restart extensions to re-enable C# functionality.'), + { modal: false }, + restartCommand, + { + title: vscode.l10n.t('Report Issue'), + action: async () => { + vscode.commands.executeCommand('csharp.reportIssue'); + // Re-show the notification so the user can still restart extensions after reporting. + this.showCrashNotificationCore(externallyTerminated); + }, + } + ); + } } } + +/** + * Resolves when the server process exits, reporting whether it was stopped externally or not. + * Note that this is only reliable on non-windows platforms - on windows a killed process has no signal and can have any exit code. + */ +function waitForExternalTermination(serverProcess: ChildProcess | undefined): Promise | undefined { + if (serverProcess === undefined) { + return undefined; + } + + return new Promise((resolve) => { + serverProcess.once('exit', (code, signal) => { + resolve( + // SIGKILL cannot be caught, blocked, or ignored, so it was sent by an external process. + signal === 'SIGKILL' || + // .NET normally handles SIGTERM and exits with 128 + SIGTERM instead of reporting the signal. + code === 143 || + // The PAL re-raises SIGTERM on some paths. + signal === 'SIGTERM' + ); + }); + }); +} diff --git a/test/lsptoolshost/unitTests/roslynLanguageClient.test.ts b/test/lsptoolshost/unitTests/roslynLanguageClient.test.ts index 1dd4262ba..9c042ff57 100644 --- a/test/lsptoolshost/unitTests/roslynLanguageClient.test.ts +++ b/test/lsptoolshost/unitTests/roslynLanguageClient.test.ts @@ -4,9 +4,23 @@ *--------------------------------------------------------------------------------------------*/ import { describe, expect, jest, test } from '@jest/globals'; +import { ChildProcess } from 'child_process'; +import { EventEmitter } from 'events'; jest.mock('vscode-languageclient/node', () => ({ - LanguageClient: class {}, + LanguageClient: class { + serverProcess: unknown; + async createMessageTransports(): Promise { + return { reader: {}, writer: {} }; + } + /** + * Stands in for the base implementation, which drops its reference to the server process + * before invoking the close handler. + */ + async handleConnectionClosed(): Promise { + this.serverProcess = undefined; + } + }, State: { Running: 2 }, })); jest.mock('vscode-languageclient', () => ({ @@ -18,26 +32,188 @@ jest.mock('vscode-languageclient', () => ({ ProtocolRequestType: class {}, RequestType: class {}, RequestType0: class {}, + State: { Running: 2 }, })); import { RoslynLanguageClient } from '../../../src/lsptoolshost/server/roslynLanguageClient'; import { TelemetryEventNames } from '../../../src/shared/telemetryEventNames'; -describe('RoslynLanguageClient', () => { - test('shows one crash notification and emits one telemetry event per crash', () => { - const sendTelemetryEvent = jest.fn(); - const showCrashNotificationCore = jest.fn(); - const client = Object.create(RoslynLanguageClient.prototype) as any; +/** A stand-in for a server process that has not exited yet. */ +function runningProcess(): ChildProcess { + return new EventEmitter() as unknown as ChildProcess; +} + +function createClient() { + const sendTelemetryEvent = jest.fn(); + const showCrashNotificationCore = jest.fn(); + const client = Object.create(RoslynLanguageClient.prototype) as any; + + client._hasShownConnectionClose = false; + client._telemetryReporter = { sendTelemetryEvent }; + client.showCrashNotificationCore = showCrashNotificationCore; + + return { client, sendTelemetryEvent, showCrashNotificationCore }; +} + +/** Launches a server process the way the base client does, so the exit listener gets attached. */ +async function launchServer(client: any, serverProcess: ChildProcess): Promise { + client.serverProcess = serverProcess; + await client.createMessageTransports('utf8'); +} - client._hasShownConnectionClose = false; - client._telemetryReporter = { sendTelemetryEvent }; - client.showCrashNotificationCore = showCrashNotificationCore; +/** Lets the fire-and-forget notification promise chain settle. */ +async function flushPendingNotifications(): Promise { + for (let i = 0; i < 5; i++) { + await Promise.resolve(); + } +} +describe('RoslynLanguageClient', () => { + test('shows one crash notification and emits one telemetry event per crash', async () => { + const { client, sendTelemetryEvent, showCrashNotificationCore } = createClient(); + + // Both the error and closed handlers fire when the server goes down. client.showCrashNotification(); client.showCrashNotification(); + await flushPendingNotifications(); expect(sendTelemetryEvent).toHaveBeenCalledTimes(1); - expect(sendTelemetryEvent).toHaveBeenCalledWith(TelemetryEventNames.ServerCrash); expect(showCrashNotificationCore).toHaveBeenCalledTimes(1); }); + + test('reports an external termination when the process was killed with SIGKILL', async () => { + const serverProcess = runningProcess(); + const { client, sendTelemetryEvent, showCrashNotificationCore } = createClient(); + await launchServer(client, serverProcess); + + serverProcess.emit('exit', null, 'SIGKILL'); + client.showCrashNotification(); + await flushPendingNotifications(); + + expect(sendTelemetryEvent).toHaveBeenCalledWith(TelemetryEventNames.ServerCrash, { + externallyTerminated: 'true', + }); + expect(showCrashNotificationCore).toHaveBeenCalledWith(true); + }); + + // .NET handles SIGTERM rather than dying from it, shutting down and exiting with 128 + SIGTERM. + // This is how an external termination usually reaches us, and it arrives as a code, not a signal. + test('reports an external termination for the SIGTERM exit code', async () => { + const serverProcess = runningProcess(); + const { client, sendTelemetryEvent, showCrashNotificationCore } = createClient(); + await launchServer(client, serverProcess); + + serverProcess.emit('exit', 143, null); + client.showCrashNotification(); + await flushPendingNotifications(); + + expect(sendTelemetryEvent).toHaveBeenCalledWith(TelemetryEventNames.ServerCrash, { + externallyTerminated: 'true', + }); + expect(showCrashNotificationCore).toHaveBeenCalledWith(true); + }); + + // Some runtime paths re-raise SIGTERM instead of exiting with 143. + test('reports an external termination when SIGTERM arrives as a signal', async () => { + const serverProcess = runningProcess(); + const { client, showCrashNotificationCore } = createClient(); + await launchServer(client, serverProcess); + + serverProcess.emit('exit', null, 'SIGTERM'); + client.showCrashNotification(); + await flushPendingNotifications(); + + expect(showCrashNotificationCore).toHaveBeenCalledWith(true); + }); + + // The .NET runtime ends fatal errors with abort(), so SIGABRT is the server genuinely failing. + test('reports a crash when the runtime aborted the process', async () => { + const serverProcess = runningProcess(); + const { client, sendTelemetryEvent, showCrashNotificationCore } = createClient(); + await launchServer(client, serverProcess); + + serverProcess.emit('exit', null, 'SIGABRT'); + client.showCrashNotification(); + await flushPendingNotifications(); + + expect(sendTelemetryEvent).toHaveBeenCalledWith(TelemetryEventNames.ServerCrash, { + externallyTerminated: 'false', + }); + expect(showCrashNotificationCore).toHaveBeenCalledWith(false); + }); + + // 0xE0434352 is an unhandled managed exception and 0xC0000005 an access violation, both of which + // are the server failing on its own rather than being stopped. + test.each([3762504530, 3221225477, 1])('reports a crash for exit code %s', async (exitCode) => { + const serverProcess = runningProcess(); + const { client, showCrashNotificationCore } = createClient(); + await launchServer(client, serverProcess); + + serverProcess.emit('exit', exitCode, null); + client.showCrashNotification(); + await flushPendingNotifications(); + + expect(showCrashNotificationCore).toHaveBeenCalledWith(false); + }); + + test('reports a crash when the server never launched', async () => { + const { client, showCrashNotificationCore } = createClient(); + + client.showCrashNotification(); + await flushPendingNotifications(); + + expect(showCrashNotificationCore).toHaveBeenCalledWith(false); + }); + + test('keeps listening after the base client drops its reference to the process', async () => { + const serverProcess = runningProcess(); + const { client, showCrashNotificationCore } = createClient(); + await launchServer(client, serverProcess); + + // The close handler runs before the process is reaped and leaves nothing to read the signal + // from, but clearing that reference does not unregister our listener. + await client.handleConnectionClosed(); + expect(client.serverProcess).toBeUndefined(); + + client.showCrashNotification(); + await flushPendingNotifications(); + expect(showCrashNotificationCore).not.toHaveBeenCalled(); + + serverProcess.emit('exit', null, 'SIGKILL'); + await flushPendingNotifications(); + + expect(showCrashNotificationCore).toHaveBeenCalledWith(true); + }); + + test('describes the current process rather than a previous session', async () => { + const killedProcess = runningProcess(); + const { client, showCrashNotificationCore } = createClient(); + await launchServer(client, killedProcess); + killedProcess.emit('exit', null, 'SIGKILL'); + + // Restarting replaces the captured exit, so the earlier kill is not reported again. + const restartedProcess = runningProcess(); + await launchServer(client, restartedProcess); + + restartedProcess.emit('exit', 1, null); + client.showCrashNotification(); + await flushPendingNotifications(); + + expect(showCrashNotificationCore).toHaveBeenCalledWith(false); + }); + + test('falls back to the generic crash message when the process outlives the connection', async () => { + jest.useFakeTimers(); + try { + const { client, showCrashNotificationCore } = createClient(); + await launchServer(client, runningProcess()); + + client.showCrashNotification(); + await jest.advanceTimersByTimeAsync(1000); + + expect(showCrashNotificationCore).toHaveBeenCalledWith(false); + } finally { + jest.useRealTimers(); + } + }); }); diff --git a/tsconfig.json b/tsconfig.json index 9cf7928aa..5f8670eb1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,8 +22,8 @@ "vscode-languageclient/selectionRange": ["./node_modules/vscode-languageclient/lib/common/selectionRange"], "vscode-languageclient/protocolConverter": ["./node_modules/vscode-languageclient/lib/common/protocolConverter"], "vscode-languageclient/node": ["./node_modules/vscode-languageclient/lib/node/main"], - "vscode-languageserver-protocol": ["./node_modules/vscode-languageclient/node_modules/vscode-languageserver-protocol/lib/common/api"], - "vscode-languageserver-protocol/node": ["./node_modules/vscode-languageclient/node_modules/vscode-languageserver-protocol/lib/node/main"], + "vscode-languageserver-protocol": ["./node_modules/vscode-languageserver-protocol/lib/common/api"], + "vscode-languageserver-protocol/node": ["./node_modules/vscode-languageserver-protocol/lib/node/main"], "vscode-jsonrpc": ["./node_modules/vscode-jsonrpc/lib/common/api"], "vscode-jsonrpc/node": ["./node_modules/vscode-jsonrpc/lib/node/main"], },