Make sure the syntax and semantic servers don't get out of sync
If one server fails for a command but the other does not, we are in an inconsistent state. Treat this as a fatal error
This commit is contained in:
parent
ab4e86df8d
commit
ed53e86205
3 changed files with 56 additions and 11 deletions
|
@ -59,6 +59,10 @@ export interface ITypeScriptServer {
|
|||
dispose(): void;
|
||||
}
|
||||
|
||||
export interface TsServerDelegate {
|
||||
onFatalError(command: string): void;
|
||||
}
|
||||
|
||||
export interface TsServerProcess {
|
||||
readonly stdout: stream.Readable;
|
||||
write(serverRequest: Proto.Request): void;
|
||||
|
@ -305,6 +309,7 @@ export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServ
|
|||
public constructor(
|
||||
private readonly syntaxServer: ITypeScriptServer,
|
||||
private readonly semanticServer: ITypeScriptServer,
|
||||
private readonly _delegate: TsServerDelegate,
|
||||
) {
|
||||
super();
|
||||
|
||||
|
@ -362,14 +367,18 @@ export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServ
|
|||
} else if (SyntaxRoutingTsServer.sharedCommands.has(command)) {
|
||||
// Dispatch to both server but only return from syntax one
|
||||
|
||||
const enum RequestState { Unresolved, Resolved, Errored }
|
||||
let syntaxRequestState = RequestState.Unresolved;
|
||||
let semanticRequestState = RequestState.Unresolved;
|
||||
|
||||
// Also make sure we never cancel requests to just one server
|
||||
let hasCompletedSyntax = false;
|
||||
let hasCompletedSemantic = false;
|
||||
let token: vscode.CancellationToken | undefined = undefined;
|
||||
if (executeInfo.token) {
|
||||
const source = new vscode.CancellationTokenSource();
|
||||
executeInfo.token.onCancellationRequested(() => {
|
||||
if (hasCompletedSyntax && !hasCompletedSemantic || hasCompletedSemantic && !hasCompletedSyntax) {
|
||||
if (syntaxRequestState !== RequestState.Unresolved && semanticRequestState === RequestState.Unresolved
|
||||
|| syntaxRequestState === RequestState.Unresolved && semanticRequestState !== RequestState.Unresolved
|
||||
) {
|
||||
// Don't cancel.
|
||||
// One of the servers completed this request so we don't want to leave the other
|
||||
// in a different state
|
||||
|
@ -382,11 +391,41 @@ export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServ
|
|||
|
||||
const semanticRequest = this.semanticServer.executeImpl(command, args, { ...executeInfo, token });
|
||||
if (semanticRequest) {
|
||||
semanticRequest.finally(() => { hasCompletedSemantic = true; });
|
||||
semanticRequest
|
||||
.then(result => {
|
||||
semanticRequestState = RequestState.Resolved;
|
||||
if (syntaxRequestState === RequestState.Errored) {
|
||||
// We've gone out of sync
|
||||
this._delegate.onFatalError(command);
|
||||
}
|
||||
return result;
|
||||
}, err => {
|
||||
semanticRequestState = RequestState.Errored;
|
||||
if (syntaxRequestState === RequestState.Resolved) {
|
||||
// We've gone out of sync
|
||||
this._delegate.onFatalError(command);
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
const syntaxRequest = this.syntaxServer.executeImpl(command, args, { ...executeInfo, token });
|
||||
if (syntaxRequest) {
|
||||
syntaxRequest.finally(() => { hasCompletedSyntax = true; });
|
||||
syntaxRequest
|
||||
.then(result => {
|
||||
syntaxRequestState = RequestState.Resolved;
|
||||
if (semanticRequestState === RequestState.Errored) {
|
||||
// We've gone out of sync
|
||||
this._delegate.onFatalError(command);
|
||||
}
|
||||
return result;
|
||||
}, err => {
|
||||
syntaxRequestState = RequestState.Errored;
|
||||
if (semanticRequestState === RequestState.Resolved) {
|
||||
// We've gone out of sync
|
||||
this._delegate.onFatalError(command);
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
return syntaxRequest;
|
||||
} else {
|
||||
|
|
|
@ -18,7 +18,7 @@ import { PluginManager } from '../utils/plugins';
|
|||
import TelemetryReporter from '../utils/telemetry';
|
||||
import Tracer from '../utils/tracer';
|
||||
import { TypeScriptVersion, TypeScriptVersionProvider } from '../utils/versionProvider';
|
||||
import { ITypeScriptServer, PipeRequestCanceller, ProcessBasedTsServer, SyntaxRoutingTsServer, TsServerProcess } from './server';
|
||||
import { ITypeScriptServer, PipeRequestCanceller, ProcessBasedTsServer, SyntaxRoutingTsServer, TsServerProcess, TsServerDelegate } from './server';
|
||||
|
||||
type ServerKind = 'main' | 'syntax' | 'semantic';
|
||||
|
||||
|
@ -35,12 +35,13 @@ export class TypeScriptServerSpawner {
|
|||
public spawn(
|
||||
version: TypeScriptVersion,
|
||||
configuration: TypeScriptServiceConfiguration,
|
||||
pluginManager: PluginManager
|
||||
pluginManager: PluginManager,
|
||||
delegate: TsServerDelegate,
|
||||
): ITypeScriptServer {
|
||||
if (this.shouldUseSeparateSyntaxServer(version, configuration)) {
|
||||
const syntaxServer = this.spawnTsServer('syntax', version, configuration, pluginManager);
|
||||
const semanticServer = this.spawnTsServer('semantic', version, configuration, pluginManager);
|
||||
return new SyntaxRoutingTsServer(syntaxServer, semanticServer);
|
||||
return new SyntaxRoutingTsServer(syntaxServer, semanticServer, delegate);
|
||||
}
|
||||
|
||||
return this.spawnTsServer('main', version, configuration, pluginManager);
|
||||
|
@ -65,7 +66,7 @@ export class TypeScriptServerSpawner {
|
|||
|
||||
if (TypeScriptServerSpawner.isLoggingEnabled(apiVersion, configuration)) {
|
||||
if (tsServerLogFile) {
|
||||
this._logger.info(`<${kind}> Log file: ${tsServerLogFile}`);
|
||||
this._logger.info(`<${kind}> Log file: ${tsServerLogFile}`);
|
||||
} else {
|
||||
this._logger.error(`<${kind}> Could not create log directory`);
|
||||
}
|
||||
|
|
|
@ -288,7 +288,9 @@ export default class TypeScriptServiceClient extends Disposable implements IType
|
|||
const apiVersion = this.versionPicker.currentVersion.apiVersion || API.defaultVersion;
|
||||
this.onDidChangeTypeScriptVersion(currentVersion);
|
||||
let mytoken = ++this.token;
|
||||
const handle = this.typescriptServerSpawner.spawn(currentVersion, this.configuration, this.pluginManager);
|
||||
const handle = this.typescriptServerSpawner.spawn(currentVersion, this.configuration, this.pluginManager, {
|
||||
onFatalError: (command) => this.fatalError(command),
|
||||
});
|
||||
this.serverState = new ServerState.Running(handle, apiVersion, undefined, true);
|
||||
this.lastStart = Date.now();
|
||||
|
||||
|
@ -665,7 +667,10 @@ export default class TypeScriptServiceClient extends Disposable implements IType
|
|||
this.logTelemetry('fatalError', { command });
|
||||
console.error(`A non-recoverable error occured while executing tsserver command: ${command}`);
|
||||
|
||||
this.restartTsServer();
|
||||
if (this.serverState.type === ServerState.Type.Running) {
|
||||
this.info('Killing TS Server');
|
||||
this.serverState.server.kill();
|
||||
}
|
||||
}
|
||||
|
||||
private dispatchEvent(event: Proto.Event) {
|
||||
|
|
Loading…
Reference in a new issue