refactor(server): avoid tsserverForkStart

This commit is contained in:
Qiming Zhao 2021-12-22 16:53:53 +08:00
parent 65e1f75be5
commit 181a337c4a
9 changed files with 199 additions and 504 deletions

View file

@ -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');
});
})();

View file

@ -29,12 +29,7 @@ export async function activate(context: ExtensionContext): Promise<API> {
registCommand({ registCommand({
id: 'tsserver.restart', id: 'tsserver.restart',
execute: (): void => { execute: (): void => {
// tslint:disable-next-line:no-floating-promises
service.stop().then(() => {
setTimeout(() => {
service.restart() service.restart()
}, 100)
})
} }
}) })

View file

@ -345,9 +345,7 @@ export default class BufferSyncSupport {
} }
public listen(): void { public listen(): void {
if (this.listening) { if (this.listening) return
return
}
this.listening = true this.listening = true
workspace.onDidOpenTextDocument( workspace.onDidOpenTextDocument(
this.openTextDocument, this.openTextDocument,

View file

@ -58,18 +58,14 @@ export default class TsserverService implements IServiceProvider {
this.disposables.push(this.clientHost) this.disposables.push(this.clientHost)
let client = this.clientHost.serviceClient let client = this.clientHost.serviceClient
return new Promise(resolve => { return new Promise(resolve => {
let started = false client.onReady(() => {
client.onTsServerStarted(() => {
Object.defineProperty(this, 'state', { Object.defineProperty(this, 'state', {
get: () => { get: () => {
return this.clientHost.serviceClient.state return this.clientHost.serviceClient.state
} }
}) })
this._onDidServiceReady.fire(void 0) this._onDidServiceReady.fire(void 0)
if (!started) {
started = true
resolve() resolve()
}
}) })
}) })
} }
@ -78,10 +74,10 @@ export default class TsserverService implements IServiceProvider {
disposeAll(this.disposables) disposeAll(this.disposables)
} }
public async restart(): Promise<void> { public restart(): void {
if (!this.clientHost) return if (!this.clientHost) return
let client = this.clientHost.serviceClient let client = this.clientHost.serviceClient
await client.restartTsServer() client.restartTsServer()
} }
public async stop(): Promise<void> { public async stop(): Promise<void> {
@ -89,6 +85,5 @@ export default class TsserverService implements IServiceProvider {
this.clientHost.reset() this.clientHost.reset()
let client = this.clientHost.serviceClient let client = this.clientHost.serviceClient
await client.stop() await client.stop()
return
} }
} }

View file

@ -49,13 +49,8 @@ export default class LanguageProvider {
) { ) {
workspace.onDidChangeConfiguration(this.configurationChanged, this, this.disposables) workspace.onDidChangeConfiguration(this.configurationChanged, this, this.disposables)
this.configurationChanged() this.configurationChanged()
client.onReady(() => {
let initialized = false
client.onTsServerStarted(async () => { // tslint:disable-line
if (!initialized) {
initialized = true
this.registerProviders(client, typingsStatus) this.registerProviders(client, typingsStatus)
}
}) })
} }

View file

@ -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<Proto.Response>
constructor(private childProcess: cp.ChildProcess) {
this._reader = new Reader<Proto.Response>(this.childProcess.stdout)
}
public readonly toCancelOnResourceChange = new Set<ToCancelOnResourceChanged>()
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()
}
}

View file

@ -2,7 +2,6 @@
* Copyright (c) Microsoft Corporation. All rights reserved. * Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information. * 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 { Document, ServiceStat, Uri, window, workspace } from 'coc.nvim'
import fs from 'fs' import fs from 'fs'
import os from 'os' import os from 'os'
@ -25,51 +24,7 @@ import Tracer from './utils/tracer'
import { inferredProjectConfig } from './utils/tsconfig' import { inferredProjectConfig } from './utils/tsconfig'
import { TypeScriptVersion, TypeScriptVersionProvider } from './utils/versionProvider' import { TypeScriptVersion, TypeScriptVersionProvider } from './utils/versionProvider'
import VersionStatus from './utils/versionStatus' import VersionStatus from './utils/versionStatus'
import { Reader } from './utils/wireProtocol' import ForkedTsServerProcess, { ToCancelOnResourceChanged } from './tsServerProcess'
interface ToCancelOnResourceChanged {
readonly resource: string
cancel(): void
}
class ForkedTsServerProcess implements Disposable {
private readonly _reader: Reader<Proto.Response>
constructor(private childProcess: cp.ChildProcess) {
this._reader = new Reader<Proto.Response>(this.childProcess.stdout)
}
public readonly toCancelOnResourceChange = new Set<ToCancelOnResourceChanged>()
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()
}
}
export interface TsDiagnostics { export interface TsDiagnostics {
readonly kind: DiagnosticKind readonly kind: DiagnosticKind
@ -78,6 +33,7 @@ export interface TsDiagnostics {
} }
export default class TypeScriptServiceClient implements ITypeScriptServiceClient { export default class TypeScriptServiceClient implements ITypeScriptServiceClient {
private token: number = 0
public state = ServiceStat.Initial public state = ServiceStat.Initial
public readonly logger: Logger = new Logger() public readonly logger: Logger = new Logger()
public readonly bufferSyncSupport: BufferSyncSupport public readonly bufferSyncSupport: BufferSyncSupport
@ -90,14 +46,13 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
private versionProvider: TypeScriptVersionProvider private versionProvider: TypeScriptVersionProvider
private tsServerLogFile: string | null = null private tsServerLogFile: string | null = null
private tsServerProcess: ForkedTsServerProcess | undefined private tsServerProcess: ForkedTsServerProcess | undefined
private servicePromise: Thenable<ForkedTsServerProcess> | null
private lastError: Error | null
private lastStart: number private lastStart: number
private numberRestarts: number private numberRestarts: number
private cancellationPipeName: string | null = null private cancellationPipeName: string | null = null
private _callbacks = new CallbackMap<Proto.Response>() private _callbacks = new CallbackMap<Proto.Response>()
private _requestQueue = new RequestQueue() private _requestQueue = new RequestQueue()
private _pendingResponses = new Set<number>() private _pendingResponses = new Set<number>()
private _onReady?: { promise: Promise<void>; resolve: () => void; reject: () => void }
private versionStatus: VersionStatus private versionStatus: VersionStatus
private readonly _onTsServerStarted = new Emitter<API>() private readonly _onTsServerStarted = new Emitter<API>()
@ -118,9 +73,15 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
) { ) {
this.pathSeparator = path.sep this.pathSeparator = path.sep
this.lastStart = Date.now() this.lastStart = Date.now()
this.servicePromise = null
this.lastError = null
this.numberRestarts = 0 this.numberRestarts = 0
let resolve: () => void
let reject: () => void
const p = new Promise<void>((res, rej) => {
resolve = res
reject = rej
})
this._onReady = { promise: p, resolve: resolve!, reject: reject! }
this.fileConfigurationManager = new FileConfigurationManager(this) this.fileConfigurationManager = new FileConfigurationManager(this)
this._configuration = TypeScriptServiceConfiguration.loadFromWorkspace() this._configuration = TypeScriptServiceConfiguration.loadFromWorkspace()
this.versionProvider = new TypeScriptVersionProvider(this._configuration) this.versionProvider = new TypeScriptVersionProvider(this._configuration)
@ -136,7 +97,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
}, null, this.disposables) }, null, this.disposables)
this.bufferSyncSupport = new BufferSyncSupport(this, modeIds) this.bufferSyncSupport = new BufferSyncSupport(this, modeIds)
this.onTsServerStarted(() => { this.onReady(() => {
this.bufferSyncSupport.listen() this.bufferSyncSupport.listen()
}) })
@ -169,14 +130,12 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
return this._configuration return this._configuration
} }
public dispose(): void { public onReady(f: () => void): Promise<void> {
if (this.servicePromise) { return this._onReady!.promise.then(f)
this.servicePromise
.then(childProcess => {
childProcess.kill()
})
.then(undefined, () => void 0)
} }
public dispose(): void {
this.tsServerProcess.kill()
this.bufferSyncSupport.dispose() this.bufferSyncSupport.dispose()
this.logger.dispose() this.logger.dispose()
this._onTsServerStarted.dispose() this._onTsServerStarted.dispose()
@ -192,40 +151,28 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
this.logger.error(message, data) this.logger.error(message, data)
} }
public restartTsServer(): Promise<any> { public restartTsServer(): void {
const start = () => { if (this.tsServerProcess) {
this.servicePromise = this.startService(true)
return this.servicePromise
}
if (this.servicePromise) {
return Promise.resolve(this.servicePromise.then(childProcess => {
this.state = ServiceStat.Stopping this.state = ServiceStat.Stopping
this.info('Killing TS Server') this.info('Killing TS Server')
this.isRestarting = true this.isRestarting = true
childProcess.kill() this.tsServerProcess.kill()
this.servicePromise = null
}).then(start))
} else {
return Promise.resolve(start())
} }
this.startService(true)
} }
public stop(): Promise<void> { public stop(): Promise<void> {
if (!this.servicePromise) return return new Promise(resolve => {
return new Promise((resolve, reject) => { let { tsServerProcess } = this
this.servicePromise.then(childProcess => { if (tsServerProcess && this.state == ServiceStat.Running) {
if (this.state == ServiceStat.Running) {
this.info('Killing TS Server') this.info('Killing TS Server')
childProcess.onExit(() => { tsServerProcess.onExit(() => {
resolve() resolve()
}) })
childProcess.kill() tsServerProcess.kill()
this.servicePromise = null
} else { } else {
resolve() resolve()
} }
}, reject)
}) })
} }
@ -259,52 +206,34 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
return this._tscPath return this._tscPath
} }
private service(): Thenable<ForkedTsServerProcess> {
if (this.servicePromise) {
return this.servicePromise
}
if (this.lastError) {
return Promise.reject<ForkedTsServerProcess>(this.lastError)
}
return this.startService().then(() => {
if (this.servicePromise) {
return this.servicePromise
}
})
}
public ensureServiceStarted(): void { public ensureServiceStarted(): void {
if (!this.servicePromise) { if (!this.tsServerProcess) {
this.startService().catch(err => { this.startService()
window.showMessage(`TSServer start failed: ${err.message}`, 'error')
this.error(`Service start failed: ${err.stack}`)
})
} }
} }
private async startService(resendModels = false): Promise<ForkedTsServerProcess> { private startService(resendModels = false): ForkedTsServerProcess | undefined {
const { ignoreLocalTsserver } = this.configuration const { ignoreLocalTsserver } = this.configuration
let currentVersion: TypeScriptVersion let currentVersion: TypeScriptVersion
if (!ignoreLocalTsserver) currentVersion = this.versionProvider.getLocalVersion() if (!ignoreLocalTsserver) currentVersion = this.versionProvider.getLocalVersion()
if (!currentVersion || !fs.existsSync(currentVersion.tsServerPath)) { if (!currentVersion || !fs.existsSync(currentVersion.tsServerPath)) {
this.info('Local tsserver not found, using bundled tsserver with coc-tsserver.')
currentVersion = this.versionProvider.getDefaultVersion() currentVersion = this.versionProvider.getDefaultVersion()
} }
if (!currentVersion || !currentVersion.isValid) { if (!currentVersion || !currentVersion.isValid) {
if (this.configuration.globalTsdk) { 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 { } 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 return
} }
this._apiVersion = currentVersion.version this._apiVersion = currentVersion.version
this._tscPath = currentVersion.tscPath this._tscPath = currentVersion.tscPath
this.versionStatus.onDidChangeTypeScriptVersion(currentVersion) this.versionStatus.onDidChangeTypeScriptVersion(currentVersion)
this.lastError = null const tsServerForkArgs = this.getTsServerArgs(currentVersion)
const tsServerForkArgs = await this.getTsServerArgs(currentVersion)
const options = { execArgv: this.getExecArgv() } const options = { execArgv: this.getExecArgv() }
this.servicePromise = this.startProcess(currentVersion, tsServerForkArgs, options, resendModels) return this.startProcess(currentVersion, tsServerForkArgs, options, resendModels)
return this.servicePromise
} }
private getExecArgv(): string[] { private getExecArgv(): string[] {
@ -322,64 +251,54 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
return args return args
} }
private startProcess(currentVersion: TypeScriptVersion, args: string[], options: IForkOptions, resendModels: boolean): Promise<ForkedTsServerProcess> { private startProcess(currentVersion: TypeScriptVersion, args: string[], options: IForkOptions, resendModels: boolean): ForkedTsServerProcess {
const myToken = ++this.token
this.state = ServiceStat.Starting this.state = ServiceStat.Starting
return new Promise((resolve, reject) => {
try { try {
fork( let childProcess = fork(currentVersion.tsServerPath, args, options, this.logger)
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.state = ServiceStat.Running
this.info('Started TSServer', JSON.stringify(currentVersion, null, 2)) this.info('Starting TSServer', JSON.stringify(currentVersion, null, 2))
const handle = new ForkedTsServerProcess(childProcess) const handle = new ForkedTsServerProcess(childProcess)
this.tsServerProcess = handle this.tsServerProcess = handle
this.lastStart = Date.now() this.lastStart = Date.now()
handle.onError((err: Error) => { handle.onError((err: Error) => {
this.lastError = err 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 errored with error.', err)
this.error(`TSServer log file: ${this.tsServerLogFile || ''}`) this.error(`TSServer log file: ${this.tsServerLogFile || ''}`)
window.showMessage(`TSServer errored with error. ${err.message}`, 'error') window.showMessage(`TSServer errored with error. ${err.message}`, 'error')
this.serviceExited(false) this.serviceExited(false)
}) })
handle.onExit((code: any) => { handle.onExit((code: any, signal: string) => {
handle.dispose()
if (this.token != myToken) return
if (code == null) { if (code == null) {
this.info('TSServer normal exit') this.info(`TSServer exited. Signal: ${signal}`)
} else { } else {
this.error(`TSServer exited with code: ${code}`) this.error(`TSServer exited with code: ${code}. Signal: ${signal}`)
} }
this.info(`TSServer log file: ${this.tsServerLogFile || ''}`) this.info(`TSServer log file: ${this.tsServerLogFile || ''}`)
this.serviceExited(!this.isRestarting) this.serviceExited(!this.isRestarting)
this.isRestarting = false this.isRestarting = false
handle.dispose()
}) })
handle.onData(msg => { handle.onData(msg => {
this.dispatchMessage(msg) this.dispatchMessage(msg)
}) })
resolve(handle)
this.serviceStarted(resendModels) this.serviceStarted(resendModels)
this._onReady!.resolve()
this._onTsServerStarted.fire(currentVersion.version) this._onTsServerStarted.fire(currentVersion.version)
return handle
} catch (err) {
this.state = ServiceStat.StartFailed
this.error('Starting TSServer failed with error.', err.stack)
return undefined
} }
)
} catch (e) {
reject(e)
}
})
} }
public async openTsServerLogFile(): Promise<boolean> { public async openTsServerLogFile(): Promise<boolean> {
const isRoot = process.getuid && process.getuid() == 0 const isRoot = process.getuid && process.getuid() == 0
let echoErr = (msg: string) => { let echoErr = (msg: string) => {
window.showMessage(msg, 'error') window.showErrorMessage(msg)
} }
if (isRoot) { if (isRoot) {
echoErr('Log disabled for root user.') echoErr('Log disabled for root user.')
@ -457,7 +376,6 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
private serviceExited(restart: boolean): void { private serviceExited(restart: boolean): void {
this.state = ServiceStat.Stopped this.state = ServiceStat.Stopped
this.servicePromise = null
this.tsServerLogFile = null this.tsServerLogFile = null
this._callbacks.destroy('Service died.') this._callbacks.destroy('Service died.')
this._callbacks = new CallbackMap<Proto.Response>() this._callbacks = new CallbackMap<Proto.Response>()
@ -479,7 +397,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
} }
} }
if (startService) { 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 { public toOpenedFilePath(uri: string, options: { suppressAlertOnFailure?: boolean } = {}): string | undefined {
if (!this.bufferSyncSupport.ensureHasBuffer(uri)) { if (!this.bufferSyncSupport.ensureHasBuffer(uri)) {
if (!options.suppressAlertOnFailure) { if (!options.suppressAlertOnFailure) {
console.error(`Unexpected resource ${uri}`) this.error(`Unexpected resource ${uri}`)
} }
return undefined return undefined
} }
@ -608,13 +526,14 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
} }
private fatalError(command: string, error: any): void { 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) { if (this.state === ServiceStat.Running) {
this.info('Killing TS Server by fatal error:', error) this.info('Killing TS Server by fatal error:', error)
this.service().then(service => { let { tsServerProcess } = this
service.kill() 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: false, lowPriority?: boolean }): undefined
private executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>> private executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>>
private executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>> | undefined { private executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>> | undefined {
if (this.servicePromise == null) { if (!this.tsServerProcess) {
return Promise.resolve(undefined) return Promise.resolve(undefined)
} }
this.bufferSyncSupport.beforeCommand(command) this.bufferSyncSupport.beforeCommand(command)
@ -687,16 +606,15 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
if (requestItem.expectsResponse && !requestItem.isAsync) { if (requestItem.expectsResponse && !requestItem.isAsync) {
this._pendingResponses.add(requestItem.request.seq) this._pendingResponses.add(requestItem.request.seq)
} }
this.service().then(childProcess => { if (!this.tsServerProcess) return
try { try {
childProcess.write(serverRequest) this.tsServerProcess.write(serverRequest)
} catch (err) { } catch (err) {
const callback = this.fetchCallback(serverRequest.seq) const callback = this.fetchCallback(serverRequest.seq)
if (callback) { if (callback) {
callback.onError(err) callback.onError(err)
} }
} }
})
} }
private tryCancelRequest(seq: number, command: string): boolean { private tryCancelRequest(seq: number, command: string): boolean {
@ -731,7 +649,6 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
if (!callback) { if (!callback) {
return undefined return undefined
} }
this._pendingResponses.delete(seq) this._pendingResponses.delete(seq)
return callback return callback
} }
@ -849,7 +766,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
} }
} }
private async getTsServerArgs(currentVersion: TypeScriptVersion): Promise<string[]> { private getTsServerArgs(currentVersion: TypeScriptVersion): string[] {
const args: string[] = [] const args: string[] = []
args.push('--allowLocalPluginLoads') args.push('--allowLocalPluginLoads')
@ -974,11 +891,8 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
public configurePlugin(pluginName: string, configuration: {}): any { public configurePlugin(pluginName: string, configuration: {}): any {
if (this.apiVersion.gte(API.v314)) { if (this.apiVersion.gte(API.v314)) {
if (!this.servicePromise) return if (!this.tsServerProcess) return
this.servicePromise.then(() => {
// tslint:disable-next-line: no-floating-promises
this.executeWithoutWaitingForResponse('configurePlugin', { pluginName, configuration }) this.executeWithoutWaitingForResponse('configurePlugin', { pluginName, configuration })
})
} }
} }

View file

@ -102,6 +102,8 @@ export default class TypeScriptServiceClientHost implements Disposable {
) )
this.languagePerId.set(description.id, manager) this.languagePerId.set(description.id, manager)
} }
this.client.ensureServiceStarted()
this.client.onReady(() => {
const languageIds = new Set<string>() const languageIds = new Set<string>()
for (const plugin of pluginManager.plugins) { for (const plugin of pluginManager.plugins) {
if (plugin.configNamespace && plugin.languages.length) { if (plugin.configNamespace && plugin.languages.length) {
@ -130,11 +132,11 @@ export default class TypeScriptServiceClientHost implements Disposable {
isExternal: true isExternal: true
}) })
} }
})
this.client.ensureServiceStarted()
this.client.onTsServerStarted(() => { this.client.onTsServerStarted(() => {
this.triggerAllDiagnostics() this.triggerAllDiagnostics()
}) })
workspace.onDidChangeConfiguration(this.configurationChanged, this, this.disposables) workspace.onDidChangeConfiguration(this.configurationChanged, this, this.disposables)
this.configurationChanged() this.configurationChanged()
} }

View file

@ -3,7 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information. * Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import cp from 'child_process' import cp from 'child_process'
import net from 'net'
import os from 'os' import os from 'os'
import path from 'path' import path from 'path'
import fs from 'fs' import fs from 'fs'
@ -36,21 +35,6 @@ export function getTempDirectory(): string | undefined {
return dir 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 { export function getTempFile(name: string): string | undefined {
const fullName = 'coc-nvim-' + name const fullName = 'coc-nvim-' + name
let dir = getTempDirectory() let dir = getTempDirectory()
@ -70,20 +54,9 @@ export function createTempDirectory(name: string) {
return res return res
} }
function generatePatchedEnv( function generatePatchedEnv(env: any, modulePath: string): any {
env: any,
stdInPipeName: string,
stdOutPipeName: string,
stdErrPipeName: string
): any {
const newEnv = Object.assign({}, env) const newEnv = Object.assign({}, env)
newEnv['NODE_PATH'] = path.join(modulePath, '..', '..', '..')
// 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
// Ensure we always have a PATH set // Ensure we always have a PATH set
newEnv['PATH'] = newEnv['PATH'] || process.env.PATH // tslint:disable-line newEnv['PATH'] = newEnv['PATH'] || process.env.PATH // tslint:disable-line
return newEnv return newEnv
@ -94,85 +67,14 @@ export function fork(
args: string[], args: string[],
options: IForkOptions, options: IForkOptions,
logger: Logger, logger: Logger,
callback: (error: any, cp: cp.ChildProcess | null) => void ): cp.ChildProcess {
): 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()
}
// Create the process // Create the process
logger.info('Forking TSServer', `PATH: ${newEnv['PATH']} `) logger.info('Forking TSServer', `PATH: ${modulePath} `)
let childProcess = cp.fork(modulePath, args, {
const bootstrapperPath = path.resolve(__dirname, '../bin/tsserverForkStart')
childProcess = cp.fork(bootstrapperPath, [modulePath].concat(args), {
silent: true, silent: true,
cwd: undefined, cwd: undefined,
env: newEnv, env: generatePatchedEnv(process.env, modulePath),
execArgv: options.execArgv execArgv: options.execArgv
}) })
return childProcess
childProcess.once('error', (err: Error) => {
closeServer()
reject(err)
})
childProcess.once('exit', (err: Error) => {
closeServer()
reject(err)
})
} }