diff --git a/docs/extensions/guides/ipc.md b/docs/extensions/guides/ipc.md index e0014768ea..06c6ac9d49 100644 --- a/docs/extensions/guides/ipc.md +++ b/docs/extensions/guides/ipc.md @@ -1,6 +1,6 @@ # Inter Process Communication -A Lens Extension can utilise IPC to send information between its `LensRendererExtension` and its `LensMainExtension`. +A Lens Extension can utilize IPC to send information between its `LensRendererExtension` and its `LensMainExtension`. 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. @@ -29,7 +29,7 @@ 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 a unidirectional form of communication. -Only `LensRendererExtension` can initiate this kind of request, and only `LensMainExtension` and respond this this kind of request. +Only `LensRendererExtension` can initiate this kind of request, and only `LensMainExtension` can and respond this this kind of request. ## Registering IPC Handlers and Listeners @@ -40,38 +40,46 @@ The general terminology is as follows: To register either a handler or a listener, you should do something like the following: +File1: ```typescript import { LensMainExtension, Interface, Types } from "@k8slens/extensions"; export class ExampleExtensionMain extends LensMainExtension { onActivate() { - this.disposers.push( - this.listenIpc({ - channel: "initialize", - listener: this.initializeListener, - verifier: this.initializeVerifier, - }) - ); + this.listenIpc({ + channel: "initialize", + listener: this.initializeListener, + verifier: this.initializeVerifier, + }); } - initializeListener = (event: Types.IpcMainEvent, uid: string) => { - console.log(`starting to initialize: ${uid}`); + initializeListener = (event: Types.IpcMainEvent, id: string) => { + console.log(`starting to initialize: ${id}`); }; - initializeVerifier = (args: unknown[]): args is [uid: string] => { + initializeVerifier = (args: unknown[]): args is [id: string] => { return args.length === 1 && typeof args[0] === "string"; } } ``` +File2: +```typescript +import { LensMainExtension, Interface, Types } from "@k8slens/extensions"; + +export class ExampleExtensionRenderer extends LensRendererExtension { + onActivate() { + setTimeout(() => this.sendIpc("initialize", "an-id"), 5000); + } +} +``` + +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`. +There is no behind the scenes transfer of these functions. + If you want to register a "handler" you would call `this.handleIpc(...)` instead. - -The `LensExtension.prototype.disopsers` is a list of `() => void`'s (or `Utils.Disposer`'s). -Those functions are sort of like destructors in that they should clean up some state. -Some functions return them to indicate that it will clean up the state which was just registered. - -As an extension developer you do not need to run them yourself. -Lens will run them when your extension gets deactivated or uninstalled. +The cleanup of these handlers is handled by Lens itself. ### Note about verification @@ -84,6 +92,11 @@ Your handler or listener will not be called if it fails this validation. Instead an error or log message will occur. This should help with debugging because you are notified immediately that there is a mismatch between what you are expecting and what was sent. +### Allowed Values + +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 Calling IPC is very simple. diff --git a/src/common/ipc/ipc.ts b/src/common/ipc/ipc.ts index 74305e0484..66e591e765 100644 --- a/src/common/ipc/ipc.ts +++ b/src/common/ipc/ipc.ts @@ -42,35 +42,33 @@ function getSubFrames(): ClusterFrameInfo[] { return toJS(Array.from(clusterFrameMap.values()), { recurseEverything: true }); } -export async function broadcastMessage(channel: string, ...args: any[]) { +export function broadcastMessage(channel: string, ...args: any[]) { const views = (webContents || remote?.webContents)?.getAllWebContents(); if (!views) return; - if (ipcRenderer) { - ipcRenderer.send(channel, ...args); - } else if (ipcMain) { - ipcMain.emit(channel, ...args); - } + ipcRenderer?.send(channel, ...args); + ipcMain?.emit(channel, ...args); - for (const view of views) { - const type = view.getType(); + const subFramesP = ipcRenderer + ? requestMain(subFramesChannel) + : Promise.resolve(getSubFrames()); - logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${view.id}`, { args }); - view.send(channel, ...args); + subFramesP + .then(subFrames => { + for (const view of views) { + try { + logger.silly(`[IPC]: broadcasting "${channel}" to ${view.getType()}=${view.id}`, { args }); + view.send(channel, ...args); - try { - const subFrames: ClusterFrameInfo[] = ipcRenderer - ? await requestMain(subFramesChannel) - : getSubFrames(); - - for (const frameInfo of subFrames) { - view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args); + for (const frameInfo of subFrames) { + view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args); + } + } catch (error) { + logger.error("[IPC]: failed to send IPC message", { error: String(error) }); + } } - } catch (error) { - logger.error("[IPC]: failed to send IPC message", { error: String(error) }); - } - } + }); } export function subscribeToBroadcast(channel: string, listener: (...args: any[]) => any) { diff --git a/src/extensions/lens-extension.ts b/src/extensions/lens-extension.ts index 2b5b4ecf05..b518059e7c 100644 --- a/src/extensions/lens-extension.ts +++ b/src/extensions/lens-extension.ts @@ -52,7 +52,7 @@ export class LensExtension { protocolHandlers: ProtocolHandlerRegistration[] = []; @observable private isEnabled = false; - disposers = disposer(); + protected disposers = disposer(); constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension) { this.id = id; diff --git a/src/extensions/lens-main-extension.ts b/src/extensions/lens-main-extension.ts index e5decc8a04..282c54fc1e 100644 --- a/src/extensions/lens-main-extension.ts +++ b/src/extensions/lens-main-extension.ts @@ -25,7 +25,6 @@ import { getExtensionPageUrl } from "./registries/page-registry"; import { CatalogEntity, catalogEntityRegistry } from "../common/catalog"; import { IObservableArray } from "mobx"; import { IpcHandlerRegistration, MenuRegistration } from "./registries"; -import type { Disposer } from "../common/utils"; import { handleCorrect, ListenerEvent, ListVerifier, onCorrect, Rest } from "../common/ipc"; import { ipcMain, IpcMain } from "electron"; @@ -53,12 +52,14 @@ export class LensMainExtension extends LensExtension { handleIpc< Handler extends (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any, - >({ channel, ...reg }: IpcHandlerRegistration): Disposer { + >({ channel, ...reg }: IpcHandlerRegistration): void { // This is to have a uniform length prefix so that two extensions cannot talk to each other's IPCs accidentally - return handleCorrect({ - channel: `extensions@${this[IpcPrefix]}:${channel}`, - ...reg, - }); + this.disposers.push( + handleCorrect({ + channel: `extensions@${this[IpcPrefix]}:${channel}`, + ...reg, + }) + ); } listenIpc< @@ -70,11 +71,13 @@ export class LensMainExtension extends LensExtension { channel: string, listener: Listener, verifier: ListVerifier>>, - }): Disposer { - return onCorrect({ - source: ipcMain, - channel: `extensions@${this[IpcPrefix]}:${channel}`, - ...reg, - }); + }): void { + this.disposers.push( + onCorrect({ + source: ipcMain, + channel: `extensions@${this[IpcPrefix]}:${channel}`, + ...reg, + }) + ); } } diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts index 0a90ed63a4..5557e492ee 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -30,7 +30,6 @@ import { CommandRegistration } from "./registries/command-registry"; import { EntitySettingRegistration } from "./registries/entity-setting-registry"; import { ipcRenderer, IpcRenderer } from "electron"; import { ListenerEvent, ListVerifier, onCorrect, Rest } from "../common/ipc"; -import { Disposer } from "../common/utils"; export class LensRendererExtension extends LensExtension { globalPages: PageRegistration[] = []; @@ -73,12 +72,14 @@ export class LensRendererExtension extends LensExtension { channel: string, listener: Listener, verifier: ListVerifier>>, - }): Disposer { - return onCorrect({ - source: ipcRenderer, - channel: `extensions@${this[IpcPrefix]}:${channel}`, - ...reg, - }); + }): void { + this.disposers.push( + onCorrect({ + source: ipcRenderer, + channel: `extensions@${this[IpcPrefix]}:${channel}`, + ...reg, + }) + ); } invokeIpc(channel: string, ...args: any[]): Promise {