From 57c87a2e71cbd4b91ef4c3f73e713c46d6d58ecc Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 26 May 2021 17:10:39 -0400 Subject: [PATCH] Change where extension IPC is exported (#2845) - Fix documentation and guide --- docs/extensions/guides/ipc.md | 63 +++++++++--------- src/extensions/core-api/index.ts | 2 + .../ipc.ts} | 25 +------ src/extensions/core-api/stores.ts | 2 - .../{main-ipc-store.ts => ipc/ipc-main.ts} | 42 ++++++++---- .../{ipc-store.ts => ipc/ipc-registrar.ts} | 15 +++-- src/extensions/ipc/ipc-renderer.ts | 65 +++++++++++++++++++ 7 files changed, 140 insertions(+), 74 deletions(-) rename src/extensions/{renderer-ipc-store.ts => core-api/ipc.ts} (54%) rename src/extensions/{main-ipc-store.ts => ipc/ipc-main.ts} (55%) rename src/extensions/{ipc-store.ts => ipc/ipc-registrar.ts} (77%) create mode 100644 src/extensions/ipc/ipc-renderer.ts diff --git a/docs/extensions/guides/ipc.md b/docs/extensions/guides/ipc.md index 91d811bdf0..b6ec38eacd 100644 --- a/docs/extensions/guides/ipc.md +++ b/docs/extensions/guides/ipc.md @@ -1,35 +1,36 @@ # Inter Process Communication -A Lens Extension can utilize IPC to send information between its `LensRendererExtension` and its `LensMainExtension`. +A Lens Extension can utilize IPC to send information between the `renderer` and `main` processes. This is useful when wanting to communicate directly within your extension. + For example, if a user logs into a service that your extension is a facade for and `main` needs to know some information so that you can start syncing items to the `Catalog`, this would be a good way to send that information along. -IPC channels are blocked off per extension. +IPC channels are sectioned off per extension. Meaning that each extension can only communicate with itself. -## Types of IPC +## Types of Communication -There are two flavours of IPC that are provided: +There are two flavours of communication that are provided: -- Event based -- Request based +- Event based (IPC) +- Request based (RPC) -### Event Based IPC +### Event Based or IPC This is the same as an [Event Emitter](https://nodejs.org/api/events.html#events_class_eventemitter) but is not limited to just one Javascript process. This is a good option when you need to report that something has happened but you don't need a response. This is a fully two-way form of communication. -Both `LensMainExtension` and `LensRendererExtension` can do this sort of IPC. +Both `main` and `renderer` can do this sort of IPC. -### Request Based IPC +### Request Based or RPC -This is more like a Remote Procedure Call (RPC). -With this sort of IPC the caller waits for the result from the other side. -This is accomplished by returning a `Promise` which needs to be `await`-ed. +This is more like a Remote Procedure Call (RPC) or Send-Receive-Reply (SRR). +With this sort of communication the caller needs to wait for the result from the other side. +This is accomplished by `await`-ing the returned `Promise`. This is a unidirectional form of communication. -Only `LensRendererExtension` can initiate this kind of request, and only `LensMainExtension` can and respond this this kind of request. +Only `renderer` can initiate this kind of request, and only `main` can and respond to this kind of request. ## Registering IPC Handlers and Listeners @@ -42,8 +43,8 @@ To register either a handler or a listener, you should do something like the fol `main.ts`: ```typescript -import { LensMainExtension, Interface, Types, Store } from "@k8slens/extensions"; -import { registerListeners, IpcMain } from "./helpers/main"; +import { LensMainExtension } from "@k8slens/extensions"; +import { IpcMain } from "./helpers/main"; export class ExampleExtensionMain extends LensMainExtension { onActivate() { @@ -59,13 +60,13 @@ Lens will automatically clean up that store and all the handlers on deactivation `helpers/main.ts`: ```typescript -import { Store } from "@k8slens/extensions"; +import { Ipc, Types } from "@k8slens/extensions"; -export class IpcMain extends Store.MainIpcStore { +export class IpcMain extends Ipc.Main { constructor(extension: LensMainExtension) { super(extension); - this.listenIpc("initialize", onInitialize); + this.listen("initialize", onInitialize); } } @@ -75,20 +76,20 @@ function onInitialize(event: Types.IpcMainEvent, id: string) { ``` In other files, it is not necessary to pass around any instances. -It should be able to just call `getInstance()` everywhere in your extension as needed. +You should be able to just call `IpcMain.getInstance()` anywhere it is needed in your extension. --- `renderer.ts`: ```typescript -import { LensRendererExtension, Interface, Types } from "@k8slens/extensions"; +import { LensRendererExtension } from "@k8slens/extensions"; import { IpcRenderer } from "./helpers/renderer"; export class ExampleExtensionRenderer extends LensRendererExtension { onActivate() { const ipc = IpcRenderer.createInstance(this); - setTimeout(() => ipc.broadcastIpc("initialize", "an-id"), 5000); + setTimeout(() => ipc.broadcast("initialize", "an-id"), 5000); } } ``` @@ -99,9 +100,9 @@ It is also needed to create an instance to broadcast messages too. `helpers/renderer.ts`: ```typescript -import { Store } from "@k8slens/extensions"; +import { Ipc } from "@k8slens/extensions"; -export class IpcMain extends Store.RendererIpcStore {} +export class IpcRenderer extends Ipc.Renderer {} ``` It is necessary to create child classes of these `abstract class`'s in your extension before you can use them. @@ -109,13 +110,16 @@ It is necessary to create child classes of these `abstract class`'s in your exte --- As this example shows: the channel names *must* be the same. -It should also be noted that "listeners" and "handlers" are specific to either `LensRendererExtension` and `LensMainExtension`. +It should also be noted that "listeners" and "handlers" are specific to either `renderer` or `main`. There is no behind the scenes transfer of these functions. -If you want to register a "handler" you would call `Store.MainIpcStore.handleIpc(...)` instead. +To register a "handler" call `IpcMain.getInstance().handle(...)`. The cleanup of these handlers is handled by Lens itself. -`Store.RendererIpcStore.broadcastIpc(...)` and `Store.MainIpcStore.broadcastIpc(...)` sends an event to all renderer frames and to main. +The `listen()` methods on `Ipc.Main` and `Ipc.Renderer` return a `Disposer`, or more specifically, a `() => void`. +This can be optionally called to remove the listener early. + +Calling either `IpcRenderer.getInstance().broadcast(...)` or `IpcMain.getInstance().broadcast(...)` sends an event to all `renderer` frames and to `main`. Because of this, no matter where you broadcast from, all listeners in `main` and `renderer` will be notified. ### Allowed Values @@ -123,9 +127,6 @@ Because of this, no matter where you broadcast from, all listeners in `main` and This IPC mechanism utilizes the [Structured Clone Algorithm](developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) for serialization. This means that more types than what are JSON serializable can be used, but not all the information will be passed through. -## Using IPC +## Using Request Based Communication -Calling IPC is very simple. -If you are meaning to do an event based call, merely call `broadcastIpc(, ...)` from within your extension. - -If you are meaning to do a request based call from `renderer`, you should do `const res = await Store.RendererIpcStore.invokeIpc(, ...));` instead. +If you are meaning to do a request based call from `renderer`, you should do `const res = await IpcRenderer.getInstance().invoke(, ...));` instead. diff --git a/src/extensions/core-api/index.ts b/src/extensions/core-api/index.ts index 56e4076ddb..249655394d 100644 --- a/src/extensions/core-api/index.ts +++ b/src/extensions/core-api/index.ts @@ -31,6 +31,7 @@ import * as Util from "./utils"; import * as Interface from "../interfaces"; import * as Catalog from "./catalog"; import * as Types from "./types"; +import * as Ipc from "./ipc"; export { App, @@ -40,4 +41,5 @@ export { Store, Types, Util, + Ipc, }; diff --git a/src/extensions/renderer-ipc-store.ts b/src/extensions/core-api/ipc.ts similarity index 54% rename from src/extensions/renderer-ipc-store.ts rename to src/extensions/core-api/ipc.ts index 75824fece7..7654fa7f27 100644 --- a/src/extensions/renderer-ipc-store.ts +++ b/src/extensions/core-api/ipc.ts @@ -18,27 +18,6 @@ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { ipcRenderer } from "electron"; -import { IpcPrefix, IpcStore } from "./ipc-store"; -import { Disposers } from "./lens-extension"; -import type { LensRendererExtension } from "./lens-renderer-extension"; -export abstract class RendererIpcStore extends IpcStore { - constructor(extension: LensRendererExtension) { - super(extension); - extension[Disposers].push(() => RendererIpcStore.resetInstance()); - } - - listenIpc(channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => any): void { - const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`; - - ipcRenderer.addListener(prefixedChannel, listener); - this.extension[Disposers].push(() => ipcRenderer.removeListener(prefixedChannel, listener)); - } - - invokeIpc(channel: string, ...args: any[]): Promise { - const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`; - - return ipcRenderer.invoke(prefixedChannel, ...args); - } -} +export { IpcMain as Main } from "../ipc/ipc-main"; +export { IpcRegistrar as Registrar } from "../ipc/ipc-registrar"; diff --git a/src/extensions/core-api/stores.ts b/src/extensions/core-api/stores.ts index a9dfe9bf1e..17eac539d6 100644 --- a/src/extensions/core-api/stores.ts +++ b/src/extensions/core-api/stores.ts @@ -20,5 +20,3 @@ */ export { ExtensionStore } from "../extension-store"; -export { MainIpcStore } from "../main-ipc-store"; -export { RendererIpcStore } from "../renderer-ipc-store"; diff --git a/src/extensions/main-ipc-store.ts b/src/extensions/ipc/ipc-main.ts similarity index 55% rename from src/extensions/main-ipc-store.ts rename to src/extensions/ipc/ipc-main.ts index 664fc0d965..094ed48a1c 100644 --- a/src/extensions/main-ipc-store.ts +++ b/src/extensions/ipc/ipc-main.ts @@ -19,27 +19,43 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { ipcMain } from "electron"; -import { IpcPrefix, IpcStore } from "./ipc-store"; -import { Disposers } from "./lens-extension"; -import type { LensMainExtension } from "./lens-main-extension"; +import { IpcPrefix, IpcRegistrar } from "./ipc-registrar"; +import { Disposers } from "../lens-extension"; +import type { LensMainExtension } from "../lens-main-extension"; +import type { Disposer } from "../../common/utils"; +import { once } from "lodash"; -export abstract class MainIpcStore extends IpcStore { +export abstract class IpcMain extends IpcRegistrar { constructor(extension: LensMainExtension) { super(extension); - extension[Disposers].push(() => MainIpcStore.resetInstance()); + extension[Disposers].push(() => IpcMain.resetInstance()); } - handleIpc(channel: string, handler: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any): void { + /** + * Listen for broadcasts within your extension + * @param channel The channel to listen for broadcasts on + * @param listener The function that will be called with the arguments of the broadcast + * @returns An optional disopser, Lens will cleanup when the extension is disabled or uninstalled even if this is not called + */ + listen(channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => any): Disposer { + const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`; + const cleanup = once(() => ipcMain.removeListener(prefixedChannel, listener)); + + ipcMain.addListener(prefixedChannel, listener); + this.extension[Disposers].push(cleanup); + + return cleanup; + } + + /** + * Declare a RPC over `channel`. Lens will cleanup when the extension is disabled or uninstalled + * @param channel The name of the RPC + * @param handler The remote procedure that is called + */ + handle(channel: string, handler: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any): void { const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`; ipcMain.handle(prefixedChannel, handler); this.extension[Disposers].push(() => ipcMain.removeHandler(prefixedChannel)); } - - listenIpc(channel: string, listener: (event: Electron.IpcMainEvent, ...args: any[]) => any): void { - const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`; - - ipcMain.addListener(prefixedChannel, listener); - this.extension[Disposers].push(() => ipcMain.removeListener(prefixedChannel, listener)); - } } diff --git a/src/extensions/ipc-store.ts b/src/extensions/ipc/ipc-registrar.ts similarity index 77% rename from src/extensions/ipc-store.ts rename to src/extensions/ipc/ipc-registrar.ts index 275522338c..84d713ace7 100644 --- a/src/extensions/ipc-store.ts +++ b/src/extensions/ipc/ipc-registrar.ts @@ -18,14 +18,14 @@ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { Singleton } from "../common/utils"; -import type { LensExtension } from "./lens-extension"; +import { Singleton } from "../../common/utils"; +import type { LensExtension } from "../lens-extension"; import { createHash } from "crypto"; -import { broadcastMessage } from "../common/ipc"; +import { broadcastMessage } from "../../common/ipc"; export const IpcPrefix = Symbol(); -export abstract class IpcStore extends Singleton { +export abstract class IpcRegistrar extends Singleton { readonly [IpcPrefix]: string; constructor(protected extension: LensExtension) { @@ -33,7 +33,12 @@ export abstract class IpcStore extends Singleton { this[IpcPrefix] = createHash("sha256").update(extension.id).digest("hex"); } - broadcastIpc(channel: string, ...args: any[]): void { + /** + * + * @param channel The channel to broadcast to your whole extension, both `main` and `renderer` + * @param args The arguments passed to all listeners + */ + broadcast(channel: string, ...args: any[]): void { broadcastMessage(`extensions@${this[IpcPrefix]}:${channel}`, ...args); } } diff --git a/src/extensions/ipc/ipc-renderer.ts b/src/extensions/ipc/ipc-renderer.ts new file mode 100644 index 0000000000..59a42f85a0 --- /dev/null +++ b/src/extensions/ipc/ipc-renderer.ts @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { ipcRenderer } from "electron"; +import { IpcPrefix, IpcRegistrar } from "./ipc-registrar"; +import { Disposers } from "../lens-extension"; +import type { LensRendererExtension } from "../lens-renderer-extension"; +import type { Disposer } from "../../common/utils"; +import { once } from "lodash"; + +export abstract class IpcRenderer extends IpcRegistrar { + constructor(extension: LensRendererExtension) { + super(extension); + extension[Disposers].push(() => IpcRenderer.resetInstance()); + } + + /** + * Listen for broadcasts within your extension. + * If the lifetime of the listener should be tied to the mounted lifetime of + * a component then putting the returned value in a `disposeOnUnmount` call will suffice. + * @param channel The channel to listen for broadcasts on + * @param listener The function that will be called with the arguments of the broadcast + * @returns An optional disopser, Lens will cleanup even if this is not called + */ + listen(channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => any): Disposer { + const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`; + const cleanup = once(() => ipcRenderer.removeListener(prefixedChannel, listener)); + + ipcRenderer.addListener(prefixedChannel, listener); + this.extension[Disposers].push(cleanup); + + return cleanup; + } + + /** + * Request main to execute its function over the `channel` channel. + * This function only interacts with functions registered via `Ipc.IpcMain.handleRpc` + * An error will be thrown if no function has been registered on `main` with this channel ID. + * @param channel The channel to invoke a RPC on + * @param args The arguments to pass to the RPC + * @returns A promise of the resulting value + */ + invoke(channel: string, ...args: any[]): Promise { + const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`; + + return ipcRenderer.invoke(prefixedChannel, ...args); + } +}