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:
|
To register either a handler or a listener, you should do something like the following:
|
||||||
|
|
||||||
File1:
|
`main/extension.ts`:
|
||||||
```typescript
|
```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 {
|
export class ExampleExtensionMain extends LensMainExtension {
|
||||||
onActivate() {
|
onActivate() {
|
||||||
this.listenIpc({
|
Store.MainIpcStore.createInstance(this);
|
||||||
channel: "initialize",
|
|
||||||
listener: this.initializeListener,
|
|
||||||
verifier: this.initializeVerifier,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeListener = (event: Types.IpcMainEvent, id: string) => {
|
registerListeners();
|
||||||
console.log(`starting to initialize: ${id}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
initializeVerifier = (args: unknown[]): args is [id: string] => {
|
|
||||||
return args.length === 1 && typeof args[0] === "string";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
```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 {
|
export class ExampleExtensionRenderer extends LensRendererExtension {
|
||||||
onActivate() {
|
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.
|
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 `LensRendererExtension` and `LensMainExtension`.
|
||||||
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 `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.
|
The cleanup of these handlers is handled by Lens itself.
|
||||||
|
|
||||||
### Note about verification
|
`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.
|
||||||
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.
|
|
||||||
|
|
||||||
### Allowed Values
|
### Allowed Values
|
||||||
|
|
||||||
@ -100,6 +112,6 @@ This means that more types than what are JSON serializable can be used, but not
|
|||||||
## Using IPC
|
## Using IPC
|
||||||
|
|
||||||
Calling IPC is very simple.
|
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 { 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 logger from "../main/logger";
|
||||||
import { ProtocolHandlerRegistration } from "./registries";
|
import { ProtocolHandlerRegistration } from "./registries";
|
||||||
import { disposer } from "../common/utils";
|
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 LensExtensionId = string; // path to manifest (package.json)
|
||||||
export type LensExtensionConstructor = new (...args: ConstructorParameters<typeof LensExtension>) => LensExtension;
|
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
|
lens?: object; // fixme: add more required fields for validation
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IpcPrefix = Symbol();
|
export const Disposers = Symbol();
|
||||||
|
|
||||||
export class LensExtension {
|
export class LensExtension {
|
||||||
readonly id: LensExtensionId;
|
readonly id: LensExtensionId;
|
||||||
readonly manifest: LensExtensionManifest;
|
readonly manifest: LensExtensionManifest;
|
||||||
readonly manifestPath: string;
|
readonly manifestPath: string;
|
||||||
readonly isBundled: boolean;
|
readonly isBundled: boolean;
|
||||||
readonly [IpcPrefix]: string;
|
|
||||||
|
|
||||||
protocolHandlers: ProtocolHandlerRegistration[] = [];
|
protocolHandlers: ProtocolHandlerRegistration[] = [];
|
||||||
|
|
||||||
@observable private isEnabled = false;
|
@observable private isEnabled = false;
|
||||||
protected disposers = disposer();
|
[Disposers] = disposer();
|
||||||
|
|
||||||
constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension) {
|
constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.manifest = manifest;
|
this.manifest = manifest;
|
||||||
this.manifestPath = manifestPath;
|
this.manifestPath = manifestPath;
|
||||||
this.isBundled = !!isBundled;
|
this.isBundled = !!isBundled;
|
||||||
this[IpcPrefix] = createHash("sha256").update(this.id).digest("hex");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
@ -98,7 +94,7 @@ export class LensExtension {
|
|||||||
if (!this.isEnabled) return;
|
if (!this.isEnabled) return;
|
||||||
this.isEnabled = false;
|
this.isEnabled = false;
|
||||||
this.onDeactivate?.();
|
this.onDeactivate?.();
|
||||||
this.disposers();
|
this[Disposers]();
|
||||||
logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`);
|
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 {
|
protected onActivate(): void {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,14 +19,12 @@
|
|||||||
* 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 { IpcPrefix, LensExtension } from "./lens-extension";
|
import { LensExtension } from "./lens-extension";
|
||||||
import { WindowManager } from "../main/window-manager";
|
import { WindowManager } from "../main/window-manager";
|
||||||
import { getExtensionPageUrl } from "./registries/page-registry";
|
import { getExtensionPageUrl } from "./registries/page-registry";
|
||||||
import { CatalogEntity, catalogEntityRegistry } from "../common/catalog";
|
import { CatalogEntity, catalogEntityRegistry } from "../common/catalog";
|
||||||
import { IObservableArray } from "mobx";
|
import { IObservableArray } from "mobx";
|
||||||
import { IpcHandlerRegistration, MenuRegistration } from "./registries";
|
import { MenuRegistration } from "./registries";
|
||||||
import { handleCorrect, ListenerEvent, ListVerifier, onCorrect, Rest } from "../common/ipc";
|
|
||||||
import { ipcMain, IpcMain } from "electron";
|
|
||||||
|
|
||||||
export class LensMainExtension extends LensExtension {
|
export class LensMainExtension extends LensExtension {
|
||||||
appMenus: MenuRegistration[] = [];
|
appMenus: MenuRegistration[] = [];
|
||||||
@ -49,35 +47,4 @@ export class LensMainExtension extends LensExtension {
|
|||||||
removeCatalogSource(id: string) {
|
removeCatalogSource(id: string) {
|
||||||
catalogEntityRegistry.removeSource(`${this.name}:${id}`);
|
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,
|
KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, WelcomeMenuRegistration,
|
||||||
} from "./registries";
|
} from "./registries";
|
||||||
import type { Cluster } from "../main/cluster";
|
import type { Cluster } from "../main/cluster";
|
||||||
import { IpcPrefix, LensExtension } from "./lens-extension";
|
import { LensExtension } from "./lens-extension";
|
||||||
import { getExtensionPageUrl } from "./registries/page-registry";
|
import { getExtensionPageUrl } from "./registries/page-registry";
|
||||||
import { CommandRegistration } from "./registries/command-registry";
|
import { CommandRegistration } from "./registries/command-registry";
|
||||||
import { EntitySettingRegistration } from "./registries/entity-setting-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 {
|
export class LensRendererExtension extends LensExtension {
|
||||||
globalPages: PageRegistration[] = [];
|
globalPages: PageRegistration[] = [];
|
||||||
@ -62,27 +60,4 @@ export class LensRendererExtension extends LensExtension {
|
|||||||
async isEnabledForCluster(cluster: Cluster): Promise<Boolean> {
|
async isEnabledForCluster(cluster: Cluster): Promise<Boolean> {
|
||||||
return (void cluster) || true;
|
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