Use more explicit state for TS server states

Try to prevent the tracked server state from getting into weird invalid states and make the state more explicit
This commit is contained in:
Matt Bierner
2019-01-11 11:35:51 -08:00
parent 2b0cee0eba
commit 6682006c8b

View File

@@ -34,6 +34,42 @@ export interface TsDiagnostics {
readonly diagnostics: Proto.Diagnostic[]; readonly diagnostics: Proto.Diagnostic[];
} }
namespace ServerState {
export const enum Type {
None,
Running,
Errored
}
export const None = new class { readonly type = Type.None; };
export class Running {
readonly type = Type.Running;
constructor(
public readonly server: TypeScriptServer,
/**
* API version obtained from the version picker after checking the corresponding path exists.
*/
public readonly apiVersion: API,
/**
* Version reported by currently-running tsserver.
*/
public tsserverVersion: string | undefined,
) { }
}
export class Errored {
readonly type = Type.Errored;
constructor(
public readonly error: Error,
) { }
}
export type State = typeof None | Running | Errored;
}
export default class TypeScriptServiceClient extends Disposable implements ITypeScriptServiceClient { export default class TypeScriptServiceClient extends Disposable implements ITypeScriptServiceClient {
private static readonly WALK_THROUGH_SNIPPET_SCHEME_COLON = `${fileSchemes.walkThroughSnippet}:`; private static readonly WALK_THROUGH_SNIPPET_SCHEME_COLON = `${fileSchemes.walkThroughSnippet}:`;
@@ -49,23 +85,13 @@ export default class TypeScriptServiceClient extends Disposable implements IType
public readonly logger: Logger = new Logger(); public readonly logger: Logger = new Logger();
private readonly typescriptServerSpawner: TypeScriptServerSpawner; private readonly typescriptServerSpawner: TypeScriptServerSpawner;
private forkedTsServer: TypeScriptServer | null; private serverState: ServerState.State = ServerState.None;
private lastError: Error | null;
private lastStart: number; private lastStart: number;
private numberRestarts: number; private numberRestarts: number;
private isRestarting: boolean = false; private isRestarting: boolean = false;
private loadingIndicator = new ServerInitializingIndicator(); private loadingIndicator = new ServerInitializingIndicator();
public readonly telemetryReporter: TelemetryReporter; public readonly telemetryReporter: TelemetryReporter;
/**
* API version obtained from the version picker after checking the corresponding path exists.
*/
private _apiVersion: API;
/**
* Version reported by currently-running tsserver.
*/
private _tsserverVersion: string | undefined;
public readonly bufferSyncSupport: BufferSyncSupport; public readonly bufferSyncSupport: BufferSyncSupport;
public readonly diagnosticsManager: DiagnosticsManager; public readonly diagnosticsManager: DiagnosticsManager;
@@ -87,8 +113,6 @@ export default class TypeScriptServiceClient extends Disposable implements IType
}); });
this._onReady!.promise = p; this._onReady!.promise = p;
this.forkedTsServer = null;
this.lastError = null;
this.numberRestarts = 0; this.numberRestarts = 0;
this._configuration = TypeScriptServiceConfiguration.loadFromWorkspace(); this._configuration = TypeScriptServiceConfiguration.loadFromWorkspace();
@@ -96,8 +120,6 @@ export default class TypeScriptServiceClient extends Disposable implements IType
this.pluginPathsProvider = new TypeScriptPluginPathsProvider(this._configuration); this.pluginPathsProvider = new TypeScriptPluginPathsProvider(this._configuration);
this.versionPicker = new TypeScriptVersionPicker(this.versionProvider, this.workspaceState); this.versionPicker = new TypeScriptVersionPicker(this.versionProvider, this.workspaceState);
this._apiVersion = API.defaultVersion;
this._tsserverVersion = undefined;
this.tracer = new Tracer(this.logger); this.tracer = new Tracer(this.logger);
this.bufferSyncSupport = new BufferSyncSupport(this, allModeIds); this.bufferSyncSupport = new BufferSyncSupport(this, allModeIds);
@@ -116,7 +138,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType
this.pluginPathsProvider.updateConfiguration(this._configuration); this.pluginPathsProvider.updateConfiguration(this._configuration);
this.tracer.updateConfiguration(); this.tracer.updateConfiguration();
if (this.forkedTsServer) { if (this.serverState.type === ServerState.Type.Running) {
if (this._configuration.checkJs !== oldConfiguration.checkJs if (this._configuration.checkJs !== oldConfiguration.checkJs
|| this._configuration.experimentalDecorators !== oldConfiguration.experimentalDecorators || this._configuration.experimentalDecorators !== oldConfiguration.experimentalDecorators
) { ) {
@@ -129,7 +151,14 @@ export default class TypeScriptServiceClient extends Disposable implements IType
} }
}, this, this._disposables); }, this, this._disposables);
this.telemetryReporter = this._register(new TelemetryReporter(() => this._tsserverVersion || this._apiVersion.versionString)); this.telemetryReporter = this._register(new TelemetryReporter(() => {
if (this.serverState.type === ServerState.Type.Running) {
if (this.serverState.tsserverVersion) {
return this.serverState.tsserverVersion;
}
}
return this.apiVersion.versionString;
}));
this.typescriptServerSpawner = new TypeScriptServerSpawner(this.versionProvider, this.logDirectoryProvider, this.pluginPathsProvider, this.logger, this.telemetryReporter, this.tracer); this.typescriptServerSpawner = new TypeScriptServerSpawner(this.versionProvider, this.logDirectoryProvider, this.pluginPathsProvider, this.logger, this.telemetryReporter, this.tracer);
@@ -147,22 +176,21 @@ export default class TypeScriptServiceClient extends Disposable implements IType
this.bufferSyncSupport.dispose(); this.bufferSyncSupport.dispose();
if (this.forkedTsServer) { if (this.serverState.type === ServerState.Type.Running) {
this.forkedTsServer.kill(); this.serverState.server.kill();
} }
this.loadingIndicator.reset(); this.loadingIndicator.reset();
} }
public restartTsServer(): void { public restartTsServer(): void {
if (this.forkedTsServer) { if (this.serverState.type === ServerState.Type.Running) {
this.info('Killing TS Server'); this.info('Killing TS Server');
this.isRestarting = true; this.isRestarting = true;
this.forkedTsServer.kill(); this.serverState.server.kill();
this.resetClientVersion();
} }
this.forkedTsServer = this.startService(true); this.serverState = this.startService(true);
} }
private readonly _onTsServerStarted = this._register(new vscode.EventEmitter<API>()); private readonly _onTsServerStarted = this._register(new vscode.EventEmitter<API>());
@@ -193,7 +221,10 @@ export default class TypeScriptServiceClient extends Disposable implements IType
public readonly onSurveyReady = this._onSurveyReady.event; public readonly onSurveyReady = this._onSurveyReady.event;
public get apiVersion(): API { public get apiVersion(): API {
return this._apiVersion; if (this.serverState.type === ServerState.Type.Running) {
return this.serverState.apiVersion;
}
return API.defaultVersion;
} }
public onReady(f: () => void): Promise<void> { public onReady(f: () => void): Promise<void> {
@@ -212,30 +243,30 @@ export default class TypeScriptServiceClient extends Disposable implements IType
this.telemetryReporter.logTelemetry(eventName, properties); this.telemetryReporter.logTelemetry(eventName, properties);
} }
private service(): TypeScriptServer | null { private service(): TypeScriptServer {
if (this.forkedTsServer) { if (this.serverState.type === ServerState.Type.Running) {
return this.forkedTsServer; return this.serverState.server;
} }
if (this.lastError) { if (this.serverState.type === ServerState.Type.Errored) {
throw this.lastError; throw this.serverState.error;
} }
this.startService(); const newState = this.startService();
if (this.forkedTsServer) { if (newState.type === ServerState.Type.Running) {
return this.forkedTsServer; return newState.server;
} }
throw new Error('Could not create TS service'); throw new Error('Could not create TS service');
} }
public ensureServiceStarted() { public ensureServiceStarted() {
if (!this.forkedTsServer) { if (this.serverState.type !== ServerState.Type.Running) {
this.startService(); this.startService();
} }
} }
private token: number = 0; private token: number = 0;
private startService(resendModels: boolean = false): TypeScriptServer | null { private startService(resendModels: boolean = false): ServerState.State {
if (this.isDisposed) { if (this.isDisposed) {
return null; return ServerState.None;
} }
let currentVersion = this.versionPicker.currentVersion; let currentVersion = this.versionPicker.currentVersion;
@@ -248,13 +279,13 @@ export default class TypeScriptServiceClient extends Disposable implements IType
currentVersion = this.versionPicker.currentVersion; currentVersion = this.versionPicker.currentVersion;
} }
this._apiVersion = this.versionPicker.currentVersion.version || API.defaultVersion; const apiVersion = this.versionPicker.currentVersion.version || API.defaultVersion;
this.onDidChangeTypeScriptVersion(currentVersion); this.onDidChangeTypeScriptVersion(currentVersion);
this.lastError = null;
let mytoken = ++this.token; let mytoken = ++this.token;
const handle = this.typescriptServerSpawner.spawn(currentVersion, this.configuration, this.pluginManager); const handle = this.typescriptServerSpawner.spawn(currentVersion, this.configuration, this.pluginManager);
this.serverState = new ServerState.Running(handle, apiVersion, undefined);
this.lastStart = Date.now(); this.lastStart = Date.now();
handle.onError((err: Error) => { handle.onError((err: Error) => {
@@ -267,7 +298,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType
vscode.window.showErrorMessage(localize('serverExitedWithError', 'TypeScript language server exited with error. Error message is: {0}', err.message || err.name)); vscode.window.showErrorMessage(localize('serverExitedWithError', 'TypeScript language server exited with error. Error message is: {0}', err.message || err.name));
} }
this.lastError = err; this.serverState = new ServerState.Errored(err);
this.error('TSServer errored with error.', err); this.error('TSServer errored with error.', err);
if (handle.tsServerLogFile) { if (handle.tsServerLogFile) {
this.error(`TSServer log file: ${handle.tsServerLogFile}`); this.error(`TSServer log file: ${handle.tsServerLogFile}`);
@@ -282,7 +313,6 @@ export default class TypeScriptServiceClient extends Disposable implements IType
*/ */
this.logTelemetry('tsserver.error'); this.logTelemetry('tsserver.error');
this.serviceExited(false); this.serviceExited(false);
this.resetClientVersion();
}); });
handle.onExit((code: any) => { handle.onExit((code: any) => {
@@ -317,16 +347,15 @@ export default class TypeScriptServiceClient extends Disposable implements IType
handle.onEvent(event => this.dispatchEvent(event)); handle.onEvent(event => this.dispatchEvent(event));
this._onReady!.resolve(); this._onReady!.resolve();
this.forkedTsServer = handle;
this._onTsServerStarted.fire(currentVersion.version); this._onTsServerStarted.fire(currentVersion.version);
if (this._apiVersion.gte(API.v300)) { if (apiVersion.gte(API.v300)) {
this.loadingIndicator.startedLoadingProject(undefined /* projectName */); this.loadingIndicator.startedLoadingProject(undefined /* projectName */);
} }
this.serviceStarted(resendModels); this.serviceStarted(resendModels);
return handle; return this.serverState;
} }
public onVersionStatusClicked(): Thenable<void> { public onVersionStatusClicked(): Thenable<void> {
@@ -372,7 +401,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType
return false; return false;
} }
if (!this.forkedTsServer || !this.forkedTsServer.tsServerLogFile) { if (this.serverState.type !== ServerState.Type.Running || !this.serverState.server.tsServerLogFile) {
vscode.window.showWarningMessage(localize( vscode.window.showWarningMessage(localize(
'typescript.openTsServerLog.noLogFile', 'typescript.openTsServerLog.noLogFile',
'TS Server has not started logging.')); 'TS Server has not started logging.'));
@@ -380,7 +409,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType
} }
try { try {
await vscode.commands.executeCommand('revealFileInOS', vscode.Uri.file(this.forkedTsServer.tsServerLogFile)); await vscode.commands.executeCommand('revealFileInOS', vscode.Uri.file(this.serverState.server.tsServerLogFile));
return true; return true;
} catch { } catch {
vscode.window.showWarningMessage(localize( vscode.window.showWarningMessage(localize(
@@ -437,10 +466,8 @@ export default class TypeScriptServiceClient extends Disposable implements IType
id: MessageAction; id: MessageAction;
} }
this.forkedTsServer = null; this.serverState = ServerState.None;
if (!restart) { if (restart) {
this.resetClientVersion();
} else {
const diff = Date.now() - this.lastStart; const diff = Date.now() - this.lastStart;
this.numberRestarts++; this.numberRestarts++;
let startService = true; let startService = true;
@@ -464,7 +491,6 @@ export default class TypeScriptServiceClient extends Disposable implements IType
} }
*/ */
this.logTelemetry('serviceExited'); this.logTelemetry('serviceExited');
this.resetClientVersion();
} else if (diff < 60 * 1000 /* 1 Minutes */) { } else if (diff < 60 * 1000 /* 1 Minutes */) {
this.lastStart = Date.now(); this.lastStart = Date.now();
prompt = vscode.window.showWarningMessage<MyMessageItem>( prompt = vscode.window.showWarningMessage<MyMessageItem>(
@@ -490,7 +516,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType
} }
public normalizedPath(resource: vscode.Uri): string | undefined { public normalizedPath(resource: vscode.Uri): string | undefined {
if (this._apiVersion.gte(API.v213)) { if (this.apiVersion.gte(API.v213)) {
if (resource.scheme === fileSchemes.walkThroughSnippet || resource.scheme === fileSchemes.untitled) { if (resource.scheme === fileSchemes.walkThroughSnippet || resource.scheme === fileSchemes.untitled) {
const dirName = path.dirname(resource.path); const dirName = path.dirname(resource.path);
const fileName = this.inMemoryResourcePrefix + path.basename(resource.path); const fileName = this.inMemoryResourcePrefix + path.basename(resource.path);
@@ -524,11 +550,11 @@ export default class TypeScriptServiceClient extends Disposable implements IType
} }
private get inMemoryResourcePrefix(): string { private get inMemoryResourcePrefix(): string {
return this._apiVersion.gte(API.v270) ? '^' : ''; return this.apiVersion.gte(API.v270) ? '^' : '';
} }
public toResource(filepath: string): vscode.Uri { public toResource(filepath: string): vscode.Uri {
if (this._apiVersion.gte(API.v213)) { if (this.apiVersion.gte(API.v213)) {
if (filepath.startsWith(TypeScriptServiceClient.WALK_THROUGH_SNIPPET_SCHEME_COLON) || (filepath.startsWith(fileSchemes.untitled + ':')) if (filepath.startsWith(TypeScriptServiceClient.WALK_THROUGH_SNIPPET_SCHEME_COLON) || (filepath.startsWith(fileSchemes.untitled + ':'))
) { ) {
let resource = vscode.Uri.parse(filepath); let resource = vscode.Uri.parse(filepath);
@@ -694,7 +720,9 @@ export default class TypeScriptServiceClient extends Disposable implements IType
break; break;
} }
if (telemetryData.telemetryEventName === 'projectInfo') { if (telemetryData.telemetryEventName === 'projectInfo') {
this._tsserverVersion = properties['version']; if (this.serverState.type === ServerState.Type.Running) {
this.serverState = new ServerState.Running(this.serverState.server, this.serverState.apiVersion, properties['version']);
}
} }
/* __GDPR__ /* __GDPR__
@@ -711,13 +739,9 @@ export default class TypeScriptServiceClient extends Disposable implements IType
this.logTelemetry(telemetryData.telemetryEventName, properties); this.logTelemetry(telemetryData.telemetryEventName, properties);
} }
private resetClientVersion() {
this._apiVersion = API.defaultVersion;
this._tsserverVersion = undefined;
}
private configurePlugin(pluginName: string, configuration: {}): any { private configurePlugin(pluginName: string, configuration: {}): any {
if (this._apiVersion.gte(API.v314)) { if (this.apiVersion.gte(API.v314)) {
this.executeWithoutWaitingForResponse('configurePlugin', { pluginName, configuration }); this.executeWithoutWaitingForResponse('configurePlugin', { pluginName, configuration });
} }
} }