From 5452fdb32d519a94610bec0e0844400ff449e710 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 6 Aug 2020 22:49:18 +0300 Subject: [PATCH] ipc-refactoring, fixes Signed-off-by: Roman Signed-off-by: Lauri Nevala --- package.json | 2 +- src/common/ipc.ts | 170 +++++++++++------- .../cluster-manager/cluster-status.tsx | 2 +- .../cluster-manager/cluster-view.tsx | 2 +- yarn.lock | 8 +- 5 files changed, 112 insertions(+), 72 deletions(-) diff --git a/package.json b/package.json index 9983c3eb66..03355ee6d9 100644 --- a/package.json +++ b/package.json @@ -265,7 +265,7 @@ "css-element-queries": "^1.2.3", "css-loader": "^3.5.3", "dompurify": "^2.0.11", - "electron": "^9.1.0", + "electron": "^9.1.2", "electron-builder": "^22.7.0", "electron-notarize": "^0.3.0", "electron-rebuild": "^1.11.0", diff --git a/src/common/ipc.ts b/src/common/ipc.ts index 95e982f56c..ef6a18e1c4 100644 --- a/src/common/ipc.ts +++ b/src/common/ipc.ts @@ -2,20 +2,118 @@ // https://www.electronjs.org/docs/api/ipc-main // https://www.electronjs.org/docs/api/ipc-renderer -import { ipcMain, ipcRenderer, WebContents, webContents } from "electron" +import { ipcMain, ipcRenderer, IpcRendererEvent, WebContents, webContents } from "electron" import logger from "../main/logger"; +import { getRandId } from "./utils"; export type IpcChannel = string; -export interface IpcHandleOpts { - timeout?: number; +export enum IpcMode { + SYNC = "sync", + ASYNC = "async", } -export interface IpcMessageHandler { - (...args: T): any; +export interface IpcChannelRequest { + msgId: string; + args: A; } -export interface IpcMessageOpts { +export interface IpcChannelResponse { + msgId: string; + data?: T; + error?: E; +} + +export interface IpcChannelInit { + channel: IpcChannel; // main <-> renderer communication channel name + mode?: IpcMode; // default: "async", use "sync" as last resort: https://www.electronjs.org/docs/api/ipc-renderer#ipcrenderersendsyncchannel-args + handle?: (...args: any[]) => any; // main-process message handler + autoBind?: boolean; // auto-bind message handler in main-process, default: false + timeout?: number; // timeout for waiting response from the sender + once?: boolean; // todo: add support +} + +export function createIpcChannel({ autoBind = false, mode = IpcMode.ASYNC, timeout = 0, handle, channel }: IpcChannelInit) { + channel = `${mode}:${channel}` + + const ipcChannel = { + channel: channel, + handleInMain: () => { + logger.info(`[IPC]: setup channel "${channel}"`); + + ipcMain.on(channel, async (event, req: IpcChannelRequest) => { + let resolved = false; + let timerId: any; + + function resolve(res: Partial) { + if (resolved) return; + res.msgId = req.msgId; // return back to sender to be able to handle response + resolved = true + logger.info(`[IPC]: sending response to "${channel}"`, res); + if (mode === IpcMode.ASYNC) { + event.reply(channel, res); + } + if (mode === IpcMode.SYNC) { + event.returnValue = res; + } + } + + if (timeout > 0) { + timerId = setTimeout(() => { + const timeoutError = new Error(`[IPC]: response timeout in ${timeout}ms`); + resolve({ error: timeoutError }) + }, timeout); + } + + try { + const data = await handle(...req.args); // todo: maybe exec in separate thread/worker + resolve({ data }) + } catch (error) { + resolve({ + error: String(error) + }) + } finally { + clearTimeout(timerId); + } + }) + }, + invokeFromRenderer: async (...args: any[]) => { + const req: IpcChannelRequest = { + msgId: getRandId({ prefix: "ipc-msg-id" }), + args: args, + } + logger.info(`[IPC]: "${channel}" sending message to main`, req); + if (mode === IpcMode.ASYNC) { + ipcRenderer.send(channel, req) + } + if (mode === IpcMode.SYNC) { + ipcRenderer.sendSync(channel, req) + } + return new Promise(async (resolve, reject) => { + ipcRenderer.on(channel, function waitResponseHandler(event: IpcRendererEvent, res: IpcChannelResponse) { + if (req.msgId === res.msgId) { + const meta = { ...req, ...res }; + if (res.data) { + logger.info(`[IPC]: "${channel}" resolve`, meta); + resolve(res.data); + } + if (res.error) { + logger.error(`[IPC]: "${channel}" reject`, meta); + reject(res.error); + } + ipcRenderer.off(channel, waitResponseHandler); // unsubscribe since handled + } + }); + }) + }, + } + if (autoBind && ipcMain) { + ipcChannel.handleInMain(); + } + return ipcChannel; +} + +export interface IpcBroadcastParams { channel: IpcChannel webContentId?: number; // sends to single webContents view filter?: (webContent: WebContents) => boolean @@ -23,7 +121,7 @@ export interface IpcMessageOpts { args?: A; } -export function broadcastIpc({ channel, webContentId, filter, args = [] }: IpcMessageOpts) { +export function broadcastIpc({ channel, webContentId, filter, args = [] }: IpcBroadcastParams) { const singleView = webContentId ? webContents.fromId(webContentId) : null; let views = singleView ? [singleView] : webContents.getAllWebContents(); if (filter) { @@ -35,61 +133,3 @@ export function broadcastIpc({ channel, webContentId, filter, args = [] }: IpcMe webContent.send(channel, ...[args].flat()); }) } - -// todo: support timeout + merge with sendMessage? -export async function invokeIpc(channel: IpcChannel, ...args: any[]): Promise { - logger.info(`[IPC]: invoke channel "${channel}"`, { args }); - return ipcRenderer.invoke(channel, ...args); -} - -// todo: make isomorphic api -export function handleIpc(channel: IpcChannel, handler: IpcMessageHandler, options: IpcHandleOpts = {}) { - const { timeout = 0 } = options; - logger.info(`[IPC]: setup to handle "${channel}"`); - - ipcMain.handle(channel, async (event, ...args) => { - logger.info(`[IPC]: handle "${channel}"`, { args }); - return new Promise(async (resolve, reject) => { - let timerId; - if (timeout) { - timerId = setTimeout(() => { - const timeoutError = new Error("[IPC]: response timeout"); - reject(timeoutError); - }, timeout); - } - try { - const result = await handler(...args); // todo: maybe exec in separate thread/worker - resolve(result); - clearTimeout(timerId); - } catch (err) { - reject(err); - } - }) - }) -} - -export interface IpcPairOptions { - channel: IpcChannel - handle?: IpcMessageHandler - autoBind?: boolean; - timeout?: number; -} - -// todo: improve api -export function createIpcChannel({ channel, autoBind, ...initOpts }: IpcPairOptions) { - const bindHandler = (opts: { handler?: IpcMessageHandler, options?: IpcHandleOpts } = {}) => { - const handler = opts.handler || initOpts.handle || Function; - const options = opts.options || { timeout: initOpts.timeout }; - handleIpc(channel, handler, options); - }; - if (autoBind) { - bindHandler(); - } - return { - channel: channel, - handleInMain: bindHandler, - invokeFromRenderer(...args: any[]) { - return invokeIpc(channel, ...args); - }, - } -} diff --git a/src/renderer/components/cluster-manager/cluster-status.tsx b/src/renderer/components/cluster-manager/cluster-status.tsx index 270a670896..e2fb6c17e8 100644 --- a/src/renderer/components/cluster-manager/cluster-status.tsx +++ b/src/renderer/components/cluster-manager/cluster-status.tsx @@ -52,7 +52,7 @@ export class ClusterStatus extends React.Component { } async refreshClusterState() { - return clusterIpc.activate.invokeFromRenderer(this.clusterId); + await clusterIpc.activate.invokeFromRenderer(this.clusterId); } reconnect = async () => { diff --git a/src/renderer/components/cluster-manager/cluster-view.tsx b/src/renderer/components/cluster-manager/cluster-view.tsx index df4981055b..5dcd6b1a36 100644 --- a/src/renderer/components/cluster-manager/cluster-view.tsx +++ b/src/renderer/components/cluster-manager/cluster-view.tsx @@ -38,7 +38,7 @@ export class ClusterView extends React.Component { view.setAttribute("enableremotemodule", "true") view.addEventListener("did-finish-load", () => { console.log('CLUSTER VIEW READY!', cluster) - view.openDevTools() + // view.openDevTools() }); view.addEventListener("did-fail-load", event => { // todo: handle diff --git a/yarn.lock b/yarn.lock index fc614075ec..384e85c19b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4647,10 +4647,10 @@ electron@*: "@types/node" "^12.0.12" extract-zip "^1.0.3" -electron@^9.1.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/electron/-/electron-9.1.0.tgz#ca77600c9e4cd591298c340e013384114d3d8d05" - integrity sha512-VRAF8KX1m0py9I9sf0kw1kWfeC87mlscfFcbcRdLBsNJ44/GrJhi3+E8rKbpHUeZNQxsPaVA5Zu5Lxb6dV/scQ== +electron@^9.1.2: + version "9.1.2" + resolved "https://registry.yarnpkg.com/electron/-/electron-9.1.2.tgz#bfa26d6b192ea13abb6f1461371fd731a8358988" + integrity sha512-xEYadr3XqIqJ4ktBPo0lhzPdovv4jLCpiUUGc2M1frUhFhwqXokwhPaTUcE+zfu5+uf/ONDnQApwjzznBsRrgQ== dependencies: "@electron/get" "^1.0.1" "@types/node" "^12.0.12"