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

improve documentation, switch to a singleton instead of extension methods

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-05-17 09:25:31 -04:00
parent 7a93d8f5c4
commit 6506e15b3b
9 changed files with 118 additions and 136 deletions

View File

@ -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(<channel>, ...<args>)` from within your extension.
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 this.invokeIpc(<channel>, ...<args>));` instead.
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

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

View File

@ -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));
}
}

View File

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

View File

@ -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);
}
}

View File

@ -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<typeof LensExtension>) => 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;
}

View File

@ -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<Handler>): 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<IpcMain>, ...args: any[]) => any
>({
channel,
...reg
}: {
channel: string,
listener: Listener,
verifier: ListVerifier<Rest<Parameters<Listener>>>,
}): void {
this.disposers.push(
onCorrect({
source: ipcMain,
channel: `extensions@${this[IpcPrefix]}:${channel}`,
...reg,
})
);
}
}

View File

@ -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<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>>>,
}): void {
this.disposers.push(
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

@ -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<Rest<Parameters<Handler>>>,
}
export interface IpcListenerRegisration {
}