Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement backend to electron IPC channels #10698

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@
{
"backend": "lib/node/hosting/backend-hosting-module",
"backendElectron": "lib/electron-node/hosting/electron-backend-hosting-module"
},
{
"backendElectron": "lib/electron-node/messaging/electron-backend-connection-module"
}
],
"keywords": [
Expand Down
23 changes: 23 additions & 0 deletions packages/core/src/electron-common/electron-test-connection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/********************************************************************************
* Copyright (C) 2022 TypeFox 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
********************************************************************************/

export const TestConnection = Symbol('TestConnection');

export const TEST_CONNECTION_PATH = '/services/test-connection';

export interface TestConnection {
runTest(): Promise<string[]>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/********************************************************************************
* Copyright (C) 2022 TypeFox 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 { ConnectionHandler } from '../../common/messaging/handler';

/**
* Name of the channel used with `process.on/message`.
*/
export const THEIA_ELECTRON_BACKEND_IPC_CHANNEL_NAME = 'theia-electron-backend-ipc';

export interface ElectronBackendMessage {
[THEIA_ELECTRON_BACKEND_IPC_CHANNEL_NAME]: string
}

export namespace ElectronBackendMessage {
export function is(message: unknown): message is ElectronBackendMessage {
return typeof message === 'object' && !!message && THEIA_ELECTRON_BACKEND_IPC_CHANNEL_NAME in message;
}
export function get(message: ElectronBackendMessage): string {
return message[THEIA_ELECTRON_BACKEND_IPC_CHANNEL_NAME];
}
export function create(data: string): ElectronBackendMessage {
return { [THEIA_ELECTRON_BACKEND_IPC_CHANNEL_NAME]: data };
}
}

/**
* A class capable of piping messaging data from the backend server to the electron main application.
* This should only be used when the app runs in `--no-cluster` mode, since we can't use normal inter-process communication.
*/
export class ElectronBackendMessagePipe {

protected electronHandler?: (data: string) => void;
protected backendHandler?: (data: string) => void;

onMessage(from: 'backend' | 'electron', handler: (data: string) => void): boolean {
if (from === 'backend') {
this.electronHandler = handler;
return !!this.backendHandler;
} else {
this.backendHandler = handler;
return !!this.electronHandler;
}
}

pushMessage(to: 'backend' | 'electron', data: string): boolean {
if (to === 'backend') {
this.electronHandler?.(data);
return !!this.electronHandler;
} else {
this.backendHandler?.(data);
return !!this.backendHandler;
}
}

}

export const ElectronBackendConnectionPipe = new ElectronBackendMessagePipe();

/**
* IPC-specific connection handler.
* Use this if you want to establish communication from the electron-main to the backend process.
*/
export const ElectronMainConnectionHandler = Symbol('ElectronBackendConnectionHandler');

export interface ElectronMainConnectionHandler extends ConnectionHandler {
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ import { ElectronMessagingContribution } from './messaging/electron-messaging-co
import { ElectronMessagingService } from './messaging/electron-messaging-service';
import { ElectronConnectionHandler } from '../electron-common/messaging/electron-connection-handler';
import { ElectronSecurityTokenService } from './electron-security-token-service';
import { ElectronMainMessagingService } from './messaging/electron-main-messaging-service';
import { ElectronMainConnectionHandler } from '../electron-common/messaging/electron-backend-connection-handler';
import { TestConnection, TEST_CONNECTION_PATH } from '../electron-common/electron-test-connection';
import { TestConnectionImpl } from './electron-test-connection';

const electronSecurityToken: ElectronSecurityToken = { value: v4() };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -34,12 +38,14 @@ const electronSecurityToken: ElectronSecurityToken = { value: v4() };
export default new ContainerModule(bind => {
bind(ElectronMainApplication).toSelf().inSingletonScope();
bind(ElectronMessagingContribution).toSelf().inSingletonScope();
bind(ElectronMainMessagingService).toSelf().inSingletonScope();
bind(ElectronSecurityToken).toConstantValue(electronSecurityToken);
bind(ElectronSecurityTokenService).toSelf().inSingletonScope();

bindContributionProvider(bind, ElectronConnectionHandler);
bindContributionProvider(bind, ElectronMessagingService.Contribution);
bindContributionProvider(bind, ElectronMainApplicationContribution);
bindContributionProvider(bind, ElectronMainConnectionHandler);

bind(ElectronMainApplicationContribution).toService(ElectronMessagingContribution);

Expand All @@ -50,4 +56,10 @@ export default new ContainerModule(bind => {
).inSingletonScope();

bind(ElectronMainProcessArgv).toSelf().inSingletonScope();

bind(TestConnection).to(TestConnectionImpl).inSingletonScope();
bind(ElectronMainConnectionHandler).toDynamicValue(context =>
new JsonRpcConnectionHandler(TEST_CONNECTION_PATH,
() => context.container.get(TestConnection))
).inSingletonScope();
});
10 changes: 9 additions & 1 deletion packages/core/src/electron-main/electron-main-application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
TitleBarStyleChanged
} from '../electron-common/messaging/electron-messages';
import { DEFAULT_WINDOW_HASH } from '../common/window';
import { ElectronMainMessagingService } from './messaging/electron-main-messaging-service';

const createYargs: (argv?: string[], cwd?: string) => Argv = require('yargs/yargs');

Expand Down Expand Up @@ -185,6 +186,9 @@ export class ElectronMainApplication {
@inject(ElectronMainProcessArgv)
protected processArgv: ElectronMainProcessArgv;

@inject(ElectronMainMessagingService)
protected readonly mainMessagingService: ElectronMainMessagingService;

@inject(ElectronSecurityTokenService)
protected electronSecurityTokenService: ElectronSecurityTokenService;

Expand Down Expand Up @@ -533,6 +537,7 @@ export class ElectronMainApplication {
// Otherwise, the forked backend processes will not know that they're serving the electron frontend.
process.env.THEIA_ELECTRON_VERSION = process.versions.electron;
if (noBackendFork) {
this.mainMessagingService.start();
process.env[ElectronSecurityToken] = JSON.stringify(this.electronSecurityToken);
// The backend server main file is supposed to export a promise resolving with the port used by the http(s) server.
const address: AddressInfo = await require(this.globals.THEIA_BACKEND_MAIN_PATH);
Expand All @@ -543,10 +548,13 @@ export class ElectronMainApplication {
this.processArgv.getProcessArgvWithoutBin(),
await this.getForkOptions(),
);
this.mainMessagingService.start(backendProcess);
return new Promise((resolve, reject) => {
// The backend server main file is also supposed to send the resolved http(s) server port via IPC.
backendProcess.on('message', (address: AddressInfo) => {
resolve(address.port);
if ('port' in address) {
resolve(address.port);
}
});
backendProcess.on('error', error => {
reject(error);
Expand Down
27 changes: 27 additions & 0 deletions packages/core/src/electron-main/electron-test-connection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/********************************************************************************
* Copyright (C) 2022 TypeFox 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 { TestConnection } from '../electron-common/electron-test-connection';
import { BrowserWindow } from '@theia/electron/shared/electron';
import { injectable } from 'inversify';

@injectable()
export class TestConnectionImpl implements TestConnection {
async runTest(): Promise<string[]> {
const titles = BrowserWindow.getAllWindows().map(e => e.getTitle());
return titles;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/********************************************************************************
* Copyright (C) 2022 TypeFox 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
********************************************************************************/

/* eslint-disable @typescript-eslint/no-explicit-any */

import { inject, injectable, named } from 'inversify';
import { createWebSocketConnection } from 'vscode-ws-jsonrpc/lib/socket/connection';
import { ContributionProvider } from '../../common/contribution-provider';
import { WebSocketChannel } from '../../common/messaging/web-socket-channel';
import { ConsoleLogger } from '../../node/messaging/logger';
import { ElectronMainConnectionHandler, ElectronBackendMessage, ElectronBackendConnectionPipe } from '../../electron-common/messaging/electron-backend-connection-handler';
import { MessagingContribution } from '../../node/messaging/messaging-contribution';
import { ChildProcess } from 'child_process';

export interface ElectronMainConnectionOptions {
}

/**
* This component replicates the role filled by `MessagingContribution` but for connecting the backend with the electron process.
* It is based on process communication using the created backend process.
* Alternatively it uses a simple message pipe object when running in `--no-cluster` mode.
*
* This component allows communication between server process (backend) and electron main process.
*/
@injectable()
export class ElectronMainMessagingService {

@inject(ContributionProvider) @named(ElectronMainConnectionHandler)
protected readonly connectionHandlers: ContributionProvider<ElectronMainConnectionHandler>;

protected readonly channelHandlers = new MessagingContribution.ConnectionHandlers<WebSocketChannel>();
protected readonly channels = new Map<number, WebSocketChannel>();
protected backendProcess?: ChildProcess;

start(backendProcess?: ChildProcess): void {
if (backendProcess) {
this.backendProcess = backendProcess;
this.backendProcess.on('message', message => {
if (ElectronBackendMessage.is(message)) {
this.handleMessage(ElectronBackendMessage.get(message));
}
});
this.backendProcess.on('exit', () => {
this.closeChannels();
});
} else {
ElectronBackendConnectionPipe.onMessage('electron', message => {
this.handleMessage(message);
});
}
for (const connectionHandler of this.connectionHandlers.getContributions()) {
this.channelHandlers.push(connectionHandler.path, (params, channel) => {
const connection = createWebSocketConnection(channel, new ConsoleLogger());
connectionHandler.onConnection(connection);
});
}
}

protected closeChannels(): void {
for (const channel of Array.from(this.channels.values())) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for (const channel of Array.from(this.channels.values())) {
for (const channel of this.channels.values()) {

This should already be iterable.

channel.close(undefined, 'Backend exited');
}
this.channels.clear();
}

protected handleMessage(data: string): void {
try {
// Start parsing the message to extract the channel id and route
const message: WebSocketChannel.Message = JSON.parse(data.toString());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One point of the discussion re: performance in our communication layer is that conversions back and forth from string to JSON are relatively expensive. Will all messages be passing through here, or only some, or how will it fit into the refactor of the messaging layer currently underway? Is it expected that this will be one terminus of the messaging system that can delegate the parsed messages to handlers, or will further parsing be necessary?

// Someone wants to open a logical channel
if (message.kind === 'open') {
const { id, path } = message;
const channel = this.createChannel(id);
if (this.channelHandlers.route(path, channel)) {
channel.ready();
this.channels.set(id, channel);
channel.onClose(() => this.channels.delete(id));
} else {
console.error('Cannot find a service for the path: ' + path);
}
} else {
const { id } = message;
const channel = this.channels.get(id);
if (channel) {
channel.handleMessage(message);
} else {
console.error('The ipc channel does not exist', id);
}
}
} catch (error) {
console.error('IPC: Failed to handle message', { error, data });
}
}

protected createChannel(id: number): WebSocketChannel {
return new WebSocketChannel(id, content => {
if (this.backendProcess) {
if (this.backendProcess.send) {
this.backendProcess.send(ElectronBackendMessage.create(content));
}
} else {
ElectronBackendConnectionPipe.pushMessage('backend', content);
}
});
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/********************************************************************************
* Copyright (C) 2022 TypeFox 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 { ContainerModule } from 'inversify';
import { TestConnection, TEST_CONNECTION_PATH } from '../../electron-common/electron-test-connection';
import { ElectronBackendConnectionProvider } from './electron-backend-connection-provider';

export default new ContainerModule(bind => {
bind(ElectronBackendConnectionProvider).toSelf().inSingletonScope();
bind(TestConnection).toDynamicValue(context =>
ElectronBackendConnectionProvider.createProxy(context.container, TEST_CONNECTION_PATH,
() => context.container.get(TestConnection))
).inSingletonScope();
});
Loading