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

Refactor ipc module (#1225)

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen 2020-11-19 11:48:13 +02:00 committed by GitHub
parent c4b98534dc
commit 5aaacb21f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 224 additions and 242 deletions

View File

@ -6,7 +6,7 @@ import { action, IReactionOptions, observable, reaction, runInAction, toJS, when
import Singleton from "./utils/singleton"; import Singleton from "./utils/singleton";
import { getAppVersion } from "./utils/app-version"; import { getAppVersion } from "./utils/app-version";
import logger from "../main/logger"; import logger from "../main/logger";
import { broadcastIpc, IpcBroadcastParams } from "./ipc"; import { broadcastMessage, subscribeToBroadcast, unsubscribeFromBroadcast } from "./ipc";
import isEqual from "lodash/isEqual"; import isEqual from "lodash/isEqual";
export interface BaseStoreParams<T = any> extends ConfOptions<T> { export interface BaseStoreParams<T = any> extends ConfOptions<T> {
@ -37,12 +37,16 @@ export class BaseStore<T = any> extends Singleton {
return path.basename(this.storeConfig.path); return path.basename(this.storeConfig.path);
} }
get path() { protected get syncRendererChannel() {
return this.storeConfig.path; return `store-sync-renderer:${this.path}`
} }
get syncChannel() { protected get syncMainChannel() {
return `STORE-SYNC:${this.path}` return `store-sync-main:${this.path}`
}
get path() {
return this.storeConfig.path;
} }
protected async init() { protected async init() {
@ -89,16 +93,16 @@ export class BaseStore<T = any> extends Singleton {
logger.silly(`[STORE]: SYNC ${this.name} from renderer`, { model }); logger.silly(`[STORE]: SYNC ${this.name} from renderer`, { model });
this.onSync(model); this.onSync(model);
}; };
ipcMain.on(this.syncChannel, callback); subscribeToBroadcast(this.syncMainChannel, callback)
this.syncDisposers.push(() => ipcMain.off(this.syncChannel, callback)); this.syncDisposers.push(() => unsubscribeFromBroadcast(this.syncMainChannel, callback));
} }
if (ipcRenderer) { if (ipcRenderer) {
const callback = (event: IpcRendererEvent, model: T) => { const callback = (event: IpcRendererEvent, model: T) => {
logger.silly(`[STORE]: SYNC ${this.name} from main`, { model }); logger.silly(`[STORE]: SYNC ${this.name} from main`, { model });
this.onSyncFromMain(model); this.onSyncFromMain(model);
}; };
ipcRenderer.on(this.syncChannel, callback); subscribeToBroadcast(this.syncRendererChannel, callback)
this.syncDisposers.push(() => ipcRenderer.off(this.syncChannel, callback)); this.syncDisposers.push(() => unsubscribeFromBroadcast(this.syncRendererChannel, callback));
} }
} }
@ -109,7 +113,8 @@ export class BaseStore<T = any> extends Singleton {
} }
unregisterIpcListener() { unregisterIpcListener() {
ipcRenderer.removeAllListeners(this.syncChannel) ipcRenderer.removeAllListeners(this.syncMainChannel)
ipcRenderer.removeAllListeners(this.syncRendererChannel)
} }
disableSync() { disableSync() {
@ -135,41 +140,10 @@ export class BaseStore<T = any> extends Singleton {
protected async onModelChange(model: T) { protected async onModelChange(model: T) {
if (ipcMain) { if (ipcMain) {
this.saveToFile(model); // save config file this.saveToFile(model); // save config file
this.syncToWebViews(model); // send update to renderer views broadcastMessage(this.syncRendererChannel, model)
} else {
broadcastMessage(this.syncMainChannel, model)
} }
// send "update-request" to main-process
if (ipcRenderer) {
ipcRenderer.send(this.syncChannel, model);
}
}
protected async syncToWebViews(model: T) {
const msg: IpcBroadcastParams = {
channel: this.syncChannel,
args: [model],
}
broadcastIpc(msg); // send to all windows (BrowserWindow, webContents)
const frames = await this.getSubFrames();
frames.forEach(frameId => {
// send to all sub-frames (e.g. cluster-view managed in iframe)
broadcastIpc({
...msg,
frameId: frameId,
frameOnly: true,
});
});
}
// todo: refactor?
protected async getSubFrames(): Promise<number[]> {
const subFrames: number[] = [];
const { clusterStore } = await import("./cluster-store");
clusterStore.clustersList.forEach(cluster => {
if (cluster.frameId) {
subFrames.push(cluster.frameId)
}
});
return subFrames;
} }
@action @action

View File

@ -0,0 +1,3 @@
import { observable } from "mobx"
export const clusterFrameMap = observable.map<string, number>();

View File

@ -1,51 +1,48 @@
import { createIpcChannel } from "./ipc"; import { handleRequest } from "./ipc";
import { ClusterId, clusterStore } from "./cluster-store"; import { ClusterId, clusterStore } from "./cluster-store";
import { extensionLoader } from "../extensions/extension-loader"
import { appEventBus } from "./event-bus" import { appEventBus } from "./event-bus"
import { ResourceApplier } from "../main/resource-applier"; import { ResourceApplier } from "../main/resource-applier";
import { ipcMain } from "electron";
import { clusterFrameMap } from "./cluster-frames"
export const clusterIpc = { export const clusterActivateHandler = "cluster:activate"
activate: createIpcChannel({ export const clusterSetFrameIdHandler = "cluster:set-frame-id"
channel: "cluster:activate", export const clusterRefreshHandler = "cluster:refresh"
handle: (clusterId: ClusterId, force = false) => { export const clusterDisconnectHandler = "cluster:disconnect"
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all"
if (ipcMain) {
handleRequest(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
const cluster = clusterStore.getById(clusterId); const cluster = clusterStore.getById(clusterId);
if (cluster) { if (cluster) {
return cluster.activate(force); return cluster.activate(force);
} }
}, })
}),
setFrameId: createIpcChannel({ handleRequest(clusterSetFrameIdHandler, (event, clusterId: ClusterId, frameId: number) => {
channel: "cluster:set-frame-id",
handle: (clusterId: ClusterId, frameId?: number) => {
const cluster = clusterStore.getById(clusterId); const cluster = clusterStore.getById(clusterId);
if (cluster) { if (cluster) {
if (frameId) cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates clusterFrameMap.set(cluster.id, frameId)
extensionLoader.broadcastExtensions(frameId)
return cluster.pushState(); return cluster.pushState();
} }
}, })
}),
refresh: createIpcChannel({ handleRequest(clusterRefreshHandler, (event, clusterId: ClusterId) => {
channel: "cluster:refresh",
handle: (clusterId: ClusterId) => {
const cluster = clusterStore.getById(clusterId); const cluster = clusterStore.getById(clusterId);
if (cluster) return cluster.refresh({ refreshMetadata: true }) if (cluster) return cluster.refresh({ refreshMetadata: true })
}, })
}),
disconnect: createIpcChannel({ handleRequest(clusterDisconnectHandler, (event, clusterId: ClusterId) => {
channel: "cluster:disconnect",
handle: (clusterId: ClusterId) => {
appEventBus.emit({name: "cluster", action: "stop"}); appEventBus.emit({name: "cluster", action: "stop"});
return clusterStore.getById(clusterId)?.disconnect(); const cluster = clusterStore.getById(clusterId);
}, if (cluster) {
}), cluster.disconnect();
clusterFrameMap.delete(cluster.id)
}
})
kubectlApplyAll: createIpcChannel({ handleRequest(clusterKubectlApplyAllHandler, (event, clusterId: ClusterId, resources: string[]) => {
channel: "cluster:kubectl-apply-all",
handle: (clusterId: ClusterId, resources: string[]) => {
appEventBus.emit({name: "cluster", action: "kubectl-apply-all"}) appEventBus.emit({name: "cluster", action: "kubectl-apply-all"})
const cluster = clusterStore.getById(clusterId); const cluster = clusterStore.getById(clusterId);
if (cluster) { if (cluster) {
@ -54,6 +51,5 @@ export const clusterIpc = {
} else { } else {
throw `${clusterId} is not a valid cluster id`; throw `${clusterId} is not a valid cluster id`;
} }
} })
}),
} }

View File

@ -2,7 +2,7 @@ import type { WorkspaceId } from "./workspace-store";
import path from "path"; import path from "path";
import { app, ipcRenderer, remote, webFrame } from "electron"; import { app, ipcRenderer, remote, webFrame } from "electron";
import { unlink } from "fs-extra"; import { unlink } from "fs-extra";
import { action, computed, observable, toJS } from "mobx"; import { action, computed, observable, reaction, toJS } from "mobx";
import { BaseStore } from "./base-store"; import { BaseStore } from "./base-store";
import { Cluster, ClusterState } from "../main/cluster"; import { Cluster, ClusterState } from "../main/cluster";
import migrations from "../migrations/cluster-store" import migrations from "../migrations/cluster-store"
@ -13,6 +13,7 @@ import { saveToAppFiles } from "./utils/saveToAppFiles";
import { KubeConfig } from "@kubernetes/client-node"; import { KubeConfig } from "@kubernetes/client-node";
import _ from "lodash"; import _ from "lodash";
import move from "array-move"; import move from "array-move";
import { subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc";
export interface ClusterIconUpload { export interface ClusterIconUpload {
clusterId: string; clusterId: string;
@ -85,21 +86,20 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
migrations: migrations, migrations: migrations,
}); });
this.pushStateToViewsPeriodically() this.pushStateToViewsAutomatically()
} }
protected pushStateToViewsPeriodically() { protected pushStateToViewsAutomatically() {
if (!ipcRenderer) { if (!ipcRenderer) {
// This is a bit of a hack, we need to do this because we might loose messages that are sent before a view is ready reaction(() => this.connectedClustersList, () => {
setInterval(() => {
this.pushState() this.pushState()
}, 5000) })
} }
} }
registerIpcListener() { registerIpcListener() {
logger.info(`[CLUSTER-STORE] start to listen (${webFrame.routingId})`) logger.info(`[CLUSTER-STORE] start to listen (${webFrame.routingId})`)
ipcRenderer.on("cluster:state", (event, clusterId: string, state: ClusterState) => { subscribeToBroadcast("cluster:state", (event, clusterId: string, state: ClusterState) => {
logger.silly(`[CLUSTER-STORE]: received push-state at ${location.host} (${webFrame.routingId})`, clusterId, state); logger.silly(`[CLUSTER-STORE]: received push-state at ${location.host} (${webFrame.routingId})`, clusterId, state);
this.getById(clusterId)?.setState(state) this.getById(clusterId)?.setState(state)
}) })
@ -107,7 +107,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
unregisterIpcListener() { unregisterIpcListener() {
super.unregisterIpcListener() super.unregisterIpcListener()
ipcRenderer.removeAllListeners("cluster:state") unsubscribeAllFromBroadcast("cluster:state")
} }
pushState() { pushState() {
@ -132,6 +132,10 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
return this.getById(this.activeCluster); return this.getById(this.activeCluster);
} }
@computed get connectedClustersList(): Cluster[] {
return this.clustersList.filter((c) => !c.disconnected)
}
isActive(id: ClusterId) { isActive(id: ClusterId) {
return this.activeCluster === id; return this.activeCluster === id;
} }

View File

@ -1,79 +1,70 @@
// Inter-protocol communications (main <-> renderer) // Inter-process communications (main <-> renderer)
// https://www.electronjs.org/docs/api/ipc-main // https://www.electronjs.org/docs/api/ipc-main
// https://www.electronjs.org/docs/api/ipc-renderer // https://www.electronjs.org/docs/api/ipc-renderer
import { ipcMain, ipcRenderer, WebContents, webContents } from "electron" import { ipcMain, ipcRenderer, webContents, remote } from "electron";
import logger from "../main/logger"; import logger from "../main/logger";
import { clusterFrameMap } from "./cluster-frames";
export type IpcChannel = string; export function handleRequest(channel: string, listener: (...args: any[]) => any) {
ipcMain.handle(channel, listener)
export interface IpcChannelOptions {
channel: IpcChannel; // main <-> renderer communication channel name
handle?: (...args: any[]) => Promise<any> | any; // message handler
autoBind?: boolean; // auto-bind message handler in main-process, default: true
timeout?: number; // timeout for waiting response from the sender
once?: boolean; // one-time event
} }
export function createIpcChannel({ autoBind = true, once, timeout = 0, handle, channel }: IpcChannelOptions) { export async function requestMain(channel: string, ...args: any[]) {
const ipcChannel = { return ipcRenderer.invoke(channel, ...args)
channel: channel,
handleInMain: () => {
logger.info(`[IPC]: setup channel "${channel}"`);
const ipcHandler = once ? ipcMain.handleOnce : ipcMain.handle;
ipcHandler(channel, async (event, ...args) => {
let timerId: any;
try {
if (timeout > 0) {
timerId = setTimeout(() => {
throw new Error(`[IPC]: response timeout in ${timeout}ms`)
}, timeout);
}
return await handle(...args); // todo: maybe exec in separate thread/worker
} catch (error) {
throw error
} finally {
clearTimeout(timerId);
}
})
},
removeHandler() {
ipcMain.removeHandler(channel);
},
invokeFromRenderer: async <T>(...args: any[]): Promise<T> => {
return ipcRenderer.invoke(channel, ...args);
},
}
if (autoBind && ipcMain) {
ipcChannel.handleInMain();
}
return ipcChannel;
} }
export interface IpcBroadcastParams<A extends any[] = any> { async function getSubFrames(): Promise<number[]> {
channel: IpcChannel const subFrames: number[] = [];
webContentId?: number; // send to single webContents view clusterFrameMap.forEach((frameId, _) => {
frameId?: number; // send to inner frame of webContents subFrames.push(frameId)
frameOnly?: boolean; // send message only to view with provided `frameId` });
filter?: (webContent: WebContents) => boolean return subFrames;
timeout?: number; // todo: add support
args?: A;
} }
export function broadcastIpc({ channel, frameId, frameOnly, webContentId, filter, args = [] }: IpcBroadcastParams) { export function broadcastMessage(channel: string, ...args: any[]) {
const singleView = webContentId ? webContents.fromId(webContentId) : null; const views = (webContents || remote?.webContents)?.getAllWebContents();
let views = singleView ? [singleView] : webContents.getAllWebContents(); if (!views) return
if (filter) {
views = views.filter(filter);
}
views.forEach(webContent => { views.forEach(webContent => {
const type = webContent.getType(); const type = webContent.getType();
logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${webContent.id}`, { args }); logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${webContent.id}`, { args });
if (!frameOnly) {
webContent.send(channel, ...args); webContent.send(channel, ...args);
} getSubFrames().then((frames) => {
if (frameId) { frames.map((frameId) => {
webContent.sendToFrame(frameId, channel, ...args) webContent.sendToFrame(frameId, channel, ...args)
}
}) })
}).catch((e) => e)
})
if (ipcRenderer) {
ipcRenderer.send(channel, ...args)
} else {
ipcMain.emit(channel, ...args)
}
}
export function subscribeToBroadcast(channel: string, listener: (...args: any[]) => any) {
if (ipcRenderer) {
ipcRenderer.on(channel, listener)
} else {
ipcMain.on(channel, listener)
}
return listener
}
export function unsubscribeFromBroadcast(channel: string, listener: (...args: any[]) => any) {
if (ipcRenderer) {
ipcRenderer.off(channel, listener)
} else {
ipcMain.off(channel, listener)
}
}
export function unsubscribeAllFromBroadcast(channel: string) {
if (ipcRenderer) {
ipcRenderer.removeAllListeners(channel)
} else {
ipcMain.removeAllListeners(channel)
}
} }

View File

@ -3,7 +3,7 @@ import { action, computed, observable, toJS, reaction } from "mobx";
import { BaseStore } from "./base-store"; import { BaseStore } from "./base-store";
import { clusterStore } from "./cluster-store" import { clusterStore } from "./cluster-store"
import { appEventBus } from "./event-bus"; import { appEventBus } from "./event-bus";
import { broadcastIpc } from "../common/ipc"; import { broadcastMessage } from "../common/ipc";
import logger from "../main/logger"; import logger from "../main/logger";
export type WorkspaceId = string; export type WorkspaceId = string;
@ -53,10 +53,7 @@ export class Workspace implements WorkspaceModel, WorkspaceState {
pushState(state = this.getState()) { pushState(state = this.getState()) {
logger.silly("[WORKSPACE] pushing state", {...state, id: this.id}) logger.silly("[WORKSPACE] pushing state", {...state, id: this.id})
broadcastIpc({ broadcastMessage("workspace:state", this.id, toJS(state))
channel: "workspace:state",
args: [this.id, toJS(state)],
});
} }
@action @action

View File

@ -6,7 +6,8 @@ import { ResourceApplier } from "../main/resource-applier"
import { Cluster } from "../main/cluster"; import { Cluster } from "../main/cluster";
import logger from "../main/logger"; import logger from "../main/logger";
import { app } from "electron" import { app } from "electron"
import { clusterIpc } from "../common/cluster-ipc" import { requestMain } from "../common/ipc";
import { clusterKubectlApplyAllHandler } from "../common/cluster-ipc";
export interface ClusterFeatureStatus { export interface ClusterFeatureStatus {
currentVersion: string; currentVersion: string;
@ -39,7 +40,7 @@ export abstract class ClusterFeature {
if (app) { if (app) {
await new ResourceApplier(cluster).kubectlApplyAll(resources) await new ResourceApplier(cluster).kubectlApplyAll(resources)
} else { } else {
await clusterIpc.kubectlApplyAll.invokeFromRenderer(cluster.id, resources) await requestMain(clusterKubectlApplyAllHandler, cluster.id, resources)
} }
} }

View File

@ -3,7 +3,7 @@ import type { LensMainExtension } from "./lens-main-extension"
import type { LensRendererExtension } from "./lens-renderer-extension" import type { LensRendererExtension } from "./lens-renderer-extension"
import type { InstalledExtension } from "./extension-manager"; import type { InstalledExtension } from "./extension-manager";
import path from "path" import path from "path"
import { broadcastIpc } from "../common/ipc" import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc"
import { action, computed, observable, reaction, toJS, when } from "mobx" import { action, computed, observable, reaction, toJS, when } from "mobx"
import logger from "../main/logger" import logger from "../main/logger"
import { app, ipcRenderer, remote } from "electron" import { app, ipcRenderer, remote } from "electron"
@ -18,24 +18,11 @@ export function extensionPackagesRoot() {
export class ExtensionLoader { export class ExtensionLoader {
protected extensions = observable.map<LensExtensionId, InstalledExtension>(); protected extensions = observable.map<LensExtensionId, InstalledExtension>();
protected instances = observable.map<LensExtensionId, LensExtension>(); protected instances = observable.map<LensExtensionId, LensExtension>();
protected readonly requestExtensionsChannel = "extensions:loaded"
@observable isLoaded = false; @observable isLoaded = false;
whenLoaded = when(() => this.isLoaded); whenLoaded = when(() => this.isLoaded);
constructor() {
if (ipcRenderer) {
ipcRenderer.on("extensions:loaded", (event, extensions: [LensExtensionId, InstalledExtension][]) => {
this.isLoaded = true;
extensions.forEach(([extId, ext]) => {
if (!this.extensions.has(extId)) {
this.extensions.set(extId, ext)
}
})
});
}
extensionsStore.manageState(this);
}
@computed get userExtensions(): Map<LensExtensionId, InstalledExtension> { @computed get userExtensions(): Map<LensExtensionId, InstalledExtension> {
const extensions = this.extensions.toJS(); const extensions = this.extensions.toJS();
extensions.forEach((ext, extId) => { extensions.forEach((ext, extId) => {
@ -47,11 +34,45 @@ export class ExtensionLoader {
} }
@action @action
async init(extensions: Map<LensExtensionId, InstalledExtension>) { async init(extensions?: Map<LensExtensionId, InstalledExtension>) {
if (extensions) {
this.extensions.replace(extensions); this.extensions.replace(extensions);
}
if (ipcRenderer) {
this.initRenderer()
} else {
this.initMain()
}
extensionsStore.manageState(this);
}
protected async initMain() {
this.isLoaded = true; this.isLoaded = true;
this.loadOnMain(); this.loadOnMain();
this.broadcastExtensions(); this.broadcastExtensions();
reaction(() => this.extensions.toJS(), () => {
this.broadcastExtensions()
})
handleRequest(this.requestExtensionsChannel, () => {
return Array.from(this.toJSON())
})
}
protected async initRenderer() {
const extensionListHandler = ( extensions: [LensExtensionId, InstalledExtension][]) => {
this.isLoaded = true;
extensions.forEach(([extId, ext]) => {
if (!this.extensions.has(extId)) {
this.extensions.set(extId, ext)
}
})
}
requestMain(this.requestExtensionsChannel).then(extensionListHandler)
subscribeToBroadcast(this.requestExtensionsChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => {
extensionListHandler(extensions)
});
} }
loadOnMain() { loadOnMain() {
@ -140,16 +161,8 @@ export class ExtensionLoader {
}) })
} }
async broadcastExtensions(frameId?: number) { broadcastExtensions() {
await when(() => this.isLoaded); broadcastMessage(this.requestExtensionsChannel, Array.from(this.toJSON()))
broadcastIpc({
channel: "extensions:loaded",
frameId: frameId,
frameOnly: !!frameId,
args: [
Array.from(this.toJSON()),
],
})
} }
} }

View File

@ -30,14 +30,14 @@ jest.mock("tcp-port-used")
import { Cluster } from "../cluster" import { Cluster } from "../cluster"
import { KubeAuthProxy } from "../kube-auth-proxy" import { KubeAuthProxy } from "../kube-auth-proxy"
import { getFreePort } from "../port" import { getFreePort } from "../port"
import { broadcastIpc } from "../../common/ipc" import { broadcastMessage } from "../../common/ipc"
import { ChildProcess, spawn, SpawnOptions } from "child_process" import { ChildProcess, spawn, SpawnOptions } from "child_process"
import { bundledKubectlPath, Kubectl } from "../kubectl" import { bundledKubectlPath, Kubectl } from "../kubectl"
import { mock, MockProxy } from 'jest-mock-extended'; import { mock, MockProxy } from 'jest-mock-extended';
import { waitUntilUsed } from 'tcp-port-used'; import { waitUntilUsed } from 'tcp-port-used';
import { Readable } from "stream" import { Readable } from "stream"
const mockBroadcastIpc = broadcastIpc as jest.MockedFunction<typeof broadcastIpc> const mockBroadcastIpc = broadcastMessage as jest.MockedFunction<typeof broadcastMessage>
const mockSpawn = spawn as jest.MockedFunction<typeof spawn> const mockSpawn = spawn as jest.MockedFunction<typeof spawn>
const mockWaitUntilUsed = waitUntilUsed as jest.MockedFunction<typeof waitUntilUsed> const mockWaitUntilUsed = waitUntilUsed as jest.MockedFunction<typeof waitUntilUsed>
@ -95,35 +95,35 @@ describe("kube auth proxy tests", () => {
await proxy.run() await proxy.run()
listeners["error"]({ message: "foobarbat" }) listeners["error"]({ message: "foobarbat" })
expect(mockBroadcastIpc).toBeCalledWith({ channel: "kube-auth:foobar", args: [{ data: "foobarbat", error: true }] }) expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "foobarbat", error: true })
}) })
it("should call spawn and broadcast exit", async () => { it("should call spawn and broadcast exit", async () => {
await proxy.run() await proxy.run()
listeners["exit"](0) listeners["exit"](0)
expect(mockBroadcastIpc).toBeCalledWith({ channel: "kube-auth:foobar", args: [{ data: "proxy exited with code: 0", error: false }] }) expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "proxy exited with code: 0", error: false })
}) })
it("should call spawn and broadcast errors from stderr", async () => { it("should call spawn and broadcast errors from stderr", async () => {
await proxy.run() await proxy.run()
listeners["stderr/data"]("an error") listeners["stderr/data"]("an error")
expect(mockBroadcastIpc).toBeCalledWith({ channel: "kube-auth:foobar", args: [{ data: "an error", error: true }] }) expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "an error", error: true })
}) })
it("should call spawn and broadcast stdout serving info", async () => { it("should call spawn and broadcast stdout serving info", async () => {
await proxy.run() await proxy.run()
listeners["stdout/data"]("Starting to serve on") listeners["stdout/data"]("Starting to serve on")
expect(mockBroadcastIpc).toBeCalledWith({ channel: "kube-auth:foobar", args: [{ data: "Authentication proxy started\n" }] }) expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "Authentication proxy started\n" })
}) })
it("should call spawn and broadcast stdout other info", async () => { it("should call spawn and broadcast stdout other info", async () => {
await proxy.run() await proxy.run()
listeners["stdout/data"]("some info") listeners["stdout/data"]("some info")
expect(mockBroadcastIpc).toBeCalledWith({ channel: "kube-auth:foobar", args: [{ data: "some info" }] }) expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "some info" })
}) })
}) })
}) })

View File

@ -4,7 +4,7 @@ import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api";
import type { WorkspaceId } from "../common/workspace-store"; import type { WorkspaceId } from "../common/workspace-store";
import { action, computed, observable, reaction, toJS, when } from "mobx"; import { action, computed, observable, reaction, toJS, when } from "mobx";
import { apiKubePrefix } from "../common/vars"; import { apiKubePrefix } from "../common/vars";
import { broadcastIpc } from "../common/ipc"; import { broadcastMessage } from "../common/ipc";
import { ContextHandler } from "./context-handler" import { ContextHandler } from "./context-handler"
import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node" import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"
import { Kubectl } from "./kubectl"; import { Kubectl } from "./kubectl";
@ -50,7 +50,6 @@ export interface ClusterState {
export class Cluster implements ClusterModel, ClusterState { export class Cluster implements ClusterModel, ClusterState {
public id: ClusterId; public id: ClusterId;
public frameId: number;
public kubeCtl: Kubectl public kubeCtl: Kubectl
public contextHandler: ContextHandler; public contextHandler: ContextHandler;
public ownerRef: string; public ownerRef: string;
@ -406,11 +405,7 @@ export class Cluster implements ClusterModel, ClusterState {
pushState(state = this.getState()) { pushState(state = this.getState()) {
logger.silly(`[CLUSTER]: push-state`, state); logger.silly(`[CLUSTER]: push-state`, state);
broadcastIpc({ broadcastMessage("cluster:state", this.id, state)
channel: "cluster:state",
frameId: this.frameId,
args: [this.id, state],
})
} }
// get cluster system meta, e.g. use in "logger" // get cluster system meta, e.g. use in "logger"

View File

@ -1,6 +1,6 @@
import { ChildProcess, spawn } from "child_process" import { ChildProcess, spawn } from "child_process"
import { waitUntilUsed } from "tcp-port-used"; import { waitUntilUsed } from "tcp-port-used";
import { broadcastIpc } from "../common/ipc"; import { broadcastMessage } from "../common/ipc";
import type { Cluster } from "./cluster" import type { Cluster } from "./cluster"
import { Kubectl } from "./kubectl" import { Kubectl } from "./kubectl"
import logger from "./logger" import logger from "./logger"
@ -94,7 +94,7 @@ export class KubeAuthProxy {
protected async sendIpcLogMessage(res: KubeAuthProxyLog) { protected async sendIpcLogMessage(res: KubeAuthProxyLog) {
const channel = `kube-auth:${this.cluster.id}` const channel = `kube-auth:${this.cluster.id}`
logger.info(`[KUBE-AUTH]: out-channel "${channel}"`, { ...res, meta: this.cluster.getMeta() }); logger.info(`[KUBE-AUTH]: out-channel "${channel}"`, { ...res, meta: this.cluster.getMeta() });
broadcastIpc({ channel: channel, args: [res] }); broadcastMessage(channel, res)
} }
public exit() { public exit() {

View File

@ -1,13 +1,13 @@
import type { ClusterId } from "../common/cluster-store"; import type { ClusterId } from "../common/cluster-store";
import { clusterStore } from "../common/cluster-store";
import { observable } from "mobx"; import { observable } from "mobx";
import { app, BrowserWindow, dialog, ipcMain, shell, webContents } from "electron" import { app, BrowserWindow, dialog, shell, webContents } from "electron"
import windowStateKeeper from "electron-window-state" import windowStateKeeper from "electron-window-state"
import { extensionLoader } from "../extensions/extension-loader";
import { appEventBus } from "../common/event-bus" import { appEventBus } from "../common/event-bus"
import { subscribeToBroadcast } from "../common/ipc"
import { initMenu } from "./menu"; import { initMenu } from "./menu";
import { initTray } from "./tray"; import { initTray } from "./tray";
import { Singleton } from "../common/utils"; import { Singleton } from "../common/utils";
import { clusterFrameMap } from "../common/cluster-frames";
export class WindowManager extends Singleton { export class WindowManager extends Singleton {
protected mainWindow: BrowserWindow; protected mainWindow: BrowserWindow;
@ -63,7 +63,7 @@ export class WindowManager extends Singleton {
shell.openExternal(url); shell.openExternal(url);
}); });
this.mainWindow.webContents.on("dom-ready", () => { this.mainWindow.webContents.on("dom-ready", () => {
extensionLoader.broadcastExtensions() appEventBus.emit({name: "app", action: "dom-ready"})
}) })
this.mainWindow.on("focus", () => { this.mainWindow.on("focus", () => {
appEventBus.emit({name: "app", action: "focus"}) appEventBus.emit({name: "app", action: "focus"})
@ -101,9 +101,9 @@ export class WindowManager extends Singleton {
protected bindEvents() { protected bindEvents() {
// track visible cluster from ui // track visible cluster from ui
ipcMain.on("cluster-view:current-id", (event, clusterId: ClusterId) => { subscribeToBroadcast("cluster-view:current-id", (event, clusterId: ClusterId) => {
this.activeClusterId = clusterId; this.activeClusterId = clusterId;
}); })
} }
async ensureMainWindow(): Promise<BrowserWindow> { async ensureMainWindow(): Promise<BrowserWindow> {
@ -130,7 +130,7 @@ export class WindowManager extends Singleton {
} }
reload() { reload() {
const frameId = clusterStore.getById(this.activeClusterId)?.frameId; const frameId = clusterFrameMap.get(this.activeClusterId)
if (frameId) { if (frameId) {
this.sendToView({ channel: "renderer:reload", frameId }); this.sendToView({ channel: "renderer:reload", frameId });
} else { } else {

View File

@ -14,6 +14,7 @@ import { clusterStore } from "../common/cluster-store";
import { i18nStore } from "./i18n"; import { i18nStore } from "./i18n";
import { themeStore } from "./theme.store"; import { themeStore } from "./theme.store";
import { extensionsStore } from "../extensions/extensions-store"; import { extensionsStore } from "../extensions/extensions-store";
import { extensionLoader } from "../extensions/extension-loader";
type AppComponent = React.ComponentType & { type AppComponent = React.ComponentType & {
init?(): Promise<void>; init?(): Promise<void>;
@ -30,6 +31,8 @@ export async function bootstrap(App: AppComponent) {
const rootElem = document.getElementById("app") const rootElem = document.getElementById("app")
rootElem.classList.toggle("is-mac", isMac); rootElem.classList.toggle("is-mac", isMac);
extensionLoader.init()
// preload common stores // preload common stores
await Promise.all([ await Promise.all([
userStore.load(), userStore.load(),

View File

@ -12,8 +12,9 @@ import { Cluster } from "../../../main/cluster";
import { ClusterIcon } from "../cluster-icon"; import { ClusterIcon } from "../cluster-icon";
import { IClusterSettingsRouteParams } from "./cluster-settings.route"; import { IClusterSettingsRouteParams } from "./cluster-settings.route";
import { clusterStore } from "../../../common/cluster-store"; import { clusterStore } from "../../../common/cluster-store";
import { clusterIpc } from "../../../common/cluster-ipc";
import { PageLayout } from "../layout/page-layout"; import { PageLayout } from "../layout/page-layout";
import { requestMain } from "../../../common/ipc";
import { clusterActivateHandler, clusterRefreshHandler } from "../../../common/cluster-ipc"
interface Props extends RouteComponentProps<IClusterSettingsRouteParams> { interface Props extends RouteComponentProps<IClusterSettingsRouteParams> {
} }
@ -41,8 +42,8 @@ export class ClusterSettings extends React.Component<Props> {
refreshCluster = async () => { refreshCluster = async () => {
if (this.cluster) { if (this.cluster) {
await clusterIpc.activate.invokeFromRenderer(this.cluster.id); await requestMain(clusterActivateHandler, this.cluster.id)
await clusterIpc.refresh.invokeFromRenderer(this.cluster.id); await requestMain(clusterRefreshHandler, this.cluster.id)
} }
} }

View File

@ -1,7 +1,6 @@
import React from "react"; import React from "react";
import { Trans } from "@lingui/macro"; import { Trans } from "@lingui/macro";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { clusterIpc } from "../../../../common/cluster-ipc";
import { clusterStore } from "../../../../common/cluster-store"; import { clusterStore } from "../../../../common/cluster-store";
import { Cluster } from "../../../../main/cluster"; import { Cluster } from "../../../../main/cluster";
import { autobind } from "../../../utils"; import { autobind } from "../../../utils";

View File

@ -33,12 +33,13 @@ import { ErrorBoundary } from "./error-boundary";
import { Terminal } from "./dock/terminal"; import { Terminal } from "./dock/terminal";
import { getHostedCluster, getHostedClusterId } from "../../common/cluster-store"; import { getHostedCluster, getHostedClusterId } from "../../common/cluster-store";
import logger from "../../main/logger"; import logger from "../../main/logger";
import { clusterIpc } from "../../common/cluster-ipc";
import { webFrame } from "electron"; import { webFrame } from "electron";
import { clusterPageRegistry } from "../../extensions/registries/page-registry"; import { clusterPageRegistry } from "../../extensions/registries/page-registry";
import { extensionLoader } from "../../extensions/extension-loader"; import { extensionLoader } from "../../extensions/extension-loader";
import { appEventBus } from "../../common/event-bus"; import { appEventBus } from "../../common/event-bus"
import { requestMain } from "../../common/ipc";
import whatInput from 'what-input'; import whatInput from 'what-input';
import { clusterSetFrameIdHandler } from "../../common/cluster-ipc";
@observer @observer
export class App extends React.Component { export class App extends React.Component {
@ -48,7 +49,7 @@ export class App extends React.Component {
logger.info(`[APP]: Init dashboard, clusterId=${clusterId}, frameId=${frameId}`) logger.info(`[APP]: Init dashboard, clusterId=${clusterId}, frameId=${frameId}`)
await Terminal.preloadFonts() await Terminal.preloadFonts()
await clusterIpc.setFrameId.invokeFromRenderer(clusterId, frameId); await requestMain(clusterSetFrameIdHandler, clusterId, frameId)
await getHostedCluster().whenReady; // cluster.activate() is done at this point await getHostedCluster().whenReady; // cluster.activate() is done at this point
extensionLoader.loadOnClusterRenderer(); extensionLoader.loadOnClusterRenderer();
appEventBus.emit({ appEventBus.emit({

View File

@ -5,13 +5,14 @@ import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { ipcRenderer } from "electron"; import { ipcRenderer } from "electron";
import { computed, observable } from "mobx"; import { computed, observable } from "mobx";
import { clusterIpc } from "../../../common/cluster-ipc"; import { requestMain, subscribeToBroadcast } from "../../../common/ipc";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { Button } from "../button"; import { Button } from "../button";
import { cssNames, IClassName } from "../../utils"; import { cssNames, IClassName } from "../../utils";
import { Cluster } from "../../../main/cluster"; import { Cluster } from "../../../main/cluster";
import { ClusterId, clusterStore } from "../../../common/cluster-store"; import { ClusterId, clusterStore } from "../../../common/cluster-store";
import { CubeSpinner } from "../spinner"; import { CubeSpinner } from "../spinner";
import { clusterActivateHandler } from "../../../common/cluster-ipc";
interface Props { interface Props {
className?: IClassName; className?: IClassName;
@ -32,7 +33,7 @@ export class ClusterStatus extends React.Component<Props> {
} }
async componentDidMount() { async componentDidMount() {
ipcRenderer.on(`kube-auth:${this.cluster.id}`, (evt, res: KubeAuthProxyLog) => { subscribeToBroadcast(`kube-auth:${this.cluster.id}`, (evt, res: KubeAuthProxyLog) => {
this.authOutput.push({ this.authOutput.push({
data: res.data.trimRight(), data: res.data.trimRight(),
error: res.error, error: res.error,
@ -48,7 +49,7 @@ export class ClusterStatus extends React.Component<Props> {
} }
activateCluster = async (force = false) => { activateCluster = async (force = false) => {
await clusterIpc.activate.invokeFromRenderer(this.props.clusterId, force); await requestMain(clusterActivateHandler, this.props.clusterId, force)
} }
reconnect = async () => { reconnect = async () => {

View File

@ -2,6 +2,7 @@ import "./clusters-menu.scss"
import React from "react"; import React from "react";
import { remote } from "electron" import { remote } from "electron"
import { requestMain } from "../../../common/ipc";
import type { Cluster } from "../../../main/cluster"; import type { Cluster } from "../../../main/cluster";
import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd"; import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
@ -20,9 +21,9 @@ import { clusterSettingsURL } from "../+cluster-settings";
import { landingURL } from "../+landing-page"; import { landingURL } from "../+landing-page";
import { Tooltip } from "../tooltip"; import { Tooltip } from "../tooltip";
import { ConfirmDialog } from "../confirm-dialog"; import { ConfirmDialog } from "../confirm-dialog";
import { clusterIpc } from "../../../common/cluster-ipc";
import { clusterViewURL } from "./cluster-view.route"; import { clusterViewURL } from "./cluster-view.route";
import { getExtensionPageUrl, globalPageMenuRegistry, globalPageRegistry } from "../../../extensions/registries"; import { getExtensionPageUrl, globalPageMenuRegistry, globalPageRegistry } from "../../../extensions/registries";
import { clusterDisconnectHandler } from "../../../common/cluster-ipc";
interface Props { interface Props {
className?: IClassName; className?: IClassName;
@ -60,7 +61,7 @@ export class ClustersMenu extends React.Component<Props> {
navigate(landingURL()); navigate(landingURL());
clusterStore.setActive(null); clusterStore.setActive(null);
} }
await clusterIpc.disconnect.invokeFromRenderer(cluster.id); await requestMain(clusterDisconnectHandler, cluster.id)
} }
})) }))
} }

View File

@ -13,16 +13,17 @@ import { WhatsNew, whatsNewRoute } from "./components/+whats-new";
import { Notifications } from "./components/notifications"; import { Notifications } from "./components/notifications";
import { ConfirmDialog } from "./components/confirm-dialog"; import { ConfirmDialog } from "./components/confirm-dialog";
import { extensionLoader } from "../extensions/extension-loader"; import { extensionLoader } from "../extensions/extension-loader";
import { broadcastMessage } from "../common/ipc";
@observer @observer
export class LensApp extends React.Component { export class LensApp extends React.Component {
static async init() { static async init() {
extensionLoader.loadOnClusterManagerRenderer(); extensionLoader.loadOnClusterManagerRenderer();
window.addEventListener("offline", () => { window.addEventListener("offline", () => {
ipcRenderer.send("network:offline") broadcastMessage("network:offline")
}) })
window.addEventListener("online", () => { window.addEventListener("online", () => {
ipcRenderer.send("network:online") broadcastMessage("network:online")
}) })
} }

View File

@ -7,6 +7,7 @@ import { createObservableHistory } from "mobx-observable-history";
import { createBrowserHistory, LocationDescriptor } from "history"; import { createBrowserHistory, LocationDescriptor } from "history";
import logger from "../main/logger"; import logger from "../main/logger";
import { clusterViewRoute, IClusterViewRouteParams } from "./components/cluster-manager/cluster-view.route"; import { clusterViewRoute, IClusterViewRouteParams } from "./components/cluster-manager/cluster-view.route";
import { broadcastMessage, subscribeToBroadcast } from "../common/ipc";
export const history = createBrowserHistory(); export const history = createBrowserHistory();
export const navigation = createObservableHistory(history); export const navigation = createObservableHistory(history);
@ -111,19 +112,19 @@ export function getMatchedClusterId(): string {
if (process.isMainFrame) { if (process.isMainFrame) {
// Keep track of active cluster-id for handling IPC/menus/etc. // Keep track of active cluster-id for handling IPC/menus/etc.
reaction(() => getMatchedClusterId(), clusterId => { reaction(() => getMatchedClusterId(), clusterId => {
ipcRenderer.send("cluster-view:current-id", clusterId); broadcastMessage("cluster-view:current-id", clusterId)
}, { }, {
fireImmediately: true fireImmediately: true
}) })
} }
// Handle navigation via IPC (e.g. from top menu) // Handle navigation via IPC (e.g. from top menu)
ipcRenderer.on("renderer:navigate", (event, location: LocationDescriptor) => { subscribeToBroadcast("renderer:navigate", (event, location: LocationDescriptor) => {
logger.info(`[IPC]: ${event.type} ${JSON.stringify(location)}`, event); logger.info(`[IPC]: ${event.type} ${JSON.stringify(location)}`, event);
navigate(location); navigate(location);
}); })
// Reload dashboard window // Reload dashboard window
ipcRenderer.on("renderer:reload", () => { subscribeToBroadcast("renderer:reload", () => {
location.reload(); location.reload();
}); })