Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions client-node-tests/src/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>(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'),
Expand Down
13 changes: 13 additions & 0 deletions client-node-tests/src/servers/failInitializeServer.ts
Original file line number Diff line number Diff line change
@@ -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<InitializeError>(1, 'Initialization refused', { retry: false });
});
connection.listen();
16 changes: 13 additions & 3 deletions client/src/common/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1554,27 +1554,37 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
if (this._clientOptions.initializationFailedHandler(error)) {
void this.initialize(connection);
} else {
void this.stop();
this.stopAfterFailedInitialize();
}
} else if (error instanceof ResponseError && error.data && error.data.retry) {
void Window.showErrorMessage(error.message, { title: 'Retry', id: 'retry' }).then(item => {
if (item && item.id === 'retry') {
void this.initialize(connection);
} else {
void this.stop();
this.stopAfterFailedInitialize();
}
});
} else {
if (error && error.message) {
void Window.showErrorMessage(error.message);
}
this.error('Server initialization failed.', error);
void this.stop();
this.stopAfterFailedInitialize();
}
throw error;
}
}

private stopAfterFailedInitialize(): void {
// The client is still starting, so the base shutdown rejects because the client is
// not running. Calling stop() still lets subclasses release what they own: the Node
// client terminates the server process. Nothing awaits the result, so handle the
// rejection here instead of leaving it unhandled.
this.stop().catch((error) => {
this.error(`Stopping server failed`, error, false);
});
}

private _clientGetRootPath(): string | undefined {
const folders = Workspace.workspaceFolders;
if (!folders || folders.length === 0) {
Expand Down
Loading