[Code] refactor launcher code, add unit tests (#36863) (#37044)

This commit is contained in:
Yulong 2019-05-24 16:04:38 +08:00 committed by GitHub
parent 384d5d9a94
commit 5ca9caf486
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 598 additions and 179 deletions

View file

@ -0,0 +1,186 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// eslint-disable-next-line max-classes-per-file
import { fork, ChildProcess } from 'child_process';
import path from 'path';
import fs from 'fs';
import { ServerOptions } from '../server_options';
import { createTestServerOption } from '../test_utils';
import { AbstractLauncher } from './abstract_launcher';
import { RequestExpander } from './request_expander';
import { LanguageServerProxy } from './proxy';
import { ConsoleLoggerFactory } from '../utils/console_logger_factory';
import { Logger } from '../log';
jest.setTimeout(10000);
// @ts-ignore
const options: ServerOptions = createTestServerOption();
// a mock function being called when then forked sub process status changes
// @ts-ignore
const mockMonitor = jest.fn();
class MockLauncher extends AbstractLauncher {
public childProcess?: ChildProcess;
constructor(name: string, targetHost: string, opt: ServerOptions) {
super(name, targetHost, opt, new ConsoleLoggerFactory());
}
createExpander(
proxy: LanguageServerProxy,
builtinWorkspace: boolean,
maxWorkspace: number
): RequestExpander {
return new RequestExpander(proxy, builtinWorkspace, maxWorkspace, this.options);
}
async getPort() {
return 19999;
}
async spawnProcess(installationPath: string, port: number, log: Logger): Promise<ChildProcess> {
const childProcess = fork(path.join(__dirname, 'mock_lang_server.js'));
this.childProcess = childProcess;
childProcess.on('message', msg => {
// eslint-disable-next-line no-console
console.log(msg);
mockMonitor(msg);
});
childProcess.send(`port ${await this.getPort()}`);
childProcess.send(`host ${this.targetHost}`);
childProcess.send('listen');
return childProcess;
}
}
class PassiveMockLauncher extends MockLauncher {
constructor(
name: string,
targetHost: string,
opt: ServerOptions,
private dieFirstTime: boolean = false
) {
super(name, targetHost, opt);
}
startConnect(proxy: LanguageServerProxy) {
proxy.awaitServerConnection();
}
async getPort() {
return 19998;
}
async spawnProcess(installationPath: string, port: number, log: Logger): Promise<ChildProcess> {
this.childProcess = fork(path.join(__dirname, 'mock_lang_server.js'));
this.childProcess.on('message', msg => {
// eslint-disable-next-line no-console
console.log(msg);
mockMonitor(msg);
});
this.childProcess.send(`port ${await this.getPort()}`);
this.childProcess.send(`host ${this.targetHost}`);
if (this.dieFirstTime) {
this.childProcess!.send('quit');
this.dieFirstTime = false;
} else {
this.childProcess!.send('connect');
}
return this.childProcess!;
}
}
beforeAll(async () => {
if (!fs.existsSync(options.workspacePath)) {
fs.mkdirSync(options.workspacePath, { recursive: true });
fs.mkdirSync(options.jdtWorkspacePath, { recursive: true });
}
});
beforeEach(() => {
mockMonitor.mockClear();
});
function delay(millis: number) {
return new Promise(resolve => {
setTimeout(() => resolve(), millis);
});
}
test('launcher can start and end a process', async () => {
const launcher = new MockLauncher('mock', 'localhost', options);
const proxy = await launcher.launch(false, 1, '');
await delay(100);
expect(mockMonitor.mock.calls[0][0]).toBe('process started');
expect(mockMonitor.mock.calls[1][0]).toBe('start listening');
expect(mockMonitor.mock.calls[2][0]).toBe('socket connected');
await proxy.exit();
await delay(100);
expect(mockMonitor.mock.calls[3][0]).toMatchObject({ method: 'shutdown' });
expect(mockMonitor.mock.calls[4][0]).toMatchObject({ method: 'exit' });
expect(mockMonitor.mock.calls[5][0]).toBe('exit process with code 0');
});
test('launcher can force kill the process if langServer can not exit', async () => {
const launcher = new MockLauncher('mock', 'localhost', options);
const proxy = await launcher.launch(false, 1, '');
await delay(100);
// set mock lang server to noExist mode
launcher.childProcess!.send('noExit');
mockMonitor.mockClear();
await proxy.exit();
await delay(2000);
expect(mockMonitor.mock.calls[0][0]).toMatchObject({ method: 'shutdown' });
expect(mockMonitor.mock.calls[1][0]).toMatchObject({ method: 'exit' });
expect(mockMonitor.mock.calls[2][0]).toBe('noExit');
expect(launcher.childProcess!.killed).toBe(true);
});
test('launcher can reconnect if process died', async () => {
const launcher = new MockLauncher('mock', 'localhost', options);
const proxy = await launcher.launch(false, 1, '');
await delay(1000);
mockMonitor.mockClear();
// let the process quit
launcher.childProcess!.send('quit');
await delay(5000);
// launcher should respawn a new process and connect
expect(mockMonitor.mock.calls[0][0]).toBe('process started');
expect(mockMonitor.mock.calls[1][0]).toBe('start listening');
expect(mockMonitor.mock.calls[2][0]).toBe('socket connected');
await proxy.exit();
await delay(2000);
});
test('passive launcher can start and end a process', async () => {
const launcher = new PassiveMockLauncher('mock', 'localhost', options);
const proxy = await launcher.launch(false, 1, '');
await delay(100);
expect(mockMonitor.mock.calls[0][0]).toBe('process started');
expect(mockMonitor.mock.calls[1][0]).toBe('start connecting');
expect(mockMonitor.mock.calls[2][0]).toBe('socket connected');
await proxy.exit();
await delay(100);
expect(mockMonitor.mock.calls[3][0]).toMatchObject({ method: 'shutdown' });
expect(mockMonitor.mock.calls[4][0]).toMatchObject({ method: 'exit' });
expect(mockMonitor.mock.calls[5][0]).toBe('exit process with code 0');
});
test('passive launcher should restart a process if a process died before connected', async () => {
const launcher = new PassiveMockLauncher('mock', 'localhost', options, true);
const proxy = await launcher.launch(false, 1, '');
await delay(100);
expect(mockMonitor.mock.calls[0][0]).toBe('process started');
expect(mockMonitor.mock.calls[1][0]).toBe('process started');
expect(mockMonitor.mock.calls[2][0]).toBe('start connecting');
expect(mockMonitor.mock.calls[3][0]).toBe('socket connected');
await proxy.exit();
await delay(1000);
});

View file

@ -0,0 +1,190 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { ChildProcess } from 'child_process';
import { ILanguageServerLauncher } from './language_server_launcher';
import { ServerOptions } from '../server_options';
import { LoggerFactory } from '../utils/log_factory';
import { Logger } from '../log';
import { LanguageServerProxy } from './proxy';
import { RequestExpander } from './request_expander';
export abstract class AbstractLauncher implements ILanguageServerLauncher {
running: boolean = false;
private _currentPid: number = -1;
private child: ChildProcess | null = null;
private _startTime: number = -1;
private _proxyConnected: boolean = false;
protected constructor(
readonly name: string,
readonly targetHost: string,
readonly options: ServerOptions,
readonly loggerFactory: LoggerFactory
) {}
public async launch(builtinWorkspace: boolean, maxWorkspace: number, installationPath: string) {
const port = await this.getPort();
const log: Logger = this.loggerFactory.getLogger([
'code',
`${this.name}@${this.targetHost}:${port}`,
]);
let child: ChildProcess;
const proxy = new LanguageServerProxy(port, this.targetHost, log, this.options.lsp);
if (this.options.lsp.detach) {
log.debug('Detach mode, expected language server launch externally');
proxy.onConnected(() => {
this.running = true;
});
proxy.onDisconnected(() => {
this.running = false;
if (!proxy.isClosed) {
log.debug(`${this.name} language server disconnected, reconnecting`);
setTimeout(() => this.reconnect(proxy, installationPath, port, log), 1000);
}
});
} else {
child = await this.spawnProcess(installationPath, port, log);
this.child = child;
log.debug('spawned a child process ' + child.pid);
this._currentPid = child.pid;
this._startTime = Date.now();
this.running = true;
this.onProcessExit(child, () => this.reconnect(proxy, installationPath, port, log));
proxy.onDisconnected(async () => {
this._proxyConnected = true;
if (!proxy.isClosed) {
log.debug('proxy disconnected, reconnecting');
setTimeout(async () => {
await this.reconnect(proxy, installationPath, port, log, child);
}, 1000);
} else if (this.child) {
log.info('proxy closed, kill process');
await this.killProcess(this.child, log);
}
});
}
proxy.onExit(() => {
log.debug('proxy exited, is the process running? ' + this.running);
if (this.child && this.running) {
const p = this.child!;
setTimeout(async () => {
if (!p.killed) {
log.debug('killing the process after 1s');
await this.killProcess(p, log);
}
}, 1000);
}
});
proxy.listen();
this.startConnect(proxy);
await new Promise(resolve => {
proxy.onConnected(() => {
this._proxyConnected = true;
resolve();
});
});
return this.createExpander(proxy, builtinWorkspace, maxWorkspace);
}
private onProcessExit(child: ChildProcess, reconnectFn: () => void) {
const pid = child.pid;
child.on('exit', () => {
if (this._currentPid === pid) {
this.running = false;
// if the process exited before proxy connected, then we reconnect
if (!this._proxyConnected) {
reconnectFn();
}
}
});
}
/**
* proxy should be connected within this timeout, otherwise we reconnect.
*/
protected startupTimeout = 3000;
/**
* try reconnect the proxy when disconnected
*/
public async reconnect(
proxy: LanguageServerProxy,
installationPath: string,
port: number,
log: Logger,
child?: ChildProcess
) {
log.debug('reconnecting');
if (this.options.lsp.detach) {
this.startConnect(proxy);
} else {
const processExpired = () => Date.now() - this._startTime > this.startupTimeout;
if (child && !child.killed && !processExpired()) {
this.startConnect(proxy);
} else {
if (child && this.running) {
log.debug('killing the old process.');
await this.killProcess(child, log);
}
this.child = await this.spawnProcess(installationPath, port, log);
log.debug('spawned a child process ' + this.child.pid);
this._currentPid = this.child.pid;
this._startTime = Date.now();
this.running = true;
this.onProcessExit(this.child, () =>
this.reconnect(proxy, installationPath, port, log, child)
);
this.startConnect(proxy);
}
}
}
abstract async getPort(): Promise<number>;
startConnect(proxy: LanguageServerProxy) {
proxy.connect();
}
/**
* await for proxy connected, create a request expander
* @param proxy
*/
abstract createExpander(
proxy: LanguageServerProxy,
builtinWorkspace: boolean,
maxWorkspace: number
): RequestExpander;
abstract async spawnProcess(
installationPath: string,
port: number,
log: Logger
): Promise<ChildProcess>;
private killProcess(child: ChildProcess, log: Logger) {
if (!child.killed) {
return new Promise<boolean>((resolve, reject) => {
// if not killed within 1s
const t = setTimeout(reject, 1000);
child.on('exit', () => {
clearTimeout(t);
resolve(true);
});
child.kill();
log.info('killed process ' + child.pid);
})
.catch(() => {
// force kill
child.kill('SIGKILL');
log.info('force killed process ' + child.pid);
return child.killed;
})
.finally(() => {
if (this._currentPid === child.pid) this.running = false;
});
}
}
}

View file

@ -13,76 +13,43 @@ import path from 'path';
import { Logger } from '../log';
import { ServerOptions } from '../server_options';
import { LoggerFactory } from '../utils/log_factory';
import { ILanguageServerLauncher } from './language_server_launcher';
import { LanguageServerProxy } from './proxy';
import { RequestExpander } from './request_expander';
import { AbstractLauncher } from './abstract_launcher';
export class JavaLauncher implements ILanguageServerLauncher {
private isRunning: boolean = false;
const JAVA_LANG_DETACH_PORT = 2090;
export class JavaLauncher extends AbstractLauncher {
private needModuleArguments: boolean = true;
constructor(
public constructor(
readonly targetHost: string,
readonly options: ServerOptions,
readonly loggerFactory: LoggerFactory
) {}
public get running(): boolean {
return this.isRunning;
) {
super('java', targetHost, options, loggerFactory);
}
public async launch(builtinWorkspace: boolean, maxWorkspace: number, installationPath: string) {
let port = 2090;
if (!this.options.lsp.detach) {
port = await getPort();
}
const log = this.loggerFactory.getLogger(['code', `java@${this.targetHost}:${port}`]);
const proxy = new LanguageServerProxy(port, this.targetHost, log, this.options.lsp);
proxy.awaitServerConnection();
if (this.options.lsp.detach) {
// detach mode
proxy.onConnected(() => {
this.isRunning = true;
});
proxy.onDisconnected(() => {
this.isRunning = false;
if (!proxy.isClosed) {
proxy.awaitServerConnection();
}
});
} else {
let child = await this.spawnJava(installationPath, port, log);
proxy.onDisconnected(async () => {
if (!proxy.isClosed) {
child.kill();
proxy.awaitServerConnection();
log.warn('language server disconnected, restarting it');
child = await this.spawnJava(installationPath, port, log);
} else {
child.kill();
}
});
proxy.onExit(() => {
if (child) {
child.kill();
}
});
}
proxy.listen();
return new Promise<RequestExpander>(resolve => {
proxy.onConnected(() => {
resolve(
new RequestExpander(proxy, builtinWorkspace, maxWorkspace, this.options, {
settings: {
'java.import.gradle.enabled': this.options.security.enableGradleImport,
'java.import.maven.enabled': this.options.security.enableMavenImport,
'java.autobuild.enabled': false,
},
})
);
});
createExpander(proxy: LanguageServerProxy, builtinWorkspace: boolean, maxWorkspace: number) {
return new RequestExpander(proxy, builtinWorkspace, maxWorkspace, this.options, {
settings: {
'java.import.gradle.enabled': this.options.security.enableGradleImport,
'java.import.maven.enabled': this.options.security.enableMavenImport,
'java.autobuild.enabled': false,
},
});
}
startConnect(proxy: LanguageServerProxy) {
proxy.awaitServerConnection();
}
async getPort(): Promise<number> {
if (!this.options.lsp.detach) {
return await getPort();
}
return JAVA_LANG_DETACH_PORT;
}
private async getJavaHome(installationPath: string, log: Logger) {
function findJDK(platform: string) {
const JDKFound = glob.sync(`**/jdks/*${platform}/jdk-*`, {
@ -132,7 +99,7 @@ export class JavaLauncher implements ILanguageServerLauncher {
return bundledJavaHome;
}
private async spawnJava(installationPath: string, port: number, log: Logger) {
async spawnProcess(installationPath: string, port: number, log: Logger) {
const launchersFound = glob.sync('**/plugins/org.eclipse.equinox.launcher_*.jar', {
cwd: installationPath,
});
@ -192,8 +159,6 @@ export class JavaLauncher implements ILanguageServerLauncher {
p.stderr.on('data', data => {
log.stderr(data.toString());
});
this.isRunning = true;
p.on('exit', () => (this.isRunning = false));
log.info(
`Launch Java Language Server at port ${port.toString()}, pid:${
p.pid

View file

@ -0,0 +1,126 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable */
// This file is used in the test, using subprocess.fork to load a module directly, so this module can not be typescript
const net = require('net');
const jsonrpc = require('vscode-jsonrpc');
const createMessageConnection = jsonrpc.createMessageConnection;
const SocketMessageReader = jsonrpc.SocketMessageReader;
const SocketMessageWriter = jsonrpc.SocketMessageWriter;
function log(msg) {
if (process.send) {
process.send(msg);
}
else {
// eslint-disable-next-line no-console
console.log(msg);
}
}
class MockLangServer {
constructor(host, port) {
this.host = host;
this.port = port;
this.socket = null;
this.connection = null;
this.shutdown = false;
}
/**
* connect remote server as a client
*/
connect() {
this.socket = new net.Socket();
this.socket.on('connect', () => {
const reader = new SocketMessageReader(this.socket);
const writer = new SocketMessageWriter(this.socket);
this.connection = createMessageConnection(reader, writer);
this.connection.listen();
this.connection.onNotification(this.onNotification.bind(this));
this.connection.onRequest(this.onRequest.bind(this));
log('socket connected');
});
this.socket.on('close', () => this.onSocketClosed());
log('start connecting');
this.socket.connect(this.port, this.host);
}
listen() {
const server = net.createServer(socket => {
server.close();
socket.on('close', () => this.onSocketClosed());
const reader = new SocketMessageReader(socket);
const writer = new SocketMessageWriter(socket);
this.connection = createMessageConnection(reader, writer);
this.connection.onNotification(this.onNotification.bind(this));
this.connection.onRequest(this.onRequest.bind(this));
this.connection.listen();
log('socket connected');
});
server.on('error', err => {
log(err);
});
log('start listening');
server.listen(this.port);
}
onNotification(method, ...params) {
log({ method, params });
// notify parent process what happened
if (method === 'exit') {
// https://microsoft.github.io/language-server-protocol/specification#exit
if (options.noExit) {
log('noExit');
}
else {
const code = this.shutdown ? 0 : 1;
log(`exit process with code ${code}`);
process.exit(code);
}
}
}
onRequest(method, ...params) {
// notify parent process what requested
log({ method, params });
if (method === 'shutdown') {
this.shutdown = true;
}
return { result: 'ok' };
}
onSocketClosed() {
// notify parent process that socket closed
log('socket closed');
}
}
log('process started');
let port = 9999;
let host = ' localhost';
const options = { noExit: false };
let langServer;
process.on('message', (msg) => {
const [cmd, value] = msg.split(' ');
switch (cmd) {
case 'port':
port = parseInt(value, 10);
break;
case 'host':
host = value;
break;
case 'noExit':
options.noExit = true;
break;
case 'listen':
langServer = new MockLangServer(host, port);
langServer.listen();
break;
case 'connect':
langServer = new MockLangServer(host, port);
langServer.connect();
break;
case 'quit':
process.exit(0);
break;
default:
// nothing to do
}
});

View file

@ -58,8 +58,9 @@ export class LanguageServerProxy implements ILanguageServerHandler {
private readonly logger: Logger;
private readonly lspOptions: LspOptions;
private eventEmitter = new EventEmitter();
private passiveConnection: boolean = false;
private connectingPromise?: Promise<MessageConnection>;
private connectingPromise: Promise<MessageConnection> | null = null;
constructor(targetPort: number, targetHost: string, logger: Logger, lspOptions: LspOptions) {
this.targetHost = targetHost;
@ -103,7 +104,7 @@ export class LanguageServerProxy implements ILanguageServerHandler {
workspaceFolders: [WorkspaceFolder],
initOptions?: object
): Promise<InitializeResult> {
const clientConn = await this.connect();
const clientConn = await this.tryConnect();
const rootUri = workspaceFolders[0].uri;
const params = {
processId: null,
@ -135,7 +136,7 @@ export class LanguageServerProxy implements ILanguageServerHandler {
this.logger.debug('received request method: ' + method);
}
return this.connect().then(clientConn => {
return this.tryConnect().then(clientConn => {
if (this.lspOptions.verbose) {
this.logger.info(`proxy method:${method} to Language Server `);
} else {
@ -149,7 +150,7 @@ export class LanguageServerProxy implements ILanguageServerHandler {
}
public async shutdown() {
const clientConn = await this.connect();
const clientConn = await this.tryConnect();
this.logger.info(`sending shutdown request`);
return await clientConn.sendRequest('shutdown');
}
@ -175,28 +176,33 @@ export class LanguageServerProxy implements ILanguageServerHandler {
}
public awaitServerConnection() {
return new Promise((res, rej) => {
const server = net.createServer(socket => {
this.initialized = false;
server.close();
this.eventEmitter.emit('connect');
socket.on('close', () => this.onSocketClosed());
// prevent calling this method multiple times which may cause 'port already in use' error
if (!this.connectingPromise) {
this.passiveConnection = true;
this.connectingPromise = new Promise((res, rej) => {
const server = net.createServer(socket => {
this.initialized = false;
server.close();
this.eventEmitter.emit('connect');
socket.on('close', () => this.onSocketClosed());
this.logger.info('Java langserver connection established on port ' + this.targetPort);
this.logger.info('langserver connection established on port ' + this.targetPort);
const reader = new SocketMessageReader(socket);
const writer = new SocketMessageWriter(socket);
this.clientConnection = createMessageConnection(reader, writer, this.logger);
this.registerOnNotificationHandler(this.clientConnection);
this.clientConnection.listen();
res(this.clientConnection);
const reader = new SocketMessageReader(socket);
const writer = new SocketMessageWriter(socket);
this.clientConnection = createMessageConnection(reader, writer, this.logger);
this.registerOnNotificationHandler(this.clientConnection);
this.clientConnection.listen();
res(this.clientConnection);
});
server.on('error', rej);
server.listen(this.targetPort, () => {
server.removeListener('error', rej);
this.logger.info('Wait langserver connection on port ' + this.targetPort);
});
});
server.on('error', rej);
server.listen(this.targetPort, () => {
server.removeListener('error', rej);
this.logger.info('Wait Java langserver connection on port ' + this.targetPort);
});
});
}
return this.connectingPromise;
}
/**
@ -225,7 +231,7 @@ export class LanguageServerProxy implements ILanguageServerHandler {
}
this.closed = false;
if (!this.connectingPromise) {
this.connectingPromise = new Promise((resolve, reject) => {
this.connectingPromise = new Promise(resolve => {
this.socket = new net.Socket();
this.socket.on('connect', () => {
@ -247,7 +253,6 @@ export class LanguageServerProxy implements ILanguageServerHandler {
this.targetPort,
this.targetHost
);
this.onDisconnected(() => setTimeout(() => this.reconnect(), 1000));
});
}
return this.connectingPromise;
@ -257,20 +262,12 @@ export class LanguageServerProxy implements ILanguageServerHandler {
return Promise.reject('should not hit here');
}
private reconnect() {
if (!this.isClosed) {
this.socket.connect(
this.targetPort,
this.targetHost
);
}
}
private onSocketClosed() {
if (this.clientConnection) {
this.clientConnection.dispose();
}
this.clientConnection = null;
this.connectingPromise = null;
this.eventEmitter.emit('close');
}
@ -305,4 +302,8 @@ export class LanguageServerProxy implements ILanguageServerHandler {
}
});
}
private tryConnect() {
return this.passiveConnection ? this.awaitServerConnection() : this.connect();
}
}

View file

@ -4,109 +4,60 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { spawn } from 'child_process';
import { ChildProcess, spawn } from 'child_process';
import getPort from 'get-port';
import { resolve } from 'path';
import { Logger } from '../log';
import { ServerOptions } from '../server_options';
import { LoggerFactory } from '../utils/log_factory';
import { ILanguageServerLauncher } from './language_server_launcher';
import { LanguageServerProxy } from './proxy';
import { RequestExpander } from './request_expander';
import { AbstractLauncher } from './abstract_launcher';
export class TypescriptServerLauncher implements ILanguageServerLauncher {
private isRunning: boolean = false;
constructor(
const TS_LANG_DETACH_PORT = 2089;
export class TypescriptServerLauncher extends AbstractLauncher {
public constructor(
readonly targetHost: string,
readonly options: ServerOptions,
readonly loggerFactory: LoggerFactory
) {}
public get running(): boolean {
return this.isRunning;
) {
super('typescript', targetHost, options, loggerFactory);
}
public async launch(builtinWorkspace: boolean, maxWorkspace: number, installationPath: string) {
let port = 2089;
async getPort() {
if (!this.options.lsp.detach) {
port = await getPort();
return await getPort();
}
const log: Logger = this.loggerFactory.getLogger(['code', `ts@${this.targetHost}:${port}`]);
const proxy = new LanguageServerProxy(port, this.targetHost, log, this.options.lsp);
return TS_LANG_DETACH_PORT;
}
if (this.options.lsp.detach) {
log.info('Detach mode, expected langserver launch externally');
proxy.onConnected(() => {
this.isRunning = true;
});
proxy.onDisconnected(() => {
this.isRunning = false;
if (!proxy.isClosed) {
log.warn('language server disconnected, reconnecting');
setTimeout(() => proxy.connect(), 1000);
}
});
} else {
const spawnTs = () => {
const p = spawn(
'node',
['--max_old_space_size=4096', installationPath, '-p', port.toString(), '-c', '1'],
{
detached: false,
stdio: 'pipe',
cwd: resolve(installationPath, '../..'),
}
);
p.stdout.on('data', data => {
log.stdout(data.toString());
});
p.stderr.on('data', data => {
log.stderr(data.toString());
});
this.isRunning = true;
p.on('exit', () => (this.isRunning = false));
return p;
};
let child = spawnTs();
log.info(`Launch Typescript Language Server at port ${port}, pid:${child.pid}`);
// TODO: how to properly implement timeout socket connection? maybe config during socket connection
// const reconnect = () => {
// log.debug('reconnecting');
// promiseTimeout(3000, proxy.connect()).then(
// () => {
// log.info('connected');
// },
// () => {
// log.error('unable to connect within 3s, respawn ts server.');
// child.kill();
// child = spawnTs();
// setTimeout(reconnect, 1000);
// }
// );
// };
proxy.onDisconnected(() => {
if (!proxy.isClosed) {
log.info('waiting language server to be connected');
if (!this.isRunning) {
log.error('detect language server killed, respawn ts server.');
child = spawnTs();
}
} else {
child.kill();
}
});
proxy.onExit(() => {
if (child) {
child.kill();
}
});
}
proxy.listen();
await proxy.connect();
createExpander(
proxy: LanguageServerProxy,
builtinWorkspace: boolean,
maxWorkspace: number
): RequestExpander {
return new RequestExpander(proxy, builtinWorkspace, maxWorkspace, this.options, {
installNodeDependency: this.options.security.installNodeDependency,
gitHostWhitelist: this.options.security.gitHostWhitelist,
});
}
async spawnProcess(installationPath: string, port: number, log: Logger): Promise<ChildProcess> {
const p = spawn(
'node',
['--max_old_space_size=4096', installationPath, '-p', port.toString(), '-c', '1'],
{
detached: false,
stdio: 'pipe',
cwd: resolve(installationPath, '../..'),
}
);
p.stdout.on('data', data => {
log.stdout(data.toString());
});
p.stderr.on('data', data => {
log.stderr(data.toString());
});
return p;
}
}