From 181a337c4a737b961ce914d728a3dc2ef54fddab Mon Sep 17 00:00:00 2001 From: Qiming Zhao Date: Wed, 22 Dec 2021 16:53:53 +0800 Subject: [PATCH] refactor(server): avoid tsserverForkStart --- bin/tsserverForkStart.js | 161 ------------ src/index.ts | 7 +- src/server/features/bufferSyncSupport.ts | 4 +- src/server/index.ts | 13 +- src/server/languageProvider.ts | 9 +- src/server/tsServerProcess.ts | 55 ++++ src/server/typescriptServiceClient.ts | 294 ++++++++-------------- src/server/typescriptServiceClientHost.ts | 48 ++-- src/server/utils/process.ts | 112 +-------- 9 files changed, 199 insertions(+), 504 deletions(-) delete mode 100644 bin/tsserverForkStart.js create mode 100644 src/server/tsServerProcess.ts diff --git a/bin/tsserverForkStart.js b/bin/tsserverForkStart.js deleted file mode 100644 index 22abfa6..0000000 --- a/bin/tsserverForkStart.js +++ /dev/null @@ -1,161 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -let net = require('net'); -let fs = require('fs'); -let ENABLE_LOGGING = false; -let log = (function () { - if (!ENABLE_LOGGING) { - return function () { }; // tslint:disable-line - } - let isFirst = true; - let LOG_LOCATION = 'C:\\stdFork.log'; - return function log(str) { - if (isFirst) { - isFirst = false; - fs.writeFileSync(LOG_LOCATION, str + '\n'); - return; - } - fs.appendFileSync(LOG_LOCATION, str + '\n'); - }; -})(); -let stdInPipeName = process.env['STDIN_PIPE_NAME']; // tslint:disable-line -let stdOutPipeName = process.env['STDOUT_PIPE_NAME']; // tslint:disable-line -let stdErrPipeName = process.env['STDERR_PIPE_NAME']; // tslint:disable-line -log('STDIN_PIPE_NAME: ' + stdInPipeName); -log('STDOUT_PIPE_NAME: ' + stdOutPipeName); -log('STDERR_PIPE_NAME: ' + stdErrPipeName); -(function () { - log('Beginning stdout redirection...'); - // Create a writing stream to the stdout pipe - let stdOutStream = net.connect(stdOutPipeName); - // unref stdOutStream to behave like a normal standard out - stdOutStream.unref(); - process.__defineGetter__('stdout', function () { - return stdOutStream; - }); - // Create a writing stream to the stderr pipe - let stdErrStream = net.connect(stdErrPipeName); - // unref stdErrStream to behave like a normal standard out - stdErrStream.unref(); - process.__defineGetter__('stderr', function () { - return stdErrStream; - }); - let fsWriteSyncString = function (// tslint:disable-line - fd, str, _position, encoding) { - // fs.writeSync(fd, string[, position[, encoding]]) - let buf = Buffer.from(str, encoding || 'utf8'); - return fsWriteSyncBuffer(fd, buf, 0, buf.length); // tslint:disable-line - }; - let fsWriteSyncBuffer = function (// tslint:disable-line - fd, buffer, off, len) { - off = Math.abs(off | 0); - len = Math.abs(len | 0); - // fs.writeSync(fd, buffer, offset, length[, position]) - let buffer_length = buffer.length; - if (off > buffer_length) { - throw new Error('offset out of bounds'); - } - if (len > buffer_length) { - throw new Error('length out of bounds'); - } - if (((off + len) | 0) < off) { - throw new Error('off + len overflow'); - } - if (buffer_length - off < len) { - // Asking for more than is left over in the buffer - throw new Error('off + len > buffer.length'); - } - let slicedBuffer = buffer; - if (off !== 0 || len !== buffer_length) { - slicedBuffer = buffer.slice(off, off + len); - } - if (fd === 1) { - stdOutStream.write(slicedBuffer); - } - else { - stdErrStream.write(slicedBuffer); - } - return slicedBuffer.length; - }; - // handle fs.writeSync(1, ...) - let originalWriteSync = fs.writeSync; - fs.writeSync = function (// tslint:disable-line - fd, data, _position, _encoding) { - if (fd !== 1 && fd !== 2) { - return originalWriteSync.apply(fs, arguments); - } - // usage: - // fs.writeSync(fd, buffer, offset, length[, position]) - // OR - // fs.writeSync(fd, string[, position[, encoding]]) - if (data instanceof Buffer) { - return fsWriteSyncBuffer.apply(null, arguments); - } - // For compatibility reasons with fs.writeSync, writing null will write "null", etc - if (typeof data !== 'string') { - data += ''; - } - return fsWriteSyncString.apply(null, arguments); - }; - log('Finished defining process.stdout, process.stderr and fs.writeSync'); -})(); -(function () { - // Begin listening to stdin pipe - let server = net.createServer(function (stream) { - // Stop accepting new connections, keep the existing one alive - server.close(); - log('Parent process has connected to my stdin. All should be good now.'); - process.__defineGetter__('stdin', function () { - return stream; - }); - // Remove myself from process.argv - process.argv.splice(1, 1); - // Load the actual program - let program = process.argv[1]; - log('Loading program: ' + program); - // Unset the custom environmental variables that should not get inherited - delete process.env['STDIN_PIPE_NAME']; // tslint:disable-line - delete process.env['STDOUT_PIPE_NAME']; // tslint:disable-line - delete process.env['STDERR_PIPE_NAME']; // tslint:disable-line - require(program); - log('Finished loading program.'); - let stdinIsReferenced = true; - let timer = setInterval(function () { - let listenerCount = stream.listeners('data').length + - stream.listeners('end').length + - stream.listeners('close').length + - stream.listeners('error').length; - // log('listenerCount: ' + listenerCount) - if (listenerCount <= 1) { - // No more "actual" listeners, only internal node - if (stdinIsReferenced) { - stdinIsReferenced = false; - // log('unreferencing stream!!!') - stream.unref(); - } - } - else { - // There are "actual" listeners - if (!stdinIsReferenced) { - stdinIsReferenced = true; - stream.ref(); - } - } - // log( - // '' + stream.listeners('data').length + - // ' ' + stream.listeners('end').length + - // ' ' + stream.listeners('close').length + - // ' ' + stream.listeners('error').length - // ) - }, 1000); - if (timer.unref) { // tslint:disable-line - timer.unref(); // tslint:disable-line - } - }); - server.listen(stdInPipeName, function () { - // signal via stdout that the parent process can now begin writing to stdin pipe - process.stdout.write('ready'); - }); -})(); diff --git a/src/index.ts b/src/index.ts index 18f2845..28b7c42 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,12 +29,7 @@ export async function activate(context: ExtensionContext): Promise { registCommand({ id: 'tsserver.restart', execute: (): void => { - // tslint:disable-next-line:no-floating-promises - service.stop().then(() => { - setTimeout(() => { - service.restart() - }, 100) - }) + service.restart() } }) diff --git a/src/server/features/bufferSyncSupport.ts b/src/server/features/bufferSyncSupport.ts index 627108d..a7ae050 100644 --- a/src/server/features/bufferSyncSupport.ts +++ b/src/server/features/bufferSyncSupport.ts @@ -345,9 +345,7 @@ export default class BufferSyncSupport { } public listen(): void { - if (this.listening) { - return - } + if (this.listening) return this.listening = true workspace.onDidOpenTextDocument( this.openTextDocument, diff --git a/src/server/index.ts b/src/server/index.ts index 92a0b1a..3637093 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -58,18 +58,14 @@ export default class TsserverService implements IServiceProvider { this.disposables.push(this.clientHost) let client = this.clientHost.serviceClient return new Promise(resolve => { - let started = false - client.onTsServerStarted(() => { + client.onReady(() => { Object.defineProperty(this, 'state', { get: () => { return this.clientHost.serviceClient.state } }) this._onDidServiceReady.fire(void 0) - if (!started) { - started = true - resolve() - } + resolve() }) }) } @@ -78,10 +74,10 @@ export default class TsserverService implements IServiceProvider { disposeAll(this.disposables) } - public async restart(): Promise { + public restart(): void { if (!this.clientHost) return let client = this.clientHost.serviceClient - await client.restartTsServer() + client.restartTsServer() } public async stop(): Promise { @@ -89,6 +85,5 @@ export default class TsserverService implements IServiceProvider { this.clientHost.reset() let client = this.clientHost.serviceClient await client.stop() - return } } diff --git a/src/server/languageProvider.ts b/src/server/languageProvider.ts index 26368d2..c68d29f 100644 --- a/src/server/languageProvider.ts +++ b/src/server/languageProvider.ts @@ -49,13 +49,8 @@ export default class LanguageProvider { ) { workspace.onDidChangeConfiguration(this.configurationChanged, this, this.disposables) this.configurationChanged() - - let initialized = false - client.onTsServerStarted(async () => { // tslint:disable-line - if (!initialized) { - initialized = true - this.registerProviders(client, typingsStatus) - } + client.onReady(() => { + this.registerProviders(client, typingsStatus) }) } diff --git a/src/server/tsServerProcess.ts b/src/server/tsServerProcess.ts new file mode 100644 index 0000000..5877237 --- /dev/null +++ b/src/server/tsServerProcess.ts @@ -0,0 +1,55 @@ + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import cp from 'child_process' +import { Disposable } from 'vscode-languageserver-protocol' +import * as Proto from './protocol' +import { Reader } from './utils/wireProtocol' + +export interface ToCancelOnResourceChanged { + readonly resource: string + cancel(): void +} + +export default class ForkedTsServerProcess implements Disposable { + private readonly _reader: Reader + + constructor(private childProcess: cp.ChildProcess) { + this._reader = new Reader(this.childProcess.stdout) + } + + public readonly toCancelOnResourceChange = new Set() + + public onExit(cb: (err: any, signal: string) => void): void { + this.childProcess.on('exit', cb) + } + + public write(serverRequest: Proto.Request): void { + this.childProcess.stdin.write( + JSON.stringify(serverRequest) + '\r\n', + 'utf8' + ) + } + + public onData(handler: (data: Proto.Response) => void): void { + this._reader.onData(handler) + } + + public onError(handler: (err: Error) => void): void { + this.childProcess.on('error', handler) + this._reader.onError(handler) + } + + public kill(): void { + this.toCancelOnResourceChange.clear() + this.childProcess.kill() + this._reader.dispose() + } + + public dispose(): void { + this.toCancelOnResourceChange.clear() + this._reader.dispose() + } +} diff --git a/src/server/typescriptServiceClient.ts b/src/server/typescriptServiceClient.ts index e009667..acaa9ad 100644 --- a/src/server/typescriptServiceClient.ts +++ b/src/server/typescriptServiceClient.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import cp from 'child_process' import { Document, ServiceStat, Uri, window, workspace } from 'coc.nvim' import fs from 'fs' import os from 'os' @@ -25,51 +24,7 @@ import Tracer from './utils/tracer' import { inferredProjectConfig } from './utils/tsconfig' import { TypeScriptVersion, TypeScriptVersionProvider } from './utils/versionProvider' import VersionStatus from './utils/versionStatus' -import { Reader } from './utils/wireProtocol' - -interface ToCancelOnResourceChanged { - readonly resource: string - cancel(): void -} - -class ForkedTsServerProcess implements Disposable { - private readonly _reader: Reader - - constructor(private childProcess: cp.ChildProcess) { - this._reader = new Reader(this.childProcess.stdout) - } - - public readonly toCancelOnResourceChange = new Set() - - public onExit(cb: (err: any) => void): void { - this.childProcess.on('exit', cb) - } - - public write(serverRequest: Proto.Request): void { - this.childProcess.stdin.write( - JSON.stringify(serverRequest) + '\r\n', - 'utf8' - ) - } - - public onData(handler: (data: Proto.Response) => void): void { - this._reader.onData(handler) - } - - public onError(handler: (err: Error) => void): void { - this.childProcess.on('error', handler) - this._reader.onError(handler) - } - - public kill(): void { - this.childProcess.kill() - this._reader.dispose() - } - - public dispose(): void { - this._reader.dispose() - } -} +import ForkedTsServerProcess, { ToCancelOnResourceChanged } from './tsServerProcess' export interface TsDiagnostics { readonly kind: DiagnosticKind @@ -78,6 +33,7 @@ export interface TsDiagnostics { } export default class TypeScriptServiceClient implements ITypeScriptServiceClient { + private token: number = 0 public state = ServiceStat.Initial public readonly logger: Logger = new Logger() public readonly bufferSyncSupport: BufferSyncSupport @@ -90,14 +46,13 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient private versionProvider: TypeScriptVersionProvider private tsServerLogFile: string | null = null private tsServerProcess: ForkedTsServerProcess | undefined - private servicePromise: Thenable | null - private lastError: Error | null private lastStart: number private numberRestarts: number private cancellationPipeName: string | null = null private _callbacks = new CallbackMap() private _requestQueue = new RequestQueue() private _pendingResponses = new Set() + private _onReady?: { promise: Promise; resolve: () => void; reject: () => void } private versionStatus: VersionStatus private readonly _onTsServerStarted = new Emitter() @@ -118,9 +73,15 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient ) { this.pathSeparator = path.sep this.lastStart = Date.now() - this.servicePromise = null - this.lastError = null this.numberRestarts = 0 + let resolve: () => void + let reject: () => void + const p = new Promise((res, rej) => { + resolve = res + reject = rej + }) + this._onReady = { promise: p, resolve: resolve!, reject: reject! } + this.fileConfigurationManager = new FileConfigurationManager(this) this._configuration = TypeScriptServiceConfiguration.loadFromWorkspace() this.versionProvider = new TypeScriptVersionProvider(this._configuration) @@ -136,7 +97,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient }, null, this.disposables) this.bufferSyncSupport = new BufferSyncSupport(this, modeIds) - this.onTsServerStarted(() => { + this.onReady(() => { this.bufferSyncSupport.listen() }) @@ -169,14 +130,12 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient return this._configuration } + public onReady(f: () => void): Promise { + return this._onReady!.promise.then(f) + } + public dispose(): void { - if (this.servicePromise) { - this.servicePromise - .then(childProcess => { - childProcess.kill() - }) - .then(undefined, () => void 0) - } + this.tsServerProcess.kill() this.bufferSyncSupport.dispose() this.logger.dispose() this._onTsServerStarted.dispose() @@ -192,40 +151,28 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient this.logger.error(message, data) } - public restartTsServer(): Promise { - const start = () => { - this.servicePromise = this.startService(true) - return this.servicePromise - } - - if (this.servicePromise) { - return Promise.resolve(this.servicePromise.then(childProcess => { - this.state = ServiceStat.Stopping - this.info('Killing TS Server') - this.isRestarting = true - childProcess.kill() - this.servicePromise = null - }).then(start)) - } else { - return Promise.resolve(start()) + public restartTsServer(): void { + if (this.tsServerProcess) { + this.state = ServiceStat.Stopping + this.info('Killing TS Server') + this.isRestarting = true + this.tsServerProcess.kill() } + this.startService(true) } public stop(): Promise { - if (!this.servicePromise) return - return new Promise((resolve, reject) => { - this.servicePromise.then(childProcess => { - if (this.state == ServiceStat.Running) { - this.info('Killing TS Server') - childProcess.onExit(() => { - resolve() - }) - childProcess.kill() - this.servicePromise = null - } else { + return new Promise(resolve => { + let { tsServerProcess } = this + if (tsServerProcess && this.state == ServiceStat.Running) { + this.info('Killing TS Server') + tsServerProcess.onExit(() => { resolve() - } - }, reject) + }) + tsServerProcess.kill() + } else { + resolve() + } }) } @@ -259,52 +206,34 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient return this._tscPath } - private service(): Thenable { - if (this.servicePromise) { - return this.servicePromise - } - if (this.lastError) { - return Promise.reject(this.lastError) - } - return this.startService().then(() => { - if (this.servicePromise) { - return this.servicePromise - } - }) - } - public ensureServiceStarted(): void { - if (!this.servicePromise) { - this.startService().catch(err => { - window.showMessage(`TSServer start failed: ${err.message}`, 'error') - this.error(`Service start failed: ${err.stack}`) - }) + if (!this.tsServerProcess) { + this.startService() } } - private async startService(resendModels = false): Promise { + private startService(resendModels = false): ForkedTsServerProcess | undefined { const { ignoreLocalTsserver } = this.configuration let currentVersion: TypeScriptVersion if (!ignoreLocalTsserver) currentVersion = this.versionProvider.getLocalVersion() if (!currentVersion || !fs.existsSync(currentVersion.tsServerPath)) { + this.info('Local tsserver not found, using bundled tsserver with coc-tsserver.') currentVersion = this.versionProvider.getDefaultVersion() } if (!currentVersion || !currentVersion.isValid) { if (this.configuration.globalTsdk) { - window.showMessage(`Can not find typescript module, in 'tsserver.tsdk': ${this.configuration.globalTsdk}`, 'error') + window.showErrorMessage(`Can not find typescript module, in 'tsserver.tsdk': ${this.configuration.globalTsdk}`) } else { - window.showMessage(`Can not find typescript module, run ':CocInstall coc-tsserver' to fix it!`, 'error') + window.showErrorMessage(`Can not find typescript module, run ':CocInstall coc-tsserver' to fix it!`) } return } this._apiVersion = currentVersion.version this._tscPath = currentVersion.tscPath this.versionStatus.onDidChangeTypeScriptVersion(currentVersion) - this.lastError = null - const tsServerForkArgs = await this.getTsServerArgs(currentVersion) + const tsServerForkArgs = this.getTsServerArgs(currentVersion) const options = { execArgv: this.getExecArgv() } - this.servicePromise = this.startProcess(currentVersion, tsServerForkArgs, options, resendModels) - return this.servicePromise + return this.startProcess(currentVersion, tsServerForkArgs, options, resendModels) } private getExecArgv(): string[] { @@ -322,64 +251,54 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient return args } - private startProcess(currentVersion: TypeScriptVersion, args: string[], options: IForkOptions, resendModels: boolean): Promise { + private startProcess(currentVersion: TypeScriptVersion, args: string[], options: IForkOptions, resendModels: boolean): ForkedTsServerProcess { + const myToken = ++this.token this.state = ServiceStat.Starting - return new Promise((resolve, reject) => { - try { - fork( - currentVersion.tsServerPath, - args, - options, - this.logger, - (err: any, childProcess: cp.ChildProcess | null) => { - if (err || !childProcess) { - this.state = ServiceStat.StartFailed - this.lastError = err - this.error('Starting TSServer failed with error.', err.stack) - return - } - this.state = ServiceStat.Running - this.info('Started TSServer', JSON.stringify(currentVersion, null, 2)) - const handle = new ForkedTsServerProcess(childProcess) - this.tsServerProcess = handle - this.lastStart = Date.now() - - handle.onError((err: Error) => { - this.lastError = err - this.error('TSServer errored with error.', err) - this.error(`TSServer log file: ${this.tsServerLogFile || ''}`) - window.showMessage(`TSServer errored with error. ${err.message}`, 'error') - this.serviceExited(false) - }) - handle.onExit((code: any) => { - if (code == null) { - this.info('TSServer normal exit') - } else { - this.error(`TSServer exited with code: ${code}`) - } - this.info(`TSServer log file: ${this.tsServerLogFile || ''}`) - this.serviceExited(!this.isRestarting) - this.isRestarting = false - handle.dispose() - }) - handle.onData(msg => { - this.dispatchMessage(msg) - }) - resolve(handle) - this.serviceStarted(resendModels) - this._onTsServerStarted.fire(currentVersion.version) - } - ) - } catch (e) { - reject(e) - } - }) + try { + let childProcess = fork(currentVersion.tsServerPath, args, options, this.logger) + this.state = ServiceStat.Running + this.info('Starting TSServer', JSON.stringify(currentVersion, null, 2)) + const handle = new ForkedTsServerProcess(childProcess) + this.tsServerProcess = handle + this.lastStart = Date.now() + handle.onError((err: Error) => { + if (this.token != myToken) return + window.showErrorMessage(`TypeScript language server exited with error. Error message is: ${err.message}`) + this.error('TSServer errored with error.', err) + this.error(`TSServer log file: ${this.tsServerLogFile || ''}`) + window.showMessage(`TSServer errored with error. ${err.message}`, 'error') + this.serviceExited(false) + }) + handle.onExit((code: any, signal: string) => { + handle.dispose() + if (this.token != myToken) return + if (code == null) { + this.info(`TSServer exited. Signal: ${signal}`) + } else { + this.error(`TSServer exited with code: ${code}. Signal: ${signal}`) + } + this.info(`TSServer log file: ${this.tsServerLogFile || ''}`) + this.serviceExited(!this.isRestarting) + this.isRestarting = false + }) + handle.onData(msg => { + this.dispatchMessage(msg) + }) + this.serviceStarted(resendModels) + this._onReady!.resolve() + this._onTsServerStarted.fire(currentVersion.version) + return handle + } catch (err) { + this.state = ServiceStat.StartFailed + this.error('Starting TSServer failed with error.', err.stack) + return undefined + } } public async openTsServerLogFile(): Promise { const isRoot = process.getuid && process.getuid() == 0 let echoErr = (msg: string) => { - window.showMessage(msg, 'error') + window.showErrorMessage(msg) } if (isRoot) { echoErr('Log disabled for root user.') @@ -457,7 +376,6 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient private serviceExited(restart: boolean): void { this.state = ServiceStat.Stopped - this.servicePromise = null this.tsServerLogFile = null this._callbacks.destroy('Service died.') this._callbacks = new CallbackMap() @@ -479,7 +397,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient } } if (startService) { - this.startService(true) // tslint:disable-line + this.startService(true) } } } @@ -491,7 +409,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient public toOpenedFilePath(uri: string, options: { suppressAlertOnFailure?: boolean } = {}): string | undefined { if (!this.bufferSyncSupport.ensureHasBuffer(uri)) { if (!options.suppressAlertOnFailure) { - console.error(`Unexpected resource ${uri}`) + this.error(`Unexpected resource ${uri}`) } return undefined } @@ -608,13 +526,14 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient } private fatalError(command: string, error: any): void { - console.error(`A non-recoverable error occured while executing tsserver command: ${command}`) - + this.error(`A non-recoverable error occured while executing tsserver command: ${command}`) if (this.state === ServiceStat.Running) { this.info('Killing TS Server by fatal error:', error) - this.service().then(service => { - service.kill() - }) + let { tsServerProcess } = this + if (tsServerProcess) { + this.tsServerProcess = undefined + tsServerProcess.kill() + } } } @@ -639,7 +558,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient private executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: CancellationToken, expectsResult: false, lowPriority?: boolean }): undefined private executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise> private executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise> | undefined { - if (this.servicePromise == null) { + if (!this.tsServerProcess) { return Promise.resolve(undefined) } this.bufferSyncSupport.beforeCommand(command) @@ -687,16 +606,15 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient if (requestItem.expectsResponse && !requestItem.isAsync) { this._pendingResponses.add(requestItem.request.seq) } - this.service().then(childProcess => { - try { - childProcess.write(serverRequest) - } catch (err) { - const callback = this.fetchCallback(serverRequest.seq) - if (callback) { - callback.onError(err) - } + if (!this.tsServerProcess) return + try { + this.tsServerProcess.write(serverRequest) + } catch (err) { + const callback = this.fetchCallback(serverRequest.seq) + if (callback) { + callback.onError(err) } - }) + } } private tryCancelRequest(seq: number, command: string): boolean { @@ -731,7 +649,6 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient if (!callback) { return undefined } - this._pendingResponses.delete(seq) return callback } @@ -849,7 +766,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient } } - private async getTsServerArgs(currentVersion: TypeScriptVersion): Promise { + private getTsServerArgs(currentVersion: TypeScriptVersion): string[] { const args: string[] = [] args.push('--allowLocalPluginLoads') @@ -974,11 +891,8 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient public configurePlugin(pluginName: string, configuration: {}): any { if (this.apiVersion.gte(API.v314)) { - if (!this.servicePromise) return - this.servicePromise.then(() => { - // tslint:disable-next-line: no-floating-promises - this.executeWithoutWaitingForResponse('configurePlugin', { pluginName, configuration }) - }) + if (!this.tsServerProcess) return + this.executeWithoutWaitingForResponse('configurePlugin', { pluginName, configuration }) } } diff --git a/src/server/typescriptServiceClientHost.ts b/src/server/typescriptServiceClientHost.ts index dea0311..0305b25 100644 --- a/src/server/typescriptServiceClientHost.ts +++ b/src/server/typescriptServiceClientHost.ts @@ -102,39 +102,41 @@ export default class TypeScriptServiceClientHost implements Disposable { ) this.languagePerId.set(description.id, manager) } - const languageIds = new Set() - for (const plugin of pluginManager.plugins) { - if (plugin.configNamespace && plugin.languages.length) { + this.client.ensureServiceStarted() + this.client.onReady(() => { + const languageIds = new Set() + for (const plugin of pluginManager.plugins) { + if (plugin.configNamespace && plugin.languages.length) { + this.registerExtensionLanguageProvider({ + id: plugin.configNamespace, + modeIds: Array.from(plugin.languages), + diagnosticSource: 'ts-plugin', + diagnosticLanguage: DiagnosticLanguage.TypeScript, + diagnosticOwner: 'typescript', + isExternal: true + }) + } else { + for (const language of plugin.languages) { + languageIds.add(language) + } + } + } + + if (languageIds.size) { this.registerExtensionLanguageProvider({ - id: plugin.configNamespace, - modeIds: Array.from(plugin.languages), + id: 'typescript-plugins', + modeIds: Array.from(languageIds.values()), diagnosticSource: 'ts-plugin', diagnosticLanguage: DiagnosticLanguage.TypeScript, diagnosticOwner: 'typescript', isExternal: true }) - } else { - for (const language of plugin.languages) { - languageIds.add(language) - } } - } - - if (languageIds.size) { - this.registerExtensionLanguageProvider({ - id: 'typescript-plugins', - modeIds: Array.from(languageIds.values()), - diagnosticSource: 'ts-plugin', - diagnosticLanguage: DiagnosticLanguage.TypeScript, - diagnosticOwner: 'typescript', - isExternal: true - }) - } - - this.client.ensureServiceStarted() + }) this.client.onTsServerStarted(() => { this.triggerAllDiagnostics() }) + workspace.onDidChangeConfiguration(this.configurationChanged, this, this.disposables) this.configurationChanged() } diff --git a/src/server/utils/process.ts b/src/server/utils/process.ts index c994d17..8580943 100644 --- a/src/server/utils/process.ts +++ b/src/server/utils/process.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import cp from 'child_process' -import net from 'net' import os from 'os' import path from 'path' import fs from 'fs' @@ -36,21 +35,6 @@ export function getTempDirectory(): string | undefined { return dir } -function generatePipeName(): string { - return getPipeName(makeRandomHexString(40)) -} - -function getPipeName(name: string): string | undefined { - const fullName = 'coc-tsc-' + name - if (process.platform === 'win32') { - return '\\\\.\\pipe\\' + fullName + '-sock' - } - const tmpdir = getTempDirectory() - if (!tmpdir) return undefined - // Mac/Unix: use socket file - return path.join(tmpdir, fullName + '.sock') -} - export function getTempFile(name: string): string | undefined { const fullName = 'coc-nvim-' + name let dir = getTempDirectory() @@ -70,20 +54,9 @@ export function createTempDirectory(name: string) { return res } -function generatePatchedEnv( - env: any, - stdInPipeName: string, - stdOutPipeName: string, - stdErrPipeName: string -): any { +function generatePatchedEnv(env: any, modulePath: string): any { const newEnv = Object.assign({}, env) - - // Set the two unique pipe names and the electron flag as process env - newEnv['STDIN_PIPE_NAME'] = stdInPipeName // tslint:disable-line - newEnv['STDOUT_PIPE_NAME'] = stdOutPipeName // tslint:disable-line - newEnv['STDERR_PIPE_NAME'] = stdErrPipeName // tslint:disable-line - newEnv['TSS_LOG'] = `-level verbose -file ${path.join(os.tmpdir(), 'coc-nvim-tsc.log')}` // tslint:disable-line - + newEnv['NODE_PATH'] = path.join(modulePath, '..', '..', '..') // Ensure we always have a PATH set newEnv['PATH'] = newEnv['PATH'] || process.env.PATH // tslint:disable-line return newEnv @@ -94,85 +67,14 @@ export function fork( args: string[], options: IForkOptions, logger: Logger, - callback: (error: any, cp: cp.ChildProcess | null) => void -): void { - let callbackCalled = false - const resolve = (result: cp.ChildProcess) => { - if (callbackCalled) return - callbackCalled = true - callback(null, result) - } - const reject = (err: any) => { - if (callbackCalled) return - callbackCalled = true - callback(err, null) - } - - // Generate three unique pipe names - const stdInPipeName = generatePipeName() - const stdOutPipeName = generatePipeName() - const stdErrPipeName = generatePipeName() - - const newEnv = generatePatchedEnv( - process.env, - stdInPipeName, - stdOutPipeName, - stdErrPipeName - ) - newEnv['NODE_PATH'] = path.join(modulePath, '..', '..', '..') - - let childProcess: cp.ChildProcess - // Begin listening to stderr pipe - let stdErrServer = net.createServer(stdErrStream => { - // From now on the childProcess.stderr is available for reading - childProcess.stderr = stdErrStream - }) - stdErrServer.listen(stdErrPipeName) - - // Begin listening to stdout pipe - let stdOutServer = net.createServer(stdOutStream => { - // The child process will write exactly one chunk with content `ready` when it has installed a listener to the stdin pipe - - stdOutStream.once('data', (_chunk: Buffer) => { - // The child process is sending me the `ready` chunk, time to connect to the stdin pipe - childProcess.stdin = net.connect(stdInPipeName) as any - - // From now on the childProcess.stdout is available for reading - childProcess.stdout = stdOutStream - - resolve(childProcess) - }) - }) - stdOutServer.listen(stdOutPipeName) - - let serverClosed = false - const closeServer = () => { - if (serverClosed) { - return - } - serverClosed = true - stdOutServer.close() - stdErrServer.close() - } - +): cp.ChildProcess { // Create the process - logger.info('Forking TSServer', `PATH: ${newEnv['PATH']} `) - - const bootstrapperPath = path.resolve(__dirname, '../bin/tsserverForkStart') - childProcess = cp.fork(bootstrapperPath, [modulePath].concat(args), { + logger.info('Forking TSServer', `PATH: ${modulePath} `) + let childProcess = cp.fork(modulePath, args, { silent: true, cwd: undefined, - env: newEnv, + env: generatePatchedEnv(process.env, modulePath), execArgv: options.execArgv }) - - childProcess.once('error', (err: Error) => { - closeServer() - reject(err) - }) - - childProcess.once('exit', (err: Error) => { - closeServer() - reject(err) - }) + return childProcess }