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({
id: 'tsserver.restart',
execute: (): void => {
// tslint:disable-next-line:no-floating-promises
service.stop().then(() => {
setTimeout(() => {
service.restart()
}, 100)
})
}
})

View file

@ -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,

View file

@ -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()
}
})
})
}
@ -78,10 +74,10 @@ export default class TsserverService implements IServiceProvider {
disposeAll(this.disposables)
}
public async restart(): Promise<void> {
public restart(): void {
if (!this.clientHost) return
let client = this.clientHost.serviceClient
await client.restartTsServer()
client.restartTsServer()
}
public async stop(): Promise<void> {
@ -89,6 +85,5 @@ export default class TsserverService implements IServiceProvider {
this.clientHost.reset()
let client = this.clientHost.serviceClient
await client.stop()
return
}
}

View file

@ -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
client.onReady(() => {
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.
* 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<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()
}
}
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<ForkedTsServerProcess> | null
private lastError: Error | null
private lastStart: number
private numberRestarts: number
private cancellationPipeName: string | null = null
private _callbacks = new CallbackMap<Proto.Response>()
private _requestQueue = new RequestQueue()
private _pendingResponses = new Set<number>()
private _onReady?: { promise: Promise<void>; resolve: () => void; reject: () => void }
private versionStatus: VersionStatus
private readonly _onTsServerStarted = new Emitter<API>()
@ -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<void>((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 dispose(): void {
if (this.servicePromise) {
this.servicePromise
.then(childProcess => {
childProcess.kill()
})
.then(undefined, () => void 0)
public onReady(f: () => void): Promise<void> {
return this._onReady!.promise.then(f)
}
public dispose(): void {
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<any> {
const start = () => {
this.servicePromise = this.startService(true)
return this.servicePromise
}
if (this.servicePromise) {
return Promise.resolve(this.servicePromise.then(childProcess => {
public restartTsServer(): void {
if (this.tsServerProcess) {
this.state = ServiceStat.Stopping
this.info('Killing TS Server')
this.isRestarting = true
childProcess.kill()
this.servicePromise = null
}).then(start))
} else {
return Promise.resolve(start())
this.tsServerProcess.kill()
}
this.startService(true)
}
public stop(): Promise<void> {
if (!this.servicePromise) return
return new Promise((resolve, reject) => {
this.servicePromise.then(childProcess => {
if (this.state == ServiceStat.Running) {
return new Promise(resolve => {
let { tsServerProcess } = this
if (tsServerProcess && this.state == ServiceStat.Running) {
this.info('Killing TS Server')
childProcess.onExit(() => {
tsServerProcess.onExit(() => {
resolve()
})
childProcess.kill()
this.servicePromise = null
tsServerProcess.kill()
} else {
resolve()
}
}, reject)
})
}
@ -259,52 +206,34 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
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 {
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<ForkedTsServerProcess> {
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<ForkedTsServerProcess> {
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
}
let childProcess = fork(currentVersion.tsServerPath, args, options, this.logger)
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)
this.tsServerProcess = handle
this.lastStart = Date.now()
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 log file: ${this.tsServerLogFile || ''}`)
window.showMessage(`TSServer errored with error. ${err.message}`, 'error')
this.serviceExited(false)
})
handle.onExit((code: any) => {
handle.onExit((code: any, signal: string) => {
handle.dispose()
if (this.token != myToken) return
if (code == null) {
this.info('TSServer normal exit')
this.info(`TSServer exited. Signal: ${signal}`)
} 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.serviceExited(!this.isRestarting)
this.isRestarting = false
handle.dispose()
})
handle.onData(msg => {
this.dispatchMessage(msg)
})
resolve(handle)
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
}
)
} catch (e) {
reject(e)
}
})
}
public async openTsServerLogFile(): Promise<boolean> {
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<Proto.Response>()
@ -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<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 {
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 => {
if (!this.tsServerProcess) return
try {
childProcess.write(serverRequest)
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<string[]> {
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
if (!this.tsServerProcess) return
this.executeWithoutWaitingForResponse('configurePlugin', { pluginName, configuration })
})
}
}

View file

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

View file

@ -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
}