Skip to content

Commit

Permalink
Reconnect same host plugin process and client
Browse files Browse the repository at this point in the history
Fixes problem of plugins not working after short disconnection.
Keeps disconnected host plugin process for at least one minute and
reconnect it if the client is the same.

Signed-off-by: Amiram Wingarten <amiram.wingarten@sap.com>
  • Loading branch information
amiramw authored and akosyakov committed May 30, 2019
1 parent 6860417 commit 797db75
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 3 deletions.
4 changes: 4 additions & 0 deletions packages/plugin-ext/src/common/plugin-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,10 @@ export interface DebugConfiguration {

export const HostedPluginClient = Symbol('HostedPluginClient');
export interface HostedPluginClient {
setClientId(clientId: number): Promise<void>;

getClientId(): Promise<number>;

postMessage(message: string): Promise<void>;

log(logPart: LogPart): void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ export class HostedPluginWatcher {
getHostedPluginClient(): HostedPluginClient {
const messageEmitter = this.onPostMessage;
const logEmitter = this.onLogMessage;
let clientId = 0;
return {
getClientId: () => Promise.resolve(clientId),
setClientId: (id: number) => {
clientId = id;
return Promise.resolve();
},
postMessage(message: string): Promise<void> {
messageEmitter.fire(JSON.parse(message));
return Promise.resolve();
Expand Down
33 changes: 31 additions & 2 deletions packages/plugin-ext/src/hosted/node/hosted-plugin-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { HostedPluginClient, ServerPluginRunner, PluginMetadata, PluginHostEnvir
import { RPCProtocolImpl } from '../../api/rpc-protocol';
import { MAIN_RPC_CONTEXT } from '../../api/plugin-api';
import { HostedPluginCliContribution } from './hosted-plugin-cli-contribution';
import {HostedPluginProcessesCache} from './hosted-plugin-processes-cache';

export interface IPCConnectionOptions {
readonly serverName: string;
Expand All @@ -41,20 +42,35 @@ export class HostedPluginProcess implements ServerPluginRunner {
@inject(HostedPluginCliContribution)
protected readonly cli: HostedPluginCliContribution;

@inject(HostedPluginProcessesCache)
protected readonly pluginProcessCache: HostedPluginProcessesCache;

@inject(ContributionProvider)
@named(PluginHostEnvironmentVariable)
protected readonly pluginHostEnvironmentVariables: ContributionProvider<PluginHostEnvironmentVariable>;

private childProcess: cp.ChildProcess | undefined;

private client: HostedPluginClient;

private async getClientId(): Promise<number> {
return await this.pluginProcessCache.getLazyClientId(this.client);
}

public setClient(client: HostedPluginClient): void {
if (this.client) {
if (this.childProcess) {
this.runPluginServer();
}
}
this.client = client;
this.getClientId().then(clientId => {
const childProcess = this.pluginProcessCache.retrieveClientChildProcess(clientId);
if (!this.childProcess && childProcess) {
this.childProcess = childProcess;
this.linkClientWithChildProcess(this.childProcess);
}
});
}

public clientClosed(): void {
Expand All @@ -77,6 +93,12 @@ export class HostedPluginProcess implements ServerPluginRunner {
}
}

public markPluginServerTerminated() {
if (this.childProcess) {
this.pluginProcessCache.scheduleChildProcessTermination(this, this.childProcess);
}
}

public terminatePluginServer(): void {
if (this.childProcess === undefined) {
return;
Expand Down Expand Up @@ -114,12 +136,19 @@ export class HostedPluginProcess implements ServerPluginRunner {
logger: this.logger,
args: []
});
this.childProcess.on('message', message => {
this.linkClientWithChildProcess(this.childProcess);

}

private linkClientWithChildProcess(childProcess: cp.ChildProcess) {
childProcess.on('message', message => {
if (this.client) {
this.client.postMessage(message);
}
});

this.getClientId().then(clientId => {
this.pluginProcessCache.linkLiveClientAndProcess(clientId, childProcess);
});
}

readonly HOSTED_PLUGIN_ENV_REGEXP_EXCLUSION = new RegExp('HOSTED_PLUGIN*');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/********************************************************************************
* Copyright (c) 2019 SAP SE or an SAP affiliate company and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { injectable } from 'inversify';
import * as cp from 'child_process';
import { HostedPluginProcess } from './hosted-plugin-process';
import { HostedPluginClient } from '../../common/plugin-protocol';

const DEF_MIN_KEEP_ALIVE_DISCONNECT_TIME = 5 * 1000; // 5 seconds

@injectable()
export class HostedPluginProcessesCache {

// child processes are kept for one minute in order to reuse them in case of network disconnections
private cachedCPMap: Map<number, { cp: cp.ChildProcess, toBeKilledAfter: number } > = new Map();

// client ids sequence
private clientIdSeq = 1;

private minKeepAliveDisconnectTime: number = process.env.THEIA_PLUGIN_HOST_MIN_KEEP_ALIVE ?
parseInt(process.env.THEIA_PLUGIN_HOST_MIN_KEEP_ALIVE) : DEF_MIN_KEEP_ALIVE_DISCONNECT_TIME;

public async getLazyClientId(client: HostedPluginClient): Promise<number> {
let clientId = await client.getClientId();
if (clientId && clientId <= this.clientIdSeq) {
return clientId;
}
clientId = this.clientIdSeq++;
await client.setClientId(clientId);
return clientId;
}

public linkLiveClientAndProcess(clientId: number, childProcess: cp.ChildProcess) {
this.cachedCPMap.set(clientId, {
cp: childProcess,
toBeKilledAfter: Infinity
});
}

public retrieveClientChildProcess(clientID: number): cp.ChildProcess | undefined {
const childProcessDatum = this.cachedCPMap.get(clientID);
return childProcessDatum && childProcessDatum.cp;
}

public scheduleChildProcessTermination(hostedPluginProcess: HostedPluginProcess, childProcess: cp.ChildProcess) {
for (const cachedChildProcessesDatum of this.cachedCPMap.values()) {
if (cachedChildProcessesDatum.cp === childProcess) {
cachedChildProcessesDatum.toBeKilledAfter = new Date().getTime() + this.minKeepAliveDisconnectTime;
}
}
setTimeout(() => {
this.cachedCPMap.forEach((cachedChildProcessesDatum, clientId) => {
if (cachedChildProcessesDatum.cp === childProcess && cachedChildProcessesDatum.toBeKilledAfter < new Date().getTime()) {
this.cachedCPMap.delete(clientId);
hostedPluginProcess.terminatePluginServer();
}
});
}, this.minKeepAliveDisconnectTime * 2);
}
}
2 changes: 1 addition & 1 deletion packages/plugin-ext/src/hosted/node/hosted-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export class HostedPluginSupport {
}

private terminatePluginServer(): void {
this.hostedPluginProcess.terminatePluginServer();
this.hostedPluginProcess.markPluginServerTerminated();
}

public runPluginServer(): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { HostedPluginProcess } from './hosted-plugin-process';
import { ExtPluginApiProvider } from '../../common/plugin-ext-api-contribution';
import { HostedPluginCliContribution } from './hosted-plugin-cli-contribution';
import { HostedPluginDeployerHandler } from './hosted-plugin-deployer-handler';
import { HostedPluginProcessesCache } from './hosted-plugin-processes-cache';

const commonHostedConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => {
bind(HostedPluginProcess).toSelf().inSingletonScope();
Expand All @@ -54,6 +55,7 @@ const commonHostedConnectionModule = ConnectionContainerModule.create(({ bind, b
});

export function bindCommonHostedBackend(bind: interfaces.Bind): void {
bind(HostedPluginProcessesCache).toSelf().inSingletonScope();
bind(HostedPluginCliContribution).toSelf().inSingletonScope();
bind(CliContribution).toService(HostedPluginCliContribution);

Expand Down

0 comments on commit 797db75

Please sign in to comment.