[Code] change the port number before respawn a new lang-server process (#38090)
This commit is contained in:
parent
76f686bf7d
commit
05947fed0c
|
@ -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');
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
32
x-pack/plugins/code/server/utils/cancelable.ts
Normal file
32
x-pack/plugins/code/server/utils/cancelable.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue