Skip to content

Commit

Permalink
Introduce timeout for keeping connection contexts alive
Browse files Browse the repository at this point in the history
Fixes #12823

- refactor front end to allow for multiple reconnections
- remove IWebsockt abstractions
- separate front end connections from service channel management
- introduce mechanism to reconnect front end to existing connection
  context based on timeouts

Contributed on behalf of STMicroelectronics

Signed-off-by: Thomas Mäder <t.s.maeder@gmail.com>
  • Loading branch information
tsmaeder committed Nov 20, 2023
1 parent bf93b29 commit 976f752
Show file tree
Hide file tree
Showing 53 changed files with 1,296 additions and 702 deletions.
15 changes: 14 additions & 1 deletion dev-packages/application-package/src/application-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ export namespace FrontendApplicationConfig {
defaultIconTheme: 'theia-file-icons',
electron: ElectronFrontendApplicationConfig.DEFAULT,
defaultLocale: '',
validatePreferencesSchema: true
validatePreferencesSchema: true,
reloadOnReconnect: false
};
export interface Partial extends ApplicationConfig {

Expand Down Expand Up @@ -132,6 +133,12 @@ export namespace FrontendApplicationConfig {
* Defaults to `true`.
*/
readonly validatePreferencesSchema?: boolean;

/**
* When 'true', the window will reload in case the front end reconnects to a back-end,
* but the back end does not have a connection context for this front end anymore.
*/
readonly reloadOnReconnect?: boolean;
}
}

Expand All @@ -142,6 +149,7 @@ export type BackendApplicationConfig = RequiredRecursive<BackendApplicationConfi
export namespace BackendApplicationConfig {
export const DEFAULT: BackendApplicationConfig = {
singleInstance: false,
frontendConnectionTimeout: 1000
};
export interface Partial extends ApplicationConfig {

Expand All @@ -151,6 +159,11 @@ export namespace BackendApplicationConfig {
* Defaults to `false`.
*/
readonly singleInstance?: boolean;

/**
* The time in ms the connection context will be preserved for reconnection after a front end disconnects.
*/
readonly frontendConnectionTimeout?: number;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
// *****************************************************************************

import { ContainerModule } from '@theia/core/shared/inversify';
import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider';
import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-source';
import { CommandContribution, MenuContribution } from '@theia/core/lib/common';
import { SampleUpdater, SampleUpdaterPath, SampleUpdaterClient } from '../../common/updater/sample-updater';
import { SampleUpdaterFrontendContribution, ElectronMenuUpdater, SampleUpdaterClientImpl } from './sample-updater-frontend-contribution';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@
import { ContainerModule } from '@theia/core/shared/inversify';
import { RpcConnectionHandler } from '@theia/core/lib/common/messaging/proxy-factory';
import { ElectronMainApplicationContribution } from '@theia/core/lib/electron-main/electron-main-application';
import { ElectronConnectionHandler } from '@theia/core/lib/electron-common/messaging/electron-connection-handler';
import { SampleUpdaterPath, SampleUpdater, SampleUpdaterClient } from '../../common/updater/sample-updater';
import { SampleUpdaterImpl } from './sample-updater-impl';
import { ConnectionHandler } from '@theia/core';

export default new ContainerModule(bind => {
bind(SampleUpdaterImpl).toSelf().inSingletonScope();
bind(SampleUpdater).toService(SampleUpdaterImpl);
bind(ElectronMainApplicationContribution).toService(SampleUpdater);
bind(ElectronConnectionHandler).toDynamicValue(context =>
bind(ConnectionHandler).toDynamicValue(context =>
new RpcConnectionHandler<SampleUpdaterClient>(SampleUpdaterPath, client => {
const server = context.container.get<SampleUpdater>(SampleUpdater);
server.setClient(client);
Expand Down
9 changes: 8 additions & 1 deletion examples/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@
"applicationName": "Theia Browser Example",
"preferences": {
"files.enableTrash": false
}
},
"reloadOnReconnect": true
}
},
"backend": {
"config": {
"frontendConnectionTimeout": 3000
}

}
},
"dependencies": {
Expand Down
8 changes: 7 additions & 1 deletion examples/electron/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@
"target": "electron",
"frontend": {
"config": {
"applicationName": "Theia Electron Example"
"applicationName": "Theia Electron Example",
"reloadOnReconnect": true
}
},
"backend": {
"config": {
"frontendConnectionTimeout": -1
}
}
},
Expand Down
12 changes: 6 additions & 6 deletions packages/core/src/browser/connection-status-service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ import { MockConnectionStatusService } from './test/mock-connection-status-servi
import * as sinon from 'sinon';

import { Container } from 'inversify';
import { WebSocketConnectionProvider } from './messaging/ws-connection-provider';
import { ILogger, Emitter, Loggable } from '../common';
import { WebsocketConnectionSource } from './messaging/ws-connection-source';

disableJSDOM();

Expand Down Expand Up @@ -101,7 +101,7 @@ describe('frontend-connection-status', function (): void {
let timer: sinon.SinonFakeTimers;
let pingSpy: sinon.SinonSpy;
beforeEach(() => {
const mockWebSocketConnectionProvider = sinon.createStubInstance(WebSocketConnectionProvider);
const mockWebSocketConnectionSource = sinon.createStubInstance(WebsocketConnectionSource);
const mockPingService: PingService = <PingService>{
ping(): Promise<void> {
return Promise.resolve(undefined);
Expand All @@ -118,11 +118,11 @@ describe('frontend-connection-status', function (): void {
testContainer.bind(PingService).toConstantValue(mockPingService);
testContainer.bind(ILogger).toConstantValue(mockILogger);
testContainer.bind(ConnectionStatusOptions).toConstantValue({ offlineTimeout: OFFLINE_TIMEOUT });
testContainer.bind(WebSocketConnectionProvider).toConstantValue(mockWebSocketConnectionProvider);
testContainer.bind(WebsocketConnectionSource).toConstantValue(mockWebSocketConnectionSource);

sinon.stub(mockWebSocketConnectionProvider, 'onSocketDidOpen').value(mockSocketOpenedEmitter.event);
sinon.stub(mockWebSocketConnectionProvider, 'onSocketDidClose').value(mockSocketClosedEmitter.event);
sinon.stub(mockWebSocketConnectionProvider, 'onIncomingMessageActivity').value(mockIncomingMessageActivityEmitter.event);
sinon.stub(mockWebSocketConnectionSource, 'onSocketDidOpen').value(mockSocketOpenedEmitter.event);
sinon.stub(mockWebSocketConnectionSource, 'onSocketDidClose').value(mockSocketClosedEmitter.event);
sinon.stub(mockWebSocketConnectionSource, 'onIncomingMessageActivity').value(mockIncomingMessageActivityEmitter.event);

timer = sinon.useFakeTimers();

Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/browser/connection-status-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import { ILogger } from '../common/logger';
import { Event, Emitter } from '../common/event';
import { DefaultFrontendApplicationContribution } from './frontend-application-contribution';
import { StatusBar, StatusBarAlignment } from './status-bar/status-bar';
import { WebSocketConnectionProvider } from './messaging/ws-connection-provider';
import { Disposable, DisposableCollection, nls } from '../common';
import { WebsocketConnectionSource } from './messaging/ws-connection-source';

/**
* Service for listening on backend connection changes.
Expand Down Expand Up @@ -119,7 +119,7 @@ export class FrontendConnectionStatusService extends AbstractConnectionStatusSer

private scheduledPing: number | undefined;

@inject(WebSocketConnectionProvider) protected readonly wsConnectionProvider: WebSocketConnectionProvider;
@inject(WebsocketConnectionSource) protected readonly wsConnectionProvider: WebsocketConnectionSource;
@inject(PingService) protected readonly pingService: PingService;

@postConstruct()
Expand Down
26 changes: 26 additions & 0 deletions packages/core/src/browser/messaging/connection-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// *****************************************************************************
// Copyright (C) 2023 STMicroelectronics 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-only WITH Classpath-exception-2.0
// *****************************************************************************

import { Channel, Event } from "../../common";

export const ConnectionSource = Symbol('ConnectionSource');

/**
* A ConnectionSource creates a Channel. The channel is valid until it sends a close event.
*/
export interface ConnectionSource {
onConnectionDidOpen: Event<Channel>;
}
38 changes: 38 additions & 0 deletions packages/core/src/browser/messaging/frontend-id-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// *****************************************************************************
// Copyright (C) 2023 STMicroelectronics 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-only WITH Classpath-exception-2.0
// *****************************************************************************

import { injectable } from "inversify";
import { generateUuid } from "../../common/uuid";

export const FrontendIdProvider = Symbol('FrontendIdProvider');

/**
* A FronendIdProvider computes an id for an instance of the front end that may be reconnected to a back end
* connection context.
*/
export interface FrontendIdProvider {
getId(): string;
}

@injectable()
export class BrowserFrontendIdProvider implements FrontendIdProvider {
protected readonly id = generateUuid(); // generate a new id each time we load the application

getId(): string {
return this.id;
}

}
24 changes: 22 additions & 2 deletions packages/core/src/browser/messaging/messaging-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,29 @@
// *****************************************************************************

import { ContainerModule } from 'inversify';
import { LocalWebSocketConnectionProvider, WebSocketConnectionProvider } from './ws-connection-provider';
import { BrowserFrontendIdProvider, FrontendIdProvider } from './frontend-id-provider';
import { WebsocketConnectionSource } from './ws-connection-source';
import { LocalConnectionProvider, RemoteConnectionProvider, ServiceConnectionProvider } from './service-connection-provider';
import { ConnectionSource } from './connection-source';
import { ConnectionCloseService, connectionCloseServicePath } from '../../common/messaging/connection-management';
import { WebSocketConnectionProvider } from './ws-connection-provider';

const backendServiceProvider = Symbol('backendServiceProvider');

export const messagingFrontendModule = new ContainerModule(bind => {
bind(ConnectionCloseService).toDynamicValue(ctx => {
return WebSocketConnectionProvider.createProxy(ctx.container, connectionCloseServicePath);
}).inSingletonScope();
bind(BrowserFrontendIdProvider).toSelf().inSingletonScope();
bind(FrontendIdProvider).toService(BrowserFrontendIdProvider);
bind(WebsocketConnectionSource).toSelf().inSingletonScope();
bind(backendServiceProvider).toDynamicValue(ctx => {
bind(ServiceConnectionProvider).toSelf().inSingletonScope();
const container = ctx.container.createChild();
container.bind(ConnectionSource).toService(WebsocketConnectionSource);
return container.get(ServiceConnectionProvider);
}).inSingletonScope();
bind(LocalConnectionProvider).toService(backendServiceProvider);
bind(RemoteConnectionProvider).toService(backendServiceProvider);
bind(WebSocketConnectionProvider).toSelf().inSingletonScope();
bind(LocalWebSocketConnectionProvider).toService(WebSocketConnectionProvider);
});
127 changes: 127 additions & 0 deletions packages/core/src/browser/messaging/service-connection-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// *****************************************************************************
// Copyright (C) 2020 Ericsson 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-only WITH Classpath-exception-2.0
// *****************************************************************************

import { inject, injectable, interfaces, postConstruct } from 'inversify';
import { Channel, RpcProxy, RpcProxyFactory } from '../../common';
import { ChannelMultiplexer } from '../../common/message-rpc/channel';
import { Deferred } from '../../common/promise-util';
import { ConnectionSource } from './connection-source';


export const LocalConnectionProvider = Symbol('LocalConnectionProvider');
export const RemoteConnectionProvider = Symbol('RemoteConnectionProvider');

export namespace ServiceConnectionProvider {
export type ConnectionHandler = (path: String, channel: Channel) => void;
}

/**
* This class manages the channels for remote services in the back end
*/
@injectable()
export class ServiceConnectionProvider {

static createProxy<T extends object>(container: interfaces.Container, path: string, arg?: object): RpcProxy<T> {
return container.get<ServiceConnectionProvider>(RemoteConnectionProvider).createProxy(path, arg);
}

static createLocalProxy<T extends object>(container: interfaces.Container, path: string, arg?: object): RpcProxy<T> {
return container.get<ServiceConnectionProvider>(LocalConnectionProvider).createProxy(path, arg);
}

static createHandler(container: interfaces.Container, path: string, arg?: object): void {
const remote = container.get<ServiceConnectionProvider>(RemoteConnectionProvider);
const local = container.get<ServiceConnectionProvider>(LocalConnectionProvider);
remote.createProxy(path, arg);
if (remote !== local) {
local.createProxy(path, arg);
}
}

protected readonly channelHandlers = new Map<string, ServiceConnectionProvider.ConnectionHandler>();

/**
* Create a proxy object to remote interface of T type
* over a web socket connection for the given path and proxy factory.
*/
createProxy<T extends object>(path: string, factory: RpcProxyFactory<T>): RpcProxy<T>;
/**
* Create a proxy object to remote interface of T type
* over a web socket connection for the given path.
*
* An optional target can be provided to handle
* notifications and requests from a remote side.
*/
createProxy<T extends object>(path: string, target?: object): RpcProxy<T>;
createProxy<T extends object>(path: string, arg?: object): RpcProxy<T> {
const factory = arg instanceof RpcProxyFactory ? arg : new RpcProxyFactory<T>(arg);
this.listen(path, (_, c) => factory.listen(c), true);
return factory.createProxy();
}

protected channelMultiplexer: ChannelMultiplexer;

private channelReadyDeferred = new Deferred<void>();
protected get channelReady(): Promise<void> {
return this.channelReadyDeferred.promise;
}

@postConstruct()
init(): void {
this.connectionSource.onConnectionDidOpen(channel => this.handleChannelCreated(channel));
}

@inject(ConnectionSource)
protected connectionSource: ConnectionSource;

/**
* This method must be invoked by subclasses when they have created the main channel.
* @param mainChannel
*/
protected handleChannelCreated(channel: Channel): void {
channel.onClose(() => {
this.handleChannelClosed(channel);
});

this.channelMultiplexer = new ChannelMultiplexer(channel);
this.channelReadyDeferred.resolve();
for (const entry of this.channelHandlers.entries()) {
this.openChannel(entry[0], entry[1]);
}
}

handleChannelClosed(channel: Channel): void {
this.channelReadyDeferred = new Deferred();
}

/**
* Install a connection handler for the given path.
*/
listen(path: string, handler: ServiceConnectionProvider.ConnectionHandler, reconnect: boolean): void {
this.openChannel(path, handler).then(() => {
if (reconnect) {
this.channelHandlers.set(path, handler);
}
});

}

private async openChannel(path: string, handler: ServiceConnectionProvider.ConnectionHandler): Promise<void> {
await this.channelReady;
const newChannel = await this.channelMultiplexer.open(path);
handler(path, newChannel);
}
}
Loading

0 comments on commit 976f752

Please sign in to comment.