Skip to content

Commit b0c9d16

Browse files
authored
Dbaeumer/better-cougar-lime (#935)
* Fix race condition * Handle errors correctly which show up because a pending response got rejected becase the connection got disposed * Fix test * Only allow send* calls when client is running or not started yet.
1 parent f433fae commit b0c9d16

6 files changed

Lines changed: 98 additions & 18 deletions

File tree

client-node-tests/src/integration.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1566,7 +1566,7 @@ suite('Server tests', () => {
15661566

15671567
await assert.rejects(async () => {
15681568
await client.stop();
1569-
}, /Connection got disposed/);
1569+
}, /Pending response rejected since connection got disposed/);
15701570
assert.strictEqual(client.needsStart(), true);
15711571
assert.strictEqual(client.needsStop(), false);
15721572

client/src/common/client.ts

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import {
3434
DocumentOnTypeFormattingRequest, RenameRequest, DocumentSymbolRequest, DocumentLinkRequest, DocumentColorRequest, DeclarationRequest, FoldingRangeRequest,
3535
ImplementationRequest, SelectionRangeRequest, TypeDefinitionRequest, CallHierarchyPrepareRequest, SemanticTokensRegistrationType, LinkedEditingRangeRequest,
3636
TypeHierarchyPrepareRequest, InlineValueRequest, InlayHintRequest, WorkspaceSymbolRequest, TextDocumentRegistrationOptions, FileOperationRegistrationOptions,
37-
ConnectionOptions, PositionEncodingKind, DocumentDiagnosticRequest, NotebookDocumentSyncRegistrationType, NotebookDocumentSyncRegistrationOptions
37+
ConnectionOptions, PositionEncodingKind, DocumentDiagnosticRequest, NotebookDocumentSyncRegistrationType, NotebookDocumentSyncRegistrationOptions, ErrorCodes
3838
} from 'vscode-languageserver-protocol';
3939

4040
import * as c2p from './codeConverter';
@@ -431,6 +431,13 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
431431
private _clientOptions: ResolvedClientOptions;
432432

433433
private _state: ClientState;
434+
private _onStart: Promise<void> | undefined;
435+
private _onStop: Promise<void> | undefined;
436+
private _connection: Connection | undefined;
437+
private _idleInterval: Disposable | undefined;
438+
private readonly _ignoredRegistrations: Set<string>;
439+
// private _idleStart: number | undefined;
440+
434441
private readonly _notificationHandlers: Map<string, GenericNotificationHandler>;
435442
private readonly _notificationDisposables: Map<string, Disposable>;
436443
private readonly _pendingNotificationHandlers: Map<string, GenericNotificationHandler>;
@@ -441,12 +448,6 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
441448
private readonly _pendingProgressHandlers: Map<string | number, { type: ProgressType<any>; handler: NotificationHandler<any> }>;
442449
private readonly _progressDisposables: Map<string | number, Disposable>;
443450

444-
private _onStart: Promise<void> | undefined;
445-
private _onStop: Promise<void> | undefined;
446-
private _connection: Connection | undefined;
447-
private _idleInterval: Disposable | undefined;
448-
// private _idleStart: number | undefined;
449-
450451
private _initializeResult: InitializeResult | undefined;
451452
private _outputChannel: OutputChannel | undefined;
452453
private _disposeOutputChannel: boolean;
@@ -510,6 +511,8 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
510511
this._clientOptions.synchronize = this._clientOptions.synchronize || {};
511512

512513
this._state = ClientState.Initial;
514+
this._ignoredRegistrations = new Set();
515+
513516
this._notificationHandlers = new Map();
514517
this._pendingNotificationHandlers = new Map();
515518
this._notificationDisposables = new Map();
@@ -642,6 +645,9 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
642645
public sendRequest<R>(method: string, token?: CancellationToken): Promise<R>;
643646
public sendRequest<R>(method: string, param: any, token?: CancellationToken): Promise<R>;
644647
public async sendRequest<R>(type: string | MessageSignature, ...params: any[]): Promise<R> {
648+
if (this.$state === ClientState.StartFailed || this.$state === ClientState.Stopping || this.$state === ClientState.Stopped) {
649+
return Promise.reject(new ResponseError(ErrorCodes.ConnectionInactive, `Client is not running`));
650+
}
645651
try {
646652
// Ensure we have a connection before we force the document sync.
647653
const connection = await this.$start();
@@ -702,6 +708,9 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
702708
public sendNotification(method: string): Promise<void>;
703709
public sendNotification(method: string, params: any): Promise<void>;
704710
public async sendNotification<P>(type: string | MessageSignature, params?: P): Promise<void> {
711+
if (this.$state === ClientState.StartFailed || this.$state === ClientState.Stopping || this.$state === ClientState.Stopped) {
712+
return Promise.reject(new ResponseError(ErrorCodes.ConnectionInactive, `Client is not running`));
713+
}
705714
try {
706715
// Ensure we have a connection before we force the document sync.
707716
const connection = await this.$start();
@@ -925,12 +934,15 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
925934
if (this._onStart !== undefined) {
926935
return this._onStart;
927936
}
937+
const [promise, resolve, reject] = this.createOnStartPromise();
938+
this._onStart = promise;
939+
940+
// We are currently stopping the language client. Await the stop
941+
// before continuing.
928942
if (this._onStop !== undefined) {
929943
await this._onStop;
930944
this._onStop = undefined;
931945
}
932-
const [promise, resolve, reject] = this.createOnStartPromise();
933-
this._onStart = promise;
934946
// If we restart then the diagnostics collection is reused.
935947
if (this._diagnostics === undefined) {
936948
this._diagnostics = this._clientOptions.diagnosticCollectionName
@@ -1206,12 +1218,12 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
12061218
return undefined;
12071219
}
12081220

1209-
public async stop(timeout: number = 2000): Promise<void> {
1221+
public stop(timeout: number = 2000): Promise<void> {
12101222
// Wait 2 seconds on stop
12111223
return this.shutdown('stop', timeout);
12121224
}
12131225

1214-
public async suspend(): Promise<void> {
1226+
public suspend(): Promise<void> {
12151227
// Wait 5 seconds on suspend.
12161228
return this.shutdown('suspend', 5000);
12171229
}
@@ -1259,6 +1271,7 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
12591271
this._onStart = undefined;
12601272
this._onStop = undefined;
12611273
this._connection = undefined;
1274+
this._ignoredRegistrations.clear();
12621275
});
12631276
}
12641277

@@ -1699,6 +1712,15 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
16991712
}
17001713

17011714
private async handleRegistrationRequest(params: RegistrationParams): Promise<void> {
1715+
// We will not receive a registration call before a client is running
1716+
// from a server. However if we stop or shutdown we might which might
1717+
// try to restart the server. So ignore registrations if we are not running
1718+
if (!this.isRunning()) {
1719+
for (const registration of params.registrations) {
1720+
this._ignoredRegistrations.add(registration.id);
1721+
}
1722+
return;
1723+
}
17021724
interface WithDocumentSelector {
17031725
documentSelector: DocumentSelector | undefined;
17041726
}
@@ -1723,6 +1745,9 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
17231745

17241746
private async handleUnregistrationRequest(params: UnregistrationParams): Promise<void> {
17251747
for (let unregistration of params.unregisterations) {
1748+
if (this._ignoredRegistrations.has(unregistration.id)) {
1749+
continue;
1750+
}
17261751
const feature = this._dynamicFeatures.get(unregistration.method);
17271752
if (!feature) {
17281753
return Promise.reject(new Error(`No feature implementation for ${unregistration.method} found. Unregistration failed.`));
@@ -1771,6 +1796,11 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
17711796
public handleFailedRequest<T>(type: MessageSignature, token: CancellationToken | undefined, error: any, defaultValue: T, showNotification: boolean = true): T {
17721797
// If we get a request cancel or a content modified don't log anything.
17731798
if (error instanceof ResponseError) {
1799+
// The connection got disposed while we were waiting for a response.
1800+
// Simply return the default value. Is the best we can do.
1801+
if (error.code === ErrorCodes.PendingResponseRejected || error.code === ErrorCodes.ConnectionInactive) {
1802+
return defaultValue;
1803+
}
17741804
if (error.code === LSPErrorCodes.RequestCancelled || error.code === LSPErrorCodes.ServerCancelled) {
17751805
if (token !== undefined && token.isCancellationRequested) {
17761806
return defaultValue;

client/src/common/diagnostic.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,9 @@ class DiagnosticRequestor implements Disposable {
361361
}
362362

363363
public pull(document: TextDocument | Uri, cb?: () => void): void {
364+
if (this.isDisposed) {
365+
return;
366+
}
364367
const uri = document instanceof Uri ? document : document.uri;
365368
this.pullAsync(document).then(() => {
366369
if (cb) {
@@ -372,6 +375,9 @@ class DiagnosticRequestor implements Disposable {
372375
}
373376

374377
private async pullAsync(document: TextDocument | Uri, version?: number | undefined): Promise<void> {
378+
if (this.isDisposed) {
379+
return;
380+
}
375381
const isUri = document instanceof Uri;
376382
const uri = isUri ? document : document.uri;
377383
const key = uri.toString();
@@ -456,6 +462,9 @@ class DiagnosticRequestor implements Disposable {
456462
}
457463

458464
public pullWorkspace(): void {
465+
if (this.isDisposed) {
466+
return;
467+
}
459468
this.pullWorkspaceAsync().then(() => {
460469
this.workspaceTimeout = RAL().timer.setTimeout(() => {
461470
this.pullWorkspace();
@@ -474,7 +483,7 @@ class DiagnosticRequestor implements Disposable {
474483
}
475484

476485
private async pullWorkspaceAsync(): Promise<void> {
477-
if (!this.provider.provideWorkspaceDiagnostics) {
486+
if (!this.provider.provideWorkspaceDiagnostics || this.isDisposed) {
478487
return;
479488
}
480489
if (this.workspaceCancellation !== undefined) {
@@ -515,6 +524,9 @@ class DiagnosticRequestor implements Disposable {
515524
textDocument: { uri: this.client.code2ProtocolConverter.asUri(document instanceof Uri ? document : document.uri) },
516525
previousResultId: previousResultId
517526
};
527+
if (this.isDisposed === true || !this.client.isRunning()) {
528+
return { kind: vsdiag.DocumentDiagnosticReportKind.full, items: [] };
529+
}
518530
return this.client.sendRequest(DocumentDiagnosticRequest.type, params, token).then(async (result) => {
519531
if (result === undefined || result === null || this.isDisposed || token.isCancellationRequested) {
520532
return { kind: vsdiag.DocumentDiagnosticReportKind.full, items: [] };
@@ -585,6 +597,9 @@ class DiagnosticRequestor implements Disposable {
585597
previousResultIds: convertPreviousResultIds(resultIds),
586598
partialResultToken: partialResultToken
587599
};
600+
if (this.isDisposed === true || !this.client.isRunning()) {
601+
return { items: [] };
602+
}
588603
return this.client.sendRequest(WorkspaceDiagnosticRequest.type, params, token).then(async (result): Promise<vsdiag.WorkspaceDiagnosticReport> => {
589604
if (token.isCancellationRequested) {
590605
return { items: [] };
@@ -640,13 +655,22 @@ class BackgroundScheduler implements Disposable {
640655
private endDocument: TextDocument | Uri | undefined;
641656
private readonly documents: LinkedMap<string, TextDocument | Uri>;
642657
private intervalHandle: Disposable | undefined;
658+
// The problem is that there could be outstanding diagnostic requests
659+
// when we shutdown which when we receive the result will trigger a
660+
// reschedule. So we remember if the background scheduler got disposed
661+
// and ignore those re-schedules
662+
private isDisposed: boolean;
643663

644664
public constructor(diagnosticRequestor: DiagnosticRequestor) {
645665
this.diagnosticRequestor = diagnosticRequestor;
646666
this.documents = new LinkedMap();
667+
this.isDisposed = false;
647668
}
648669

649670
public add(document: TextDocument | Uri): void {
671+
if (this.isDisposed === true) {
672+
return;
673+
}
650674
const key = document instanceof Uri ? document.toString() : document.uri.toString();
651675
if (this.documents.has(key)) {
652676
return;
@@ -672,6 +696,9 @@ class BackgroundScheduler implements Disposable {
672696
}
673697

674698
public trigger(): void {
699+
if (this.isDisposed === true) {
700+
return;
701+
}
675702
// We have a round running. So simply make sure we run up to the
676703
// last document
677704
if (this.intervalHandle !== undefined) {
@@ -693,6 +720,7 @@ class BackgroundScheduler implements Disposable {
693720
}
694721

695722
public dispose(): void {
723+
this.isDisposed = true;
696724
this.stop();
697725
this.documents.clear();
698726
}
@@ -866,11 +894,10 @@ class DiagnosticFeatureProviderImpl implements DiagnosticProviderShape {
866894

867895
export class DiagnosticFeature extends TextDocumentLanguageFeature<DiagnosticOptions, DiagnosticRegistrationOptions, DiagnosticProviderShape, DiagnosticProviderMiddleware, $DiagnosticPullOptions> {
868896

869-
private readonly tabs: Tabs;
897+
private tabs: Tabs | undefined;
870898

871899
constructor(client: FeatureClient<DiagnosticProviderMiddleware, $DiagnosticPullOptions>) {
872900
super(client, DocumentDiagnosticRequest.type);
873-
this.tabs = new Tabs();
874901
}
875902

876903
public fillClientCapabilities(capabilities: ClientCapabilities): void {
@@ -899,11 +926,17 @@ export class DiagnosticFeature extends TextDocumentLanguageFeature<DiagnosticOpt
899926
}
900927

901928
public dispose(): void {
902-
this.tabs.dispose();
929+
if (this.tabs !== undefined) {
930+
this.tabs.dispose();
931+
this.tabs = undefined;
932+
}
903933
super.dispose();
904934
}
905935

906936
protected registerLanguageProvider(options: DiagnosticRegistrationOptions): [Disposable, DiagnosticProviderShape] {
937+
if (this.tabs === undefined) {
938+
this.tabs = new Tabs();
939+
}
907940
const provider = new DiagnosticFeatureProviderImpl(this._client, this.tabs, options);
908941
return [provider.disposable, provider];
909942
}

client/src/common/fileOperations.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@ abstract class FileOperationFeature<I, E extends Event<I>> implements DynamicFea
9191
assign(value, this._clientCapability, true);
9292
}
9393

94-
9594
public initialize(capabilities: proto.ServerCapabilities): void {
9695
const options = capabilities.workspace?.fileOperations;
9796
const capability = options !== undefined ? access(options, this._serverCapability) : undefined;

jsonrpc/src/common/connection.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1392,7 +1392,7 @@ export function createMessageConnection(messageReader: MessageReader, messageWri
13921392
}
13931393
state = ConnectionState.Disposed;
13941394
disposeEmitter.fire(undefined);
1395-
const error = new Error('Connection got disposed.');
1395+
const error = new ResponseError(ErrorCodes.PendingResponseRejected, 'Pending response rejected since connection got disposed');
13961396
for (const promise of responsePromises.values()) {
13971397
promise.reject(error);
13981398
}

jsonrpc/src/common/messages.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,27 @@ export namespace ErrorCodes {
5757
/** @deprecated use jsonrpcReservedErrorRangeStart */
5858
export const serverErrorStart: -32099 = /* jsonrpcReservedErrorRangeStart */ -32099;
5959

60+
/**
61+
* An error occurred when write a message to the transport layer.
62+
*/
6063
export const MessageWriteError: -32099 = -32099;
64+
65+
/**
66+
* An error occurred when reading a message from the transport layer.
67+
*/
6168
export const MessageReadError: -32098 = -32098;
6269

70+
/**
71+
* The connection got disposed or lost and all pending responses got
72+
* rejected.
73+
*/
74+
export const PendingResponseRejected: -32097 = -32097;
75+
76+
/**
77+
* The connection is inactive and a use of it failed.
78+
*/
79+
export const ConnectionInactive: -32096 = -32096;
80+
6381
/**
6482
* Error code indicating that a server received a notification or
6583
* request before the server has received the `initialize` request.

0 commit comments

Comments
 (0)