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

Change where extension IPC is exported (#2845)

- Fix documentation and guide
This commit is contained in:
Sebastian Malton 2021-05-26 17:10:39 -04:00 committed by GitHub
parent c9e0aa221a
commit 57c87a2e71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 140 additions and 74 deletions

View File

@ -1,35 +1,36 @@
# Inter Process Communication # 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. 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. 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. 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 - Event based (IPC)
- Request based - 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 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 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. 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). This is more like a Remote Procedure Call (RPC) or Send-Receive-Reply (SRR).
With this sort of IPC the caller waits for the result from the other side. With this sort of communication the caller needs to wait for the result from the other side.
This is accomplished by returning a `Promise<T>` which needs to be `await`-ed. This is accomplished by `await`-ing the returned `Promise<any>`.
This is a unidirectional form of communication. 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 ## 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`: `main.ts`:
```typescript ```typescript
import { LensMainExtension, Interface, Types, Store } from "@k8slens/extensions"; import { LensMainExtension } from "@k8slens/extensions";
import { registerListeners, IpcMain } from "./helpers/main"; import { IpcMain } from "./helpers/main";
export class ExampleExtensionMain extends LensMainExtension { export class ExampleExtensionMain extends LensMainExtension {
onActivate() { onActivate() {
@ -59,13 +60,13 @@ Lens will automatically clean up that store and all the handlers on deactivation
`helpers/main.ts`: `helpers/main.ts`:
```typescript ```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) { constructor(extension: LensMainExtension) {
super(extension); 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. 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`: `renderer.ts`:
```typescript ```typescript
import { LensRendererExtension, Interface, Types } from "@k8slens/extensions"; import { LensRendererExtension } from "@k8slens/extensions";
import { IpcRenderer } from "./helpers/renderer"; import { IpcRenderer } from "./helpers/renderer";
export class ExampleExtensionRenderer extends LensRendererExtension { export class ExampleExtensionRenderer extends LensRendererExtension {
onActivate() { onActivate() {
const ipc = IpcRenderer.createInstance(this); 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`: `helpers/renderer.ts`:
```typescript ```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. 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. 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. 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. 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. Because of this, no matter where you broadcast from, all listeners in `main` and `renderer` will be notified.
### Allowed Values ### 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 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. 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 a request based call from `renderer`, you should do `const res = await IpcRenderer.getInstance().invoke(<channel>, ...<args>));` instead.
If you are meaning to do an event based call, merely call `broadcastIpc(<channel>, ...<args>)` 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(<channel>, ...<args>));` instead.

View File

@ -31,6 +31,7 @@ import * as Util from "./utils";
import * as Interface from "../interfaces"; import * as Interface from "../interfaces";
import * as Catalog from "./catalog"; import * as Catalog from "./catalog";
import * as Types from "./types"; import * as Types from "./types";
import * as Ipc from "./ipc";
export { export {
App, App,
@ -40,4 +41,5 @@ export {
Store, Store,
Types, Types,
Util, Util,
Ipc,
}; };

View File

@ -18,27 +18,6 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * 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. * 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 { export { IpcMain as Main } from "../ipc/ipc-main";
constructor(extension: LensRendererExtension) { export { IpcRegistrar as Registrar } from "../ipc/ipc-registrar";
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<any> {
const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`;
return ipcRenderer.invoke(prefixedChannel, ...args);
}
}

View File

@ -20,5 +20,3 @@
*/ */
export { ExtensionStore } from "../extension-store"; export { ExtensionStore } from "../extension-store";
export { MainIpcStore } from "../main-ipc-store";
export { RendererIpcStore } from "../renderer-ipc-store";

View File

@ -19,27 +19,43 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { ipcMain } from "electron"; import { ipcMain } from "electron";
import { IpcPrefix, IpcStore } from "./ipc-store"; import { IpcPrefix, IpcRegistrar } from "./ipc-registrar";
import { Disposers } from "./lens-extension"; import { Disposers } from "../lens-extension";
import type { LensMainExtension } from "./lens-main-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) { constructor(extension: LensMainExtension) {
super(extension); 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}`; const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`;
ipcMain.handle(prefixedChannel, handler); ipcMain.handle(prefixedChannel, handler);
this.extension[Disposers].push(() => ipcMain.removeHandler(prefixedChannel)); 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));
}
} }

View File

@ -18,14 +18,14 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * 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. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { Singleton } from "../common/utils"; import { Singleton } from "../../common/utils";
import type { LensExtension } from "./lens-extension"; import type { LensExtension } from "../lens-extension";
import { createHash } from "crypto"; import { createHash } from "crypto";
import { broadcastMessage } from "../common/ipc"; import { broadcastMessage } from "../../common/ipc";
export const IpcPrefix = Symbol(); export const IpcPrefix = Symbol();
export abstract class IpcStore extends Singleton { export abstract class IpcRegistrar extends Singleton {
readonly [IpcPrefix]: string; readonly [IpcPrefix]: string;
constructor(protected extension: LensExtension) { constructor(protected extension: LensExtension) {
@ -33,7 +33,12 @@ export abstract class IpcStore extends Singleton {
this[IpcPrefix] = createHash("sha256").update(extension.id).digest("hex"); 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); broadcastMessage(`extensions@${this[IpcPrefix]}:${channel}`, ...args);
} }
} }

View File

@ -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<any> {
const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`;
return ipcRenderer.invoke(prefixedChannel, ...args);
}
}