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 <[email protected]>
  • Loading branch information
amiramw committed May 29, 2019
1 parent a963e8b commit fac6886
Show file tree
Hide file tree
Showing 6 changed files with 116 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
39 changes: 37 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,41 @@ 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> {
let clientId = await this.client.getClientId();
if (clientId) {
return clientId;
}
clientId = this.pluginProcessCache.getFreshClientId();
await this.client.setClientId(clientId);
return clientId;
}

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 +99,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 +142,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,66 @@
/********************************************************************************
* 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';

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 getFreshClientId(): number {
return this.clientIdSeq++;
}

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 fac6886

Please sign in to comment.