diff --git a/docs/extensions/guides/ipc.md b/docs/extensions/guides/ipc.md index 06c6ac9d49..cc78282179 100644 --- a/docs/extensions/guides/ipc.md +++ b/docs/extensions/guides/ipc.md @@ -40,57 +40,69 @@ The general terminology is as follows: To register either a handler or a listener, you should do something like the following: -File1: +`main/extension.ts`: ```typescript -import { LensMainExtension, Interface, Types } from "@k8slens/extensions"; +import { LensMainExtension, Interface, Types, Store } from "@k8slens/extensions"; +import { registerListeners } from "./helper-file"; export class ExampleExtensionMain extends LensMainExtension { onActivate() { - this.listenIpc({ - channel: "initialize", - listener: this.initializeListener, - verifier: this.initializeVerifier, - }); - } + Store.MainIpcStore.createInstance(this); - initializeListener = (event: Types.IpcMainEvent, id: string) => { - console.log(`starting to initialize: ${id}`); - }; - - initializeVerifier = (args: unknown[]): args is [id: string] => { - return args.length === 1 && typeof args[0] === "string"; + registerListeners(); } } ``` -File2: +This file shows that you need to create an instance of the store to be able to use IPC. +Lens will automatically clean up that store and all the handlers on deactivation and uninstall. + +--- + +`main/helper-file.ts`: ```typescript -import { LensMainExtension, Interface, Types } from "@k8slens/extensions"; +import { Store } from "@k8slens/extensions"; + +function onInitialize(event: Types.IpcMainEvent, id: string) { + console.log(`starting to initialize: ${id}`); +} + +export function registerListeners() { + Store.MainIpcStore.getInstance().listenIpc("initialize", onInitialize); +} +``` + +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. + +--- + +`renderer/extension.ts`: +```typescript +import { LensRendererExtension, Interface, Types } from "@k8slens/extensions"; export class ExampleExtensionRenderer extends LensRendererExtension { onActivate() { - setTimeout(() => this.sendIpc("initialize", "an-id"), 5000); + const ipcStore = Store.RendererIpcStore.createInstance(this); + + setTimeout(() => ipcStore.broadcastIpc("initialize", "an-id"), 5000); } } ``` +It is also needed to create an instance to broadcast messages too. + +--- + 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. +If you want to register a "handler" you would call `Store.MainIpcStore.handleIpc(...)` instead. The cleanup of these handlers is handled by Lens itself. -### Note about verification - -We require that extension developers provide a verification function when registering a listener or handler. -This is done as a preventative measure to help separate issues that can happen at runtime. -While it is possible to use the unary truth function, this is highly discouraged. - -The verification function should do some cursory validation on the values send along the channel. -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. +`Store.RendererIpcStore.broadcastIpc(...)` and `Store.MainIpcStore.broadcastIpc(...)` sends an event to all renderer frames and to main. +Because of this, if you have the same channel name in both `main` and `renderer` both will receive the events. ### Allowed Values @@ -100,6 +112,6 @@ This means that more types than what are JSON serializable can be used, but not ## Using IPC Calling IPC is very simple. -If you are meaning to do an event based call, merely call `this.sendIpc(, ...)` from within your extension. +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 this.invokeIpc(, ...));` instead. +If you are meaning to do a request based call from `renderer`, you should do `const res = await Store.RendererIpcStore.invokeIpc(, ...));` instead. diff --git a/src/extensions/core-api/stores.ts b/src/extensions/core-api/stores.ts index 17eac539d6..5897ea73b9 100644 --- a/src/extensions/core-api/stores.ts +++ b/src/extensions/core-api/stores.ts @@ -20,3 +20,5 @@ */ export { ExtensionStore } from "../extension-store"; +export { MainIpcStore } from "../ipc-main-store"; +export { RendererIpcStore } from "../ipc-renderer-store"; diff --git a/src/extensions/ipc-main-store.ts b/src/extensions/ipc-main-store.ts new file mode 100644 index 0000000000..d78ce2b9ce --- /dev/null +++ b/src/extensions/ipc-main-store.ts @@ -0,0 +1,25 @@ +import { ipcMain } from "electron"; +import { IpcPrefix, IpcStore } from "./ipc-store"; +import { Disposers } from "./lens-extension"; +import { LensMainExtension } from "./lens-main-extension"; + +export class MainIpcStore extends IpcStore { + constructor(extension: LensMainExtension) { + super(extension); + extension[Disposers].push(() => MainIpcStore.resetInstance()); + } + + handleIpc(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-renderer-store.ts b/src/extensions/ipc-renderer-store.ts new file mode 100644 index 0000000000..5d329fc531 --- /dev/null +++ b/src/extensions/ipc-renderer-store.ts @@ -0,0 +1,24 @@ +import { ipcRenderer } from "electron"; +import { IpcPrefix, IpcStore } from "./ipc-store"; +import { Disposers } from "./lens-extension"; +import { LensRendererExtension } from "./lens-renderer-extension"; + +export 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); + } +} diff --git a/src/extensions/ipc-store.ts b/src/extensions/ipc-store.ts new file mode 100644 index 0000000000..c274c30549 --- /dev/null +++ b/src/extensions/ipc-store.ts @@ -0,0 +1,19 @@ +import { Singleton } from "../common/utils"; +import { LensExtension } from "./lens-extension"; +import { createHash } from "crypto"; +import { broadcastMessage } from "../common/ipc"; + +export const IpcPrefix = Symbol(); + +export abstract class IpcStore extends Singleton { + readonly [IpcPrefix]: string; + + constructor(protected extension: LensExtension) { + super(); + this[IpcPrefix] = createHash("sha256").update(extension.id).digest("hex"); + } + + broadcastIpc(channel: string, ...args: any[]): void { + broadcastMessage(`extensions@${this[IpcPrefix]}:${channel}`, ...args); + } +} diff --git a/src/extensions/lens-extension.ts b/src/extensions/lens-extension.ts index b518059e7c..d9df2c9993 100644 --- a/src/extensions/lens-extension.ts +++ b/src/extensions/lens-extension.ts @@ -25,8 +25,6 @@ import { FilesystemProvisionerStore } from "../main/extension-filesystem"; import logger from "../main/logger"; import { ProtocolHandlerRegistration } from "./registries"; import { disposer } from "../common/utils"; -import { createHash } from "crypto"; -import { broadcastMessage } from "../common/ipc"; export type LensExtensionId = string; // path to manifest (package.json) export type LensExtensionConstructor = new (...args: ConstructorParameters) => LensExtension; @@ -40,26 +38,24 @@ export interface LensExtensionManifest { lens?: object; // fixme: add more required fields for validation } -export const IpcPrefix = Symbol(); +export const Disposers = Symbol(); export class LensExtension { readonly id: LensExtensionId; readonly manifest: LensExtensionManifest; readonly manifestPath: string; readonly isBundled: boolean; - readonly [IpcPrefix]: string; protocolHandlers: ProtocolHandlerRegistration[] = []; @observable private isEnabled = false; - protected disposers = disposer(); + [Disposers] = disposer(); constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension) { this.id = id; this.manifest = manifest; this.manifestPath = manifestPath; this.isBundled = !!isBundled; - this[IpcPrefix] = createHash("sha256").update(this.id).digest("hex"); } get name() { @@ -98,7 +94,7 @@ export class LensExtension { if (!this.isEnabled) return; this.isEnabled = false; this.onDeactivate?.(); - this.disposers(); + this[Disposers](); logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`); } @@ -134,10 +130,6 @@ export class LensExtension { }; } - sendIpc(channel: string, ...args: any[]): void { - broadcastMessage(`extensions@${this[IpcPrefix]}:${channel}`, ...args); - } - protected onActivate(): void { return; } diff --git a/src/extensions/lens-main-extension.ts b/src/extensions/lens-main-extension.ts index 282c54fc1e..b02456c6d4 100644 --- a/src/extensions/lens-main-extension.ts +++ b/src/extensions/lens-main-extension.ts @@ -19,14 +19,12 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { IpcPrefix, LensExtension } from "./lens-extension"; +import { LensExtension } from "./lens-extension"; import { WindowManager } from "../main/window-manager"; import { getExtensionPageUrl } from "./registries/page-registry"; import { CatalogEntity, catalogEntityRegistry } from "../common/catalog"; import { IObservableArray } from "mobx"; -import { IpcHandlerRegistration, MenuRegistration } from "./registries"; -import { handleCorrect, ListenerEvent, ListVerifier, onCorrect, Rest } from "../common/ipc"; -import { ipcMain, IpcMain } from "electron"; +import { MenuRegistration } from "./registries"; export class LensMainExtension extends LensExtension { appMenus: MenuRegistration[] = []; @@ -49,35 +47,4 @@ export class LensMainExtension extends LensExtension { removeCatalogSource(id: string) { catalogEntityRegistry.removeSource(`${this.name}:${id}`); } - - handleIpc< - Handler extends (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any, - >({ channel, ...reg }: IpcHandlerRegistration): void { - // This is to have a uniform length prefix so that two extensions cannot talk to each other's IPCs accidentally - this.disposers.push( - handleCorrect({ - channel: `extensions@${this[IpcPrefix]}:${channel}`, - ...reg, - }) - ); - } - - listenIpc< - Listener extends (event: ListenerEvent, ...args: any[]) => any - >({ - channel, - ...reg - }: { - channel: string, - listener: Listener, - verifier: ListVerifier>>, - }): 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 5557e492ee..232040ac9f 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -24,12 +24,10 @@ import type { KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, WelcomeMenuRegistration, } from "./registries"; import type { Cluster } from "../main/cluster"; -import { IpcPrefix, LensExtension } from "./lens-extension"; +import { LensExtension } from "./lens-extension"; import { getExtensionPageUrl } from "./registries/page-registry"; 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"; export class LensRendererExtension extends LensExtension { globalPages: PageRegistration[] = []; @@ -62,27 +60,4 @@ export class LensRendererExtension extends LensExtension { async isEnabledForCluster(cluster: Cluster): Promise { return (void cluster) || true; } - - listenIpc< - Listener extends (event: ListenerEvent, ...args: any[]) => any - >({ - channel, - ...reg - }: { - channel: string, - listener: Listener, - verifier: ListVerifier>>, - }): void { - this.disposers.push( - onCorrect({ - source: ipcRenderer, - channel: `extensions@${this[IpcPrefix]}:${channel}`, - ...reg, - }) - ); - } - - invokeIpc(channel: string, ...args: any[]): Promise { - return ipcRenderer.invoke(`extensions@${this[IpcPrefix]}:${channel}`, ...args); - } } diff --git a/src/extensions/registries/ipc.ts b/src/extensions/registries/ipc.ts deleted file mode 100644 index 4195de4976..0000000000 --- a/src/extensions/registries/ipc.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * 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 { ListVerifier, Rest } from "../../common/ipc"; - -export interface IpcHandlerRegistration< - Handler extends (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any, -> { - channel: string; - handler: Handler, - verifier: ListVerifier>>, -} - -export interface IpcListenerRegisration { - -}