[Code] change the port number before respawn a new lang-server process (#38090)

This commit is contained in:
Yulong 2019-06-07 10:50:41 +08:00 committed by GitHub
parent 76f686bf7d
commit 05947fed0c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 89 additions and 36 deletions

View file

@ -16,6 +16,7 @@ import { RequestExpander } from './request_expander';
import { LanguageServerProxy } from './proxy';
import { ConsoleLoggerFactory } from '../utils/console_logger_factory';
import { Logger } from '../log';
import getPort from 'get-port';
jest.setTimeout(10000);
@ -42,7 +43,7 @@ class MockLauncher extends AbstractLauncher {
}
async getPort() {
return 19999;
return await getPort();
}
async spawnProcess(installationPath: string, port: number, log: Logger): Promise<ChildProcess> {
@ -53,17 +54,17 @@ class MockLauncher extends AbstractLauncher {
console.log(msg);
mockMonitor(msg);
});
childProcess.send(`port ${await this.getPort()}`);
childProcess.send(`port ${port}`);
childProcess.send(`host ${this.targetHost}`);
childProcess.send('listen');
return childProcess;
}
protected killProcess(child: ChildProcess, log: Logger): Promise<boolean> {
protected killProcess(child: ChildProcess): Promise<boolean> {
// don't kill the process so fast, otherwise no normal exit can happen
return new Promise<boolean>(resolve => {
setTimeout(async () => {
const killed = await super.killProcess(child, log);
const killed = await super.killProcess(child);
resolve(killed);
}, 100);
});
@ -95,7 +96,7 @@ class PassiveMockLauncher extends MockLauncher {
console.log(msg);
mockMonitor(msg);
});
this.childProcess.send(`port ${await this.getPort()}`);
this.childProcess.send(`port ${port}`);
this.childProcess.send(`host ${this.targetHost}`);
if (this.dieFirstTime) {
this.childProcess!.send('quit');

View file

@ -12,25 +12,27 @@ import { Logger } from '../log';
import { LanguageServerProxy } from './proxy';
import { RequestExpander } from './request_expander';
let seqNo = 1;
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;
private readonly log: Logger;
protected constructor(
readonly name: string,
readonly targetHost: string,
readonly options: ServerOptions,
readonly loggerFactory: LoggerFactory
) {}
) {
this.log = this.loggerFactory.getLogger([`${seqNo++}`, `${this.name}`, 'code']);
}
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}`,
]);
const log: Logger = this.log;
let child: ChildProcess;
const proxy = new LanguageServerProxy(port, this.targetHost, log, this.options.lsp);
if (this.options.lsp.detach) {
@ -42,7 +44,7 @@ export abstract class AbstractLauncher implements ILanguageServerLauncher {
this.running = false;
if (!proxy.isClosed) {
log.debug(`${this.name} language server disconnected, reconnecting`);
setTimeout(() => this.reconnect(proxy, installationPath, port, log), 1000);
setTimeout(() => this.reconnect(proxy, installationPath), 1000);
}
});
} else {
@ -53,18 +55,18 @@ export abstract class AbstractLauncher implements ILanguageServerLauncher {
this._startTime = Date.now();
this.running = true;
this.onProcessExit(child, () => {
if (!proxy.isClosed) this.reconnect(proxy, installationPath, port, log);
if (!proxy.isClosed) this.reconnect(proxy, installationPath);
});
proxy.onDisconnected(async () => {
this._proxyConnected = false;
if (!proxy.isClosed) {
log.debug('proxy disconnected, reconnecting');
setTimeout(async () => {
await this.reconnect(proxy, installationPath, port, log, child);
await this.reconnect(proxy, installationPath, child);
}, 1000);
} else if (this.child) {
log.info('proxy closed, kill process');
await this.killProcess(this.child, log);
await this.killProcess(this.child);
}
});
}
@ -72,7 +74,7 @@ export abstract class AbstractLauncher implements ILanguageServerLauncher {
log.debug('proxy exited, is the process running? ' + this.running);
if (this.child && this.running) {
const p = this.child!;
this.killProcess(p, log);
this.killProcess(p);
}
});
proxy.listen();
@ -102,7 +104,7 @@ export abstract class AbstractLauncher implements ILanguageServerLauncher {
/**
* proxy should be connected within this timeout, otherwise we reconnect.
*/
protected startupTimeout = 3000;
protected startupTimeout = 10000;
/**
* try reconnect the proxy when disconnected
@ -110,11 +112,9 @@ export abstract class AbstractLauncher implements ILanguageServerLauncher {
public async reconnect(
proxy: LanguageServerProxy,
installationPath: string,
port: number,
log: Logger,
child?: ChildProcess
) {
log.debug('reconnecting');
this.log.debug('reconnecting');
if (this.options.lsp.detach) {
this.startConnect(proxy);
} else {
@ -123,17 +123,17 @@ export abstract class AbstractLauncher implements ILanguageServerLauncher {
this.startConnect(proxy);
} else {
if (child && this.running) {
log.debug('killing the old process.');
await this.killProcess(child, log);
this.log.debug('killing the old process.');
await this.killProcess(child);
}
this.child = await this.spawnProcess(installationPath, port, log);
log.debug('spawned a child process ' + this.child.pid);
const port = await this.getPort();
proxy.changePort(port);
this.child = await this.spawnProcess(installationPath, port, this.log);
this.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.onProcessExit(this.child, () => this.reconnect(proxy, installationPath, child));
this.startConnect(proxy);
}
}
@ -161,7 +161,7 @@ export abstract class AbstractLauncher implements ILanguageServerLauncher {
log: Logger
): Promise<ChildProcess>;
protected killProcess(child: ChildProcess, log: Logger) {
protected killProcess(child: ChildProcess) {
if (!child.killed) {
return new Promise<boolean>((resolve, reject) => {
// if not killed within 1s
@ -171,12 +171,12 @@ export abstract class AbstractLauncher implements ILanguageServerLauncher {
resolve(true);
});
child.kill();
log.info('killed process ' + child.pid);
this.log.info('killed process ' + child.pid);
})
.catch(() => {
// force kill
child.kill('SIGKILL');
log.info('force killed process ' + child.pid);
this.log.info('force killed process ' + child.pid);
return child.killed;
})
.finally(() => {

View file

@ -32,6 +32,7 @@ import { HttpMessageReader } from './http_message_reader';
import { HttpMessageWriter } from './http_message_writer';
import { HttpRequestEmitter } from './http_request_emitter';
import { createRepliesMap } from './replies_map';
import { Cancelable } from '../utils/cancelable';
export interface ILanguageServerHandler {
lastAccess?: number;
@ -54,13 +55,13 @@ export class LanguageServerProxy implements ILanguageServerHandler {
private httpEmitter = new HttpRequestEmitter();
private replies = createRepliesMap();
private readonly targetHost: string;
private readonly targetPort: number;
private targetPort: number;
private readonly logger: Logger;
private readonly lspOptions: LspOptions;
private eventEmitter = new EventEmitter();
private passiveConnection: boolean = false;
private connectingPromise: Promise<MessageConnection> | null = null;
private connectingPromise: Cancelable<MessageConnection> | null = null;
constructor(targetPort: number, targetHost: string, logger: Logger, lspOptions: LspOptions) {
this.targetHost = targetHost;
@ -178,7 +179,7 @@ export class LanguageServerProxy implements ILanguageServerHandler {
// 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) => {
this.connectingPromise = new Cancelable((res, rej, onCancel) => {
const server = net.createServer(socket => {
this.initialized = false;
server.close();
@ -199,9 +200,13 @@ export class LanguageServerProxy implements ILanguageServerHandler {
server.removeListener('error', rej);
this.logger.info('Wait langserver connection on port ' + this.targetPort);
});
onCancel!(() => {
server.close();
rej('canceled');
});
});
}
return this.connectingPromise;
return this.connectingPromise.promise;
}
/**
@ -230,7 +235,7 @@ export class LanguageServerProxy implements ILanguageServerHandler {
}
this.closed = false;
if (!this.connectingPromise) {
this.connectingPromise = new Promise(resolve => {
this.connectingPromise = new Cancelable((resolve, reject, onCancel) => {
this.socket = new net.Socket();
this.socket.on('connect', () => {
@ -252,9 +257,12 @@ export class LanguageServerProxy implements ILanguageServerHandler {
this.targetPort,
this.targetHost
);
onCancel!(() => {
reject('canceled');
});
});
}
return this.connectingPromise;
return this.connectingPromise.promise;
}
public unloadWorkspace(workspaceDir: string): Promise<void> {
@ -300,6 +308,18 @@ export class LanguageServerProxy implements ILanguageServerHandler {
}
private tryConnect() {
return this.passiveConnection ? this.awaitServerConnection() : this.connect();
return this.passiveConnection
? ((this.connectingPromise as unknown) as Promise<MessageConnection>)
: this.connect();
}
public changePort(port: number) {
if (port !== this.targetPort) {
this.targetPort = port;
if (this.connectingPromise) {
this.connectingPromise.cancel();
this.connectingPromise = null;
}
}
}
}

View file

@ -0,0 +1,32 @@
/*
* 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.
*/
type Resolve<T> = (t: T) => void;
type Reject = (error: any) => void;
type Cancel = () => void;
type OnCancel = (cancel: Cancel) => void;
export class Cancelable<T> {
public readonly promise: Promise<T>;
private resolve: Resolve<T> | undefined = undefined;
private reject: Reject | undefined = undefined;
private _cancel: Cancel | undefined = undefined;
constructor(readonly fn: (resolve: Resolve<T>, reject: Reject, onCancel: OnCancel) => void) {
this.promise = new Promise<T>((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
fn(this.resolve!, this.reject!, (cancel: Cancel) => {
this._cancel = cancel;
});
}
public cancel(): void {
if (this._cancel) {
this._cancel();
}
}
}