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:
parent
7a93d8f5c4
commit
6506e15b3b
@ -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.
|
||||
|
||||
@ -20,3 +20,5 @@
|
||||
*/
|
||||
|
||||
export { ExtensionStore } from "../extension-store";
|
||||
export { MainIpcStore } from "../ipc-main-store";
|
||||
export { RendererIpcStore } from "../ipc-renderer-store";
|
||||
|
||||
25
src/extensions/ipc-main-store.ts
Normal file
25
src/extensions/ipc-main-store.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
24
src/extensions/ipc-renderer-store.ts
Normal file
24
src/extensions/ipc-renderer-store.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
19
src/extensions/ipc-store.ts
Normal file
19
src/extensions/ipc-store.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user