1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Add IPC capabilities for Extensions

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-05-14 10:26:24 -04:00
parent 6a33944f52
commit e3135bc65c
11 changed files with 282 additions and 34 deletions

View File

@ -24,6 +24,7 @@ Each guide or code sample includes the following:
| [KubeObjectListLayout](kube-object-list-layout.md) | |
| [Working with mobx](working-with-mobx.md) | |
| [Protocol Handlers](protocol-handlers.md) | |
| [Sending Data between main and renderer](ipc.md) | |
## Samples

View File

@ -0,0 +1,92 @@
# Inter Process Communication
A Lens Extension can utilise 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.
IPC channels are blocked off per extension.
Meaning that each extension can only communicate with itself.
## Types of IPC
There are two flavours of IPC that are provided:
- Event based
- Request based
### Event Based 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.
### Request Based IPC
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<T>` 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.
## Registering IPC Handlers and Listeners
The general terminology is as follows:
- A "handler" is the function that responds to a "Request Based IPC" event.
- A "listener" is the function that is called when a "Event Based IPC" event is emitted.
To register either a handler or a listener, you should do something like the following:
```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,
})
);
}
initializeListener = (event: Types.IpcMainEvent, uid: string) => {
console.log(`starting to initialize: ${uid}`);
};
initializeVerifier = (args: unknown[]): args is [uid: string] => {
return args.length === 1 && typeof args[0] === "string";
}
}
```
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.
### 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.
## Using IPC
Calling IPC is very simple.
If you are meaning to do an event based call, merely call `this.sendIpc(<channel>, ...<args>)` from within your extension.
If you are meaning to do a request based call from `renderer`, you should do `const res = await this.invokeIpc(<channel>, ...<args>));` instead.

View File

@ -35,6 +35,7 @@ nav:
- Stores: extensions/guides/stores.md
- Working with MobX: extensions/guides/working-with-mobx.md
- Protocol Handlers: extensions/guides/protocol-handlers.md
- IPC: extensions/guides/ipc.md
- Testing and Publishing:
- Testing Extensions: extensions/testing-and-publishing/testing.md
- Publishing Extensions: extensions/testing-and-publishing/publishing.md

View File

@ -19,10 +19,12 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { ipcMain } from "electron";
import { EventEmitter } from "events";
import logger from "../../main/logger";
import { Disposer } from "../utils";
export type HandlerEvent<EM extends EventEmitter> = Parameters<Parameters<EM["on"]>[1]>[0];
export type ListenerEvent<EM extends EventEmitter> = Parameters<Parameters<EM["on"]>[1]>[0];
export type ListVerifier<T extends any[]> = (args: unknown[]) => args is T;
export type Rest<T> = T extends [any, ...infer R] ? R : [];
@ -34,22 +36,22 @@ export type Rest<T> = T extends [any, ...infer R] ? R : [];
* @param verifier The function to be called to verify that the args are the correct type
*/
export function onceCorrect<
EM extends EventEmitter,
L extends (event: HandlerEvent<EM>, ...args: any[]) => any
IPC extends EventEmitter,
Listener extends (event: ListenerEvent<IPC>, ...args: any[]) => any
>({
source,
channel,
listener,
verifier,
}: {
source: EM,
channel: string | symbol,
listener: L,
verifier: ListVerifier<Rest<Parameters<L>>>,
source: IPC,
channel: string,
listener: Listener,
verifier: ListVerifier<Rest<Parameters<Listener>>>,
}): void {
function handler(event: HandlerEvent<EM>, ...args: unknown[]): void {
function wrappedListener(event: ListenerEvent<IPC>, ...args: unknown[]): void {
if (verifier(args)) {
source.removeListener(channel, handler); // remove immediately
source.removeListener(channel, wrappedListener); // remove immediately
(async () => (listener(event, ...args)))() // might return a promise, or throw, or reject
.catch((error: any) => logger.error("[IPC]: channel once handler threw error", { channel, error }));
@ -58,7 +60,7 @@ export function onceCorrect<
}
}
source.on(channel, handler);
source.on(channel, wrappedListener);
}
/**
@ -68,25 +70,53 @@ export function onceCorrect<
* @param verifier The function to be called to verify that the args are the correct type
*/
export function onCorrect<
EM extends EventEmitter,
L extends (event: HandlerEvent<EM>, ...args: any[]) => any
IPC extends EventEmitter,
Listener extends (event: ListenerEvent<IPC>, ...args: any[]) => any
>({
source,
channel,
listener,
verifier,
}: {
source: EM,
channel: string | symbol,
listener: L,
verifier: ListVerifier<Rest<Parameters<L>>>,
}): void {
source.on(channel, (event, ...args: unknown[]) => {
source: IPC,
channel: string,
listener: Listener,
verifier: ListVerifier<Rest<Parameters<Listener>>>,
}): Disposer {
function wrappedListener(event: ListenerEvent<IPC>, ...args: unknown[]) {
if (verifier(args)) {
(async () => (listener(event, ...args)))() // might return a promise, or throw, or reject
.catch(error => logger.error("[IPC]: channel on handler threw error", { channel, error }));
} else {
logger.error("[IPC]: channel was emitted with invalid data", { channel, args });
}
});
}
source.on(channel, wrappedListener);
return () => source.off(channel, wrappedListener);
}
export function handleCorrect<
Handler extends (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any,
>({
channel,
handler,
verifier,
}: {
channel: string,
handler: Handler,
verifier: ListVerifier<Rest<Parameters<Handler>>>,
}): Disposer {
function wrappedHandler(event: Electron.IpcMainInvokeEvent, ...args: unknown[]): ReturnType<Handler> {
if (verifier(args)) {
return handler(event, ...args);
}
throw new TypeError(`Invalid args for invoke on channel: ${channel}`);
}
ipcMain.handle(channel, wrappedHandler);
return () => ipcMain.removeHandler(channel);
}

View File

@ -31,6 +31,7 @@ import * as Util from "./utils";
import * as ClusterFeature from "./cluster-feature";
import * as Interface from "../interfaces";
import * as Catalog from "./catalog";
import * as Types from "./types";
export {
App,
@ -39,5 +40,6 @@ export {
ClusterFeature,
Interface,
Store,
Types,
Util,
};

View File

@ -0,0 +1,24 @@
/**
* 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.
*/
export type IpcMainInvokeEvent = Electron.IpcMainInvokeEvent;
export type IpcRendererEvent = Electron.IpcRendererEvent;
export type IpcMainEvent = Electron.IpcMainEvent;

View File

@ -23,7 +23,10 @@ import type { InstalledExtension } from "./extension-discovery";
import { action, observable, reaction } from "mobx";
import { FilesystemProvisionerStore } from "../main/extension-filesystem";
import logger from "../main/logger";
import { ProtocolHandlerRegistration } from "./registries/protocol-handler-registry";
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<typeof LensExtension>) => LensExtension;
@ -37,21 +40,26 @@ export interface LensExtensionManifest {
lens?: object; // fixme: add more required fields for validation
}
export const IpcPrefix = 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;
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() {
@ -62,6 +70,10 @@ export class LensExtension {
return this.manifest.version;
}
get description() {
return this.manifest.description;
}
/**
* getExtensionFileFolder returns the path to an already created folder. This
* folder is for the sole use of this extension.
@ -73,15 +85,11 @@ export class LensExtension {
return FilesystemProvisionerStore.getInstance().requestDirectory(this.id);
}
get description() {
return this.manifest.description;
}
@action
async enable() {
if (this.isEnabled) return;
this.isEnabled = true;
this.onActivate();
this.onActivate?.();
logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`);
}
@ -89,7 +97,8 @@ export class LensExtension {
async disable() {
if (!this.isEnabled) return;
this.isEnabled = false;
this.onDeactivate();
this.onDeactivate?.();
this.disposers();
logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`);
}
@ -125,13 +134,12 @@ export class LensExtension {
};
}
protected onActivate() {
// mock
sendIpc(channel: string, ...args: any[]): void {
broadcastMessage(`extensions@${this[IpcPrefix]}:${channel}`, ...args);
}
protected onDeactivate() {
// mock
}
protected onActivate?: () => void;
protected onDeactivate?: () => void;
}
export function sanitizeExtensionName(name: string) {

View File

@ -19,12 +19,15 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { MenuRegistration } from "./registries/menu-registry";
import { LensExtension } from "./lens-extension";
import { IpcPrefix, 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 type { Disposer } from "../common/utils";
import { handleCorrect, ListenerEvent, ListVerifier, onCorrect, Rest } from "../common/ipc";
import { ipcMain, IpcMain } from "electron";
export class LensMainExtension extends LensExtension {
appMenus: MenuRegistration[] = [];
@ -47,4 +50,31 @@ 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<Handler>): Disposer {
// 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,
});
}
listenIpc<
Listener extends (event: ListenerEvent<IpcMain>, ...args: any[]) => any
>({
channel,
...reg
}: {
channel: string,
listener: Listener,
verifier: ListVerifier<Rest<Parameters<Listener>>>,
}): Disposer {
return onCorrect({
source: ipcMain,
channel: `extensions@${this[IpcPrefix]}:${channel}`,
...reg,
});
}
}

View File

@ -24,10 +24,13 @@ import type {
KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, WelcomeMenuRegistration,
} from "./registries";
import type { Cluster } from "../main/cluster";
import { LensExtension } from "./lens-extension";
import { IpcPrefix, 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";
import { Disposer } from "../common/utils";
export class LensRendererExtension extends LensExtension {
globalPages: PageRegistration[] = [];
@ -60,4 +63,25 @@ export class LensRendererExtension extends LensExtension {
async isEnabledForCluster(cluster: Cluster): Promise<Boolean> {
return (void cluster) || true;
}
listenIpc<
Listener extends (event: ListenerEvent<IpcRenderer>, ...args: any[]) => any
>({
channel,
...reg
}: {
channel: string,
listener: Listener,
verifier: ListVerifier<Rest<Parameters<Listener>>>,
}): Disposer {
return onCorrect({
source: ipcRenderer,
channel: `extensions@${this[IpcPrefix]}:${channel}`,
...reg,
});
}
invokeIpc(channel: string, ...args: any[]): Promise<any> {
return ipcRenderer.invoke(`extensions@${this[IpcPrefix]}:${channel}`, ...args);
}
}

View File

@ -32,3 +32,5 @@ export * from "./kube-object-status-registry";
export * from "./command-registry";
export * from "./entity-setting-registry";
export * from "./welcome-menu-registry";
export * from "./protocol-handler-registry";
export * from "./ipc";

View File

@ -0,0 +1,34 @@
/**
* 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<Rest<Parameters<Handler>>>,
}
export interface IpcListenerRegisration {
}