diff --git a/client-node-tests/src/integration.test.ts b/client-node-tests/src/integration.test.ts index 2ddae1a2c..e63a5a9fd 100644 --- a/client-node-tests/src/integration.test.ts +++ b/client-node-tests/src/integration.test.ts @@ -2427,6 +2427,49 @@ suite('Server tests', () => { } }).timeout(5000); + test('A refused initialization stops the client without an unhandled rejection', async () => { + const outputChannel = vscode.window.createOutputChannel('Initialization refused', { log: true }); + const client = new lsclient.LanguageClient('initialization-refused', 'Initialization refused', { + module: path.join(__dirname, './servers/failInitializeServer.js'), + transport: lsclient.TransportKind.ipc, + }, { + outputChannel, + initializationFailedHandler: () => false, + errorHandler: { + error: () => ({ action: lsclient.ErrorAction.Continue }), + closed: () => ({ action: lsclient.CloseAction.DoNotRestart, handled: true }), + } + }); + // The client moves from StartFailed to Stopped when the server's connection closes. + const stopped = new Promise(resolve => { + const listener = client.onDidChangeState(event => { + if (event.newState === lsclient.State.Stopped) { + listener.dispose(); + resolve(); + } + }); + }); + const unhandled: unknown[] = []; + const onUnhandled = (reason: unknown) => unhandled.push(reason); + process.on('unhandledRejection', onUnhandled); + const stop = sinon.spy(client, 'stop'); + try { + await assert.rejects(client.start(), /Initialization refused/); + // The client still stops after the failure, so the Node client terminates the + // server process, which closes the connection. The process has then also logged + // its exit, so the output channel can be disposed. + assert.strictEqual(stop.callCount, 1); + await stopped; + assert.deepStrictEqual(unhandled, []); + } finally { + process.off('unhandledRejection', onUnhandled); + stop.restore(); + await client.dispose(); + client.diagnostics?.dispose(); + outputChannel.dispose(); + } + }).timeout(10000); + test('Stop fails if server crashes after shutdown request', async () => { const serverOptions: lsclient.ServerOptions = { module: path.join(__dirname, './servers/crashOnShutdownServer.js'), diff --git a/client-node-tests/src/servers/failInitializeServer.ts b/client-node-tests/src/servers/failInitializeServer.ts new file mode 100644 index 000000000..35a244ca9 --- /dev/null +++ b/client-node-tests/src/servers/failInitializeServer.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createConnection, InitializeError, ResponseError } from 'vscode-languageserver/node'; + +const connection = createConnection(); +connection.onInitialize(() => { + // Refuse initialization but keep running, so the client still owns a live connection. + return new ResponseError(1, 'Initialization refused', { retry: false }); +}); +connection.listen(); diff --git a/client/src/common/client.ts b/client/src/common/client.ts index a86688b13..80f77c9da 100644 --- a/client/src/common/client.ts +++ b/client/src/common/client.ts @@ -1554,14 +1554,14 @@ export abstract class BaseLanguageClient implements FeatureClient { if (item && item.id === 'retry') { void this.initialize(connection); } else { - void this.stop(); + this.stopAfterFailedInitialize(); } }); } else { @@ -1569,12 +1569,22 @@ export abstract class BaseLanguageClient implements FeatureClient { + this.error(`Stopping server failed`, error, false); + }); + } + private _clientGetRootPath(): string | undefined { const folders = Workspace.workspaceFolders; if (!folders || folders.length === 0) {