mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
parent
fe7aa09898
commit
400d661b92
6 changed files with 598 additions and 179 deletions
186
x-pack/plugins/code/server/lsp/abstract_launcher.test.ts
Normal file
186
x-pack/plugins/code/server/lsp/abstract_launcher.test.ts
Normal 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);
|
||||
});
|
190
x-pack/plugins/code/server/lsp/abstract_launcher.ts
Normal file
190
x-pack/plugins/code/server/lsp/abstract_launcher.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
126
x-pack/plugins/code/server/lsp/mock_lang_server.js
Normal file
126
x-pack/plugins/code/server/lsp/mock_lang_server.js
Normal 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
|
||||
}
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue