diff --git a/src/common/base-store.ts b/src/common/base-store.ts index b2ba812b0a..17cc8d08e1 100644 --- a/src/common/base-store.ts +++ b/src/common/base-store.ts @@ -6,7 +6,7 @@ import { action, IReactionOptions, observable, reaction, runInAction, toJS, when import Singleton from "./utils/singleton"; import { getAppVersion } from "./utils/app-version"; import logger from "../main/logger"; -import { broadcastIpc, IpcBroadcastParams } from "./ipc"; +import { broadcastMessage, subscribeToBroadcast, unsubscribeFromBroadcast } from "./ipc"; import isEqual from "lodash/isEqual"; export interface BaseStoreParams extends ConfOptions { @@ -37,12 +37,16 @@ export class BaseStore extends Singleton { return path.basename(this.storeConfig.path); } - get path() { - return this.storeConfig.path; + protected get syncRendererChannel() { + return `store-sync-renderer:${this.path}` } - get syncChannel() { - return `STORE-SYNC:${this.path}` + protected get syncMainChannel() { + return `store-sync-main:${this.path}` + } + + get path() { + return this.storeConfig.path; } protected async init() { @@ -89,16 +93,16 @@ export class BaseStore extends Singleton { logger.silly(`[STORE]: SYNC ${this.name} from renderer`, { model }); this.onSync(model); }; - ipcMain.on(this.syncChannel, callback); - this.syncDisposers.push(() => ipcMain.off(this.syncChannel, callback)); + subscribeToBroadcast(this.syncMainChannel, callback) + this.syncDisposers.push(() => unsubscribeFromBroadcast(this.syncMainChannel, callback)); } if (ipcRenderer) { const callback = (event: IpcRendererEvent, model: T) => { logger.silly(`[STORE]: SYNC ${this.name} from main`, { model }); this.onSyncFromMain(model); }; - ipcRenderer.on(this.syncChannel, callback); - this.syncDisposers.push(() => ipcRenderer.off(this.syncChannel, callback)); + subscribeToBroadcast(this.syncRendererChannel, callback) + this.syncDisposers.push(() => unsubscribeFromBroadcast(this.syncRendererChannel, callback)); } } @@ -109,7 +113,8 @@ export class BaseStore extends Singleton { } unregisterIpcListener() { - ipcRenderer.removeAllListeners(this.syncChannel) + ipcRenderer.removeAllListeners(this.syncMainChannel) + ipcRenderer.removeAllListeners(this.syncRendererChannel) } disableSync() { @@ -135,41 +140,10 @@ export class BaseStore extends Singleton { protected async onModelChange(model: T) { if (ipcMain) { 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 { - const subFrames: number[] = []; - const { clusterStore } = await import("./cluster-store"); - clusterStore.clustersList.forEach(cluster => { - if (cluster.frameId) { - subFrames.push(cluster.frameId) - } - }); - return subFrames; } @action diff --git a/src/common/cluster-frames.ts b/src/common/cluster-frames.ts new file mode 100644 index 0000000000..af950b0785 --- /dev/null +++ b/src/common/cluster-frames.ts @@ -0,0 +1,3 @@ +import { observable } from "mobx" + +export const clusterFrameMap = observable.map(); diff --git a/src/common/cluster-ipc.ts b/src/common/cluster-ipc.ts index 69a3ebf80f..c2204bc6b4 100644 --- a/src/common/cluster-ipc.ts +++ b/src/common/cluster-ipc.ts @@ -1,59 +1,55 @@ -import { createIpcChannel } from "./ipc"; +import { handleRequest } from "./ipc"; import { ClusterId, clusterStore } from "./cluster-store"; -import { extensionLoader } from "../extensions/extension-loader" import { appEventBus } from "./event-bus" import { ResourceApplier } from "../main/resource-applier"; +import { ipcMain } from "electron"; +import { clusterFrameMap } from "./cluster-frames" -export const clusterIpc = { - activate: createIpcChannel({ - channel: "cluster:activate", - handle: (clusterId: ClusterId, force = false) => { - const cluster = clusterStore.getById(clusterId); - if (cluster) { - return cluster.activate(force); - } - }, - }), +export const clusterActivateHandler = "cluster:activate" +export const clusterSetFrameIdHandler = "cluster:set-frame-id" +export const clusterRefreshHandler = "cluster:refresh" +export const clusterDisconnectHandler = "cluster:disconnect" +export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all" - setFrameId: createIpcChannel({ - channel: "cluster:set-frame-id", - handle: (clusterId: ClusterId, frameId?: number) => { - const cluster = clusterStore.getById(clusterId); - if (cluster) { - if (frameId) cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates - extensionLoader.broadcastExtensions(frameId) - return cluster.pushState(); - } - }, - }), - refresh: createIpcChannel({ - channel: "cluster:refresh", - handle: (clusterId: ClusterId) => { - const cluster = clusterStore.getById(clusterId); - if (cluster) return cluster.refresh({ refreshMetadata: true }) - }, - }), - - disconnect: createIpcChannel({ - channel: "cluster:disconnect", - handle: (clusterId: ClusterId) => { - appEventBus.emit({name: "cluster", action: "stop"}); - return clusterStore.getById(clusterId)?.disconnect(); - }, - }), - - kubectlApplyAll: createIpcChannel({ - channel: "cluster:kubectl-apply-all", - handle: (clusterId: ClusterId, resources: string[]) => { - appEventBus.emit({name: "cluster", action: "kubectl-apply-all"}) - const cluster = clusterStore.getById(clusterId); - if (cluster) { - const applier = new ResourceApplier(cluster) - applier.kubectlApplyAll(resources) - } else { - throw `${clusterId} is not a valid cluster id`; - } +if (ipcMain) { + handleRequest(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => { + const cluster = clusterStore.getById(clusterId); + if (cluster) { + return cluster.activate(force); } - }), + }) + + handleRequest(clusterSetFrameIdHandler, (event, clusterId: ClusterId, frameId: number) => { + const cluster = clusterStore.getById(clusterId); + if (cluster) { + clusterFrameMap.set(cluster.id, frameId) + return cluster.pushState(); + } + }) + + handleRequest(clusterRefreshHandler, (event, clusterId: ClusterId) => { + const cluster = clusterStore.getById(clusterId); + if (cluster) return cluster.refresh({ refreshMetadata: true }) + }) + + handleRequest(clusterDisconnectHandler, (event, clusterId: ClusterId) => { + appEventBus.emit({name: "cluster", action: "stop"}); + const cluster = clusterStore.getById(clusterId); + if (cluster) { + cluster.disconnect(); + clusterFrameMap.delete(cluster.id) + } + }) + + handleRequest(clusterKubectlApplyAllHandler, (event, clusterId: ClusterId, resources: string[]) => { + appEventBus.emit({name: "cluster", action: "kubectl-apply-all"}) + const cluster = clusterStore.getById(clusterId); + if (cluster) { + const applier = new ResourceApplier(cluster) + applier.kubectlApplyAll(resources) + } else { + throw `${clusterId} is not a valid cluster id`; + } + }) } diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index 7ea92b612a..da99ce439f 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -2,7 +2,7 @@ import type { WorkspaceId } from "./workspace-store"; import path from "path"; import { app, ipcRenderer, remote, webFrame } from "electron"; 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 { Cluster, ClusterState } from "../main/cluster"; import migrations from "../migrations/cluster-store" @@ -13,6 +13,7 @@ import { saveToAppFiles } from "./utils/saveToAppFiles"; import { KubeConfig } from "@kubernetes/client-node"; import _ from "lodash"; import move from "array-move"; +import { subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc"; export interface ClusterIconUpload { clusterId: string; @@ -85,21 +86,20 @@ export class ClusterStore extends BaseStore { migrations: migrations, }); - this.pushStateToViewsPeriodically() + this.pushStateToViewsAutomatically() } - protected pushStateToViewsPeriodically() { + protected pushStateToViewsAutomatically() { 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 - setInterval(() => { + reaction(() => this.connectedClustersList, () => { this.pushState() - }, 5000) + }) } } registerIpcListener() { 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); this.getById(clusterId)?.setState(state) }) @@ -107,7 +107,7 @@ export class ClusterStore extends BaseStore { unregisterIpcListener() { super.unregisterIpcListener() - ipcRenderer.removeAllListeners("cluster:state") + unsubscribeAllFromBroadcast("cluster:state") } pushState() { @@ -132,6 +132,10 @@ export class ClusterStore extends BaseStore { return this.getById(this.activeCluster); } + @computed get connectedClustersList(): Cluster[] { + return this.clustersList.filter((c) => !c.disconnected) + } + isActive(id: ClusterId) { return this.activeCluster === id; } diff --git a/src/common/ipc.ts b/src/common/ipc.ts index 97c4dd05cd..882498a3a4 100644 --- a/src/common/ipc.ts +++ b/src/common/ipc.ts @@ -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-renderer -import { ipcMain, ipcRenderer, WebContents, webContents } from "electron" +import { ipcMain, ipcRenderer, webContents, remote } from "electron"; import logger from "../main/logger"; +import { clusterFrameMap } from "./cluster-frames"; -export type IpcChannel = string; - -export interface IpcChannelOptions { - channel: IpcChannel; // main <-> renderer communication channel name - handle?: (...args: any[]) => Promise | 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 handleRequest(channel: string, listener: (...args: any[]) => any) { + ipcMain.handle(channel, listener) } -export function createIpcChannel({ autoBind = true, once, timeout = 0, handle, channel }: IpcChannelOptions) { - const ipcChannel = { - 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 (...args: any[]): Promise => { - return ipcRenderer.invoke(channel, ...args); - }, - } - if (autoBind && ipcMain) { - ipcChannel.handleInMain(); - } - return ipcChannel; +export async function requestMain(channel: string, ...args: any[]) { + return ipcRenderer.invoke(channel, ...args) } -export interface IpcBroadcastParams { - channel: IpcChannel - webContentId?: number; // send to single webContents view - frameId?: number; // send to inner frame of webContents - frameOnly?: boolean; // send message only to view with provided `frameId` - filter?: (webContent: WebContents) => boolean - timeout?: number; // todo: add support - args?: A; +async function getSubFrames(): Promise { + const subFrames: number[] = []; + clusterFrameMap.forEach((frameId, _) => { + subFrames.push(frameId) + }); + return subFrames; } -export function broadcastIpc({ channel, frameId, frameOnly, webContentId, filter, args = [] }: IpcBroadcastParams) { - const singleView = webContentId ? webContents.fromId(webContentId) : null; - let views = singleView ? [singleView] : webContents.getAllWebContents(); - if (filter) { - views = views.filter(filter); - } +export function broadcastMessage(channel: string, ...args: any[]) { + const views = (webContents || remote?.webContents)?.getAllWebContents(); + if (!views) return + views.forEach(webContent => { const type = webContent.getType(); logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${webContent.id}`, { args }); - if (!frameOnly) { - webContent.send(channel, ...args); - } - if (frameId) { - webContent.sendToFrame(frameId, channel, ...args) - } + webContent.send(channel, ...args); + getSubFrames().then((frames) => { + frames.map((frameId) => { + 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) + } } diff --git a/src/common/workspace-store.ts b/src/common/workspace-store.ts index 70f8d08fd2..baae6287ab 100644 --- a/src/common/workspace-store.ts +++ b/src/common/workspace-store.ts @@ -3,7 +3,7 @@ import { action, computed, observable, toJS, reaction } from "mobx"; import { BaseStore } from "./base-store"; import { clusterStore } from "./cluster-store" import { appEventBus } from "./event-bus"; -import { broadcastIpc } from "../common/ipc"; +import { broadcastMessage } from "../common/ipc"; import logger from "../main/logger"; export type WorkspaceId = string; @@ -53,10 +53,7 @@ export class Workspace implements WorkspaceModel, WorkspaceState { pushState(state = this.getState()) { logger.silly("[WORKSPACE] pushing state", {...state, id: this.id}) - broadcastIpc({ - channel: "workspace:state", - args: [this.id, toJS(state)], - }); + broadcastMessage("workspace:state", this.id, toJS(state)) } @action diff --git a/src/extensions/cluster-feature.ts b/src/extensions/cluster-feature.ts index 08904750e2..90cbd3a0a9 100644 --- a/src/extensions/cluster-feature.ts +++ b/src/extensions/cluster-feature.ts @@ -6,7 +6,8 @@ import { ResourceApplier } from "../main/resource-applier" import { Cluster } from "../main/cluster"; import logger from "../main/logger"; import { app } from "electron" -import { clusterIpc } from "../common/cluster-ipc" +import { requestMain } from "../common/ipc"; +import { clusterKubectlApplyAllHandler } from "../common/cluster-ipc"; export interface ClusterFeatureStatus { currentVersion: string; @@ -39,7 +40,7 @@ export abstract class ClusterFeature { if (app) { await new ResourceApplier(cluster).kubectlApplyAll(resources) } else { - await clusterIpc.kubectlApplyAll.invokeFromRenderer(cluster.id, resources) + await requestMain(clusterKubectlApplyAllHandler, cluster.id, resources) } } diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index 969cff33b1..c5339f31c9 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -3,7 +3,7 @@ import type { LensMainExtension } from "./lens-main-extension" import type { LensRendererExtension } from "./lens-renderer-extension" import type { InstalledExtension } from "./extension-manager"; 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 logger from "../main/logger" import { app, ipcRenderer, remote } from "electron" @@ -18,24 +18,11 @@ export function extensionPackagesRoot() { export class ExtensionLoader { protected extensions = observable.map(); protected instances = observable.map(); + protected readonly requestExtensionsChannel = "extensions:loaded" @observable isLoaded = false; 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 { const extensions = this.extensions.toJS(); extensions.forEach((ext, extId) => { @@ -47,11 +34,45 @@ export class ExtensionLoader { } @action - async init(extensions: Map) { - this.extensions.replace(extensions); + async init(extensions?: Map) { + if (extensions) { + this.extensions.replace(extensions); + } + if (ipcRenderer) { + this.initRenderer() + } else { + this.initMain() + } + extensionsStore.manageState(this); + } + + protected async initMain() { this.isLoaded = true; this.loadOnMain(); 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() { @@ -140,16 +161,8 @@ export class ExtensionLoader { }) } - async broadcastExtensions(frameId?: number) { - await when(() => this.isLoaded); - broadcastIpc({ - channel: "extensions:loaded", - frameId: frameId, - frameOnly: !!frameId, - args: [ - Array.from(this.toJSON()), - ], - }) + broadcastExtensions() { + broadcastMessage(this.requestExtensionsChannel, Array.from(this.toJSON())) } } diff --git a/src/main/__test__/kube-auth-proxy.test.ts b/src/main/__test__/kube-auth-proxy.test.ts index dead980f82..8ef1f058e7 100644 --- a/src/main/__test__/kube-auth-proxy.test.ts +++ b/src/main/__test__/kube-auth-proxy.test.ts @@ -30,14 +30,14 @@ jest.mock("tcp-port-used") import { Cluster } from "../cluster" import { KubeAuthProxy } from "../kube-auth-proxy" import { getFreePort } from "../port" -import { broadcastIpc } from "../../common/ipc" +import { broadcastMessage } from "../../common/ipc" import { ChildProcess, spawn, SpawnOptions } from "child_process" import { bundledKubectlPath, Kubectl } from "../kubectl" import { mock, MockProxy } from 'jest-mock-extended'; import { waitUntilUsed } from 'tcp-port-used'; import { Readable } from "stream" -const mockBroadcastIpc = broadcastIpc as jest.MockedFunction +const mockBroadcastIpc = broadcastMessage as jest.MockedFunction const mockSpawn = spawn as jest.MockedFunction const mockWaitUntilUsed = waitUntilUsed as jest.MockedFunction @@ -95,35 +95,35 @@ describe("kube auth proxy tests", () => { await proxy.run() 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 () => { await proxy.run() 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 () => { await proxy.run() 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 () => { await proxy.run() 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 () => { await proxy.run() 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" }) }) }) }) diff --git a/src/main/cluster.ts b/src/main/cluster.ts index b808f6d997..ecb6e6db9c 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -4,7 +4,7 @@ import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api"; import type { WorkspaceId } from "../common/workspace-store"; import { action, computed, observable, reaction, toJS, when } from "mobx"; import { apiKubePrefix } from "../common/vars"; -import { broadcastIpc } from "../common/ipc"; +import { broadcastMessage } from "../common/ipc"; import { ContextHandler } from "./context-handler" import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node" import { Kubectl } from "./kubectl"; @@ -50,7 +50,6 @@ export interface ClusterState { export class Cluster implements ClusterModel, ClusterState { public id: ClusterId; - public frameId: number; public kubeCtl: Kubectl public contextHandler: ContextHandler; public ownerRef: string; @@ -406,11 +405,7 @@ export class Cluster implements ClusterModel, ClusterState { pushState(state = this.getState()) { logger.silly(`[CLUSTER]: push-state`, state); - broadcastIpc({ - channel: "cluster:state", - frameId: this.frameId, - args: [this.id, state], - }) + broadcastMessage("cluster:state", this.id, state) } // get cluster system meta, e.g. use in "logger" diff --git a/src/main/kube-auth-proxy.ts b/src/main/kube-auth-proxy.ts index 0bea0e720a..5377ecb829 100644 --- a/src/main/kube-auth-proxy.ts +++ b/src/main/kube-auth-proxy.ts @@ -1,6 +1,6 @@ import { ChildProcess, spawn } from "child_process" import { waitUntilUsed } from "tcp-port-used"; -import { broadcastIpc } from "../common/ipc"; +import { broadcastMessage } from "../common/ipc"; import type { Cluster } from "./cluster" import { Kubectl } from "./kubectl" import logger from "./logger" @@ -94,7 +94,7 @@ export class KubeAuthProxy { protected async sendIpcLogMessage(res: KubeAuthProxyLog) { const channel = `kube-auth:${this.cluster.id}` logger.info(`[KUBE-AUTH]: out-channel "${channel}"`, { ...res, meta: this.cluster.getMeta() }); - broadcastIpc({ channel: channel, args: [res] }); + broadcastMessage(channel, res) } public exit() { diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index 034e14b61a..019ed270eb 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -1,13 +1,13 @@ import type { ClusterId } from "../common/cluster-store"; -import { clusterStore } from "../common/cluster-store"; 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 { extensionLoader } from "../extensions/extension-loader"; import { appEventBus } from "../common/event-bus" +import { subscribeToBroadcast } from "../common/ipc" import { initMenu } from "./menu"; import { initTray } from "./tray"; import { Singleton } from "../common/utils"; +import { clusterFrameMap } from "../common/cluster-frames"; export class WindowManager extends Singleton { protected mainWindow: BrowserWindow; @@ -63,7 +63,7 @@ export class WindowManager extends Singleton { shell.openExternal(url); }); this.mainWindow.webContents.on("dom-ready", () => { - extensionLoader.broadcastExtensions() + appEventBus.emit({name: "app", action: "dom-ready"}) }) this.mainWindow.on("focus", () => { appEventBus.emit({name: "app", action: "focus"}) @@ -101,9 +101,9 @@ export class WindowManager extends Singleton { protected bindEvents() { // 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; - }); + }) } async ensureMainWindow(): Promise { @@ -130,7 +130,7 @@ export class WindowManager extends Singleton { } reload() { - const frameId = clusterStore.getById(this.activeClusterId)?.frameId; + const frameId = clusterFrameMap.get(this.activeClusterId) if (frameId) { this.sendToView({ channel: "renderer:reload", frameId }); } else { diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 52a92b7c11..cfadb5c379 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -14,6 +14,7 @@ import { clusterStore } from "../common/cluster-store"; import { i18nStore } from "./i18n"; import { themeStore } from "./theme.store"; import { extensionsStore } from "../extensions/extensions-store"; +import { extensionLoader } from "../extensions/extension-loader"; type AppComponent = React.ComponentType & { init?(): Promise; @@ -30,6 +31,8 @@ export async function bootstrap(App: AppComponent) { const rootElem = document.getElementById("app") rootElem.classList.toggle("is-mac", isMac); + extensionLoader.init() + // preload common stores await Promise.all([ userStore.load(), diff --git a/src/renderer/components/+cluster-settings/cluster-settings.tsx b/src/renderer/components/+cluster-settings/cluster-settings.tsx index fc6cca3cce..55f4bb5ec4 100644 --- a/src/renderer/components/+cluster-settings/cluster-settings.tsx +++ b/src/renderer/components/+cluster-settings/cluster-settings.tsx @@ -12,8 +12,9 @@ import { Cluster } from "../../../main/cluster"; import { ClusterIcon } from "../cluster-icon"; import { IClusterSettingsRouteParams } from "./cluster-settings.route"; import { clusterStore } from "../../../common/cluster-store"; -import { clusterIpc } from "../../../common/cluster-ipc"; import { PageLayout } from "../layout/page-layout"; +import { requestMain } from "../../../common/ipc"; +import { clusterActivateHandler, clusterRefreshHandler } from "../../../common/cluster-ipc" interface Props extends RouteComponentProps { } @@ -41,8 +42,8 @@ export class ClusterSettings extends React.Component { refreshCluster = async () => { if (this.cluster) { - await clusterIpc.activate.invokeFromRenderer(this.cluster.id); - await clusterIpc.refresh.invokeFromRenderer(this.cluster.id); + await requestMain(clusterActivateHandler, this.cluster.id) + await requestMain(clusterRefreshHandler, this.cluster.id) } } diff --git a/src/renderer/components/+cluster-settings/components/remove-cluster-button.tsx b/src/renderer/components/+cluster-settings/components/remove-cluster-button.tsx index dda3e7c137..f407ee383a 100644 --- a/src/renderer/components/+cluster-settings/components/remove-cluster-button.tsx +++ b/src/renderer/components/+cluster-settings/components/remove-cluster-button.tsx @@ -1,7 +1,6 @@ import React from "react"; import { Trans } from "@lingui/macro"; import { observer } from "mobx-react"; -import { clusterIpc } from "../../../../common/cluster-ipc"; import { clusterStore } from "../../../../common/cluster-store"; import { Cluster } from "../../../../main/cluster"; import { autobind } from "../../../utils"; diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index 57f2095c97..f05ec8a7a8 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -33,12 +33,13 @@ import { ErrorBoundary } from "./error-boundary"; import { Terminal } from "./dock/terminal"; import { getHostedCluster, getHostedClusterId } from "../../common/cluster-store"; import logger from "../../main/logger"; -import { clusterIpc } from "../../common/cluster-ipc"; import { webFrame } from "electron"; import { clusterPageRegistry } from "../../extensions/registries/page-registry"; 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 { clusterSetFrameIdHandler } from "../../common/cluster-ipc"; @observer 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}`) await Terminal.preloadFonts() - await clusterIpc.setFrameId.invokeFromRenderer(clusterId, frameId); + await requestMain(clusterSetFrameIdHandler, clusterId, frameId) await getHostedCluster().whenReady; // cluster.activate() is done at this point extensionLoader.loadOnClusterRenderer(); appEventBus.emit({ diff --git a/src/renderer/components/cluster-manager/cluster-status.tsx b/src/renderer/components/cluster-manager/cluster-status.tsx index 5a488c4405..f8d76395bc 100644 --- a/src/renderer/components/cluster-manager/cluster-status.tsx +++ b/src/renderer/components/cluster-manager/cluster-status.tsx @@ -5,13 +5,14 @@ import React from "react"; import { observer } from "mobx-react"; import { ipcRenderer } from "electron"; import { computed, observable } from "mobx"; -import { clusterIpc } from "../../../common/cluster-ipc"; +import { requestMain, subscribeToBroadcast } from "../../../common/ipc"; import { Icon } from "../icon"; import { Button } from "../button"; import { cssNames, IClassName } from "../../utils"; import { Cluster } from "../../../main/cluster"; import { ClusterId, clusterStore } from "../../../common/cluster-store"; import { CubeSpinner } from "../spinner"; +import { clusterActivateHandler } from "../../../common/cluster-ipc"; interface Props { className?: IClassName; @@ -32,7 +33,7 @@ export class ClusterStatus extends React.Component { } async componentDidMount() { - ipcRenderer.on(`kube-auth:${this.cluster.id}`, (evt, res: KubeAuthProxyLog) => { + subscribeToBroadcast(`kube-auth:${this.cluster.id}`, (evt, res: KubeAuthProxyLog) => { this.authOutput.push({ data: res.data.trimRight(), error: res.error, @@ -48,7 +49,7 @@ export class ClusterStatus extends React.Component { } activateCluster = async (force = false) => { - await clusterIpc.activate.invokeFromRenderer(this.props.clusterId, force); + await requestMain(clusterActivateHandler, this.props.clusterId, force) } reconnect = async () => { diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index 1fb818a812..f3b390a198 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -2,6 +2,7 @@ import "./clusters-menu.scss" import React from "react"; import { remote } from "electron" +import { requestMain } from "../../../common/ipc"; import type { Cluster } from "../../../main/cluster"; import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd"; import { observer } from "mobx-react"; @@ -20,9 +21,9 @@ import { clusterSettingsURL } from "../+cluster-settings"; import { landingURL } from "../+landing-page"; import { Tooltip } from "../tooltip"; import { ConfirmDialog } from "../confirm-dialog"; -import { clusterIpc } from "../../../common/cluster-ipc"; import { clusterViewURL } from "./cluster-view.route"; import { getExtensionPageUrl, globalPageMenuRegistry, globalPageRegistry } from "../../../extensions/registries"; +import { clusterDisconnectHandler } from "../../../common/cluster-ipc"; interface Props { className?: IClassName; @@ -60,7 +61,7 @@ export class ClustersMenu extends React.Component { navigate(landingURL()); clusterStore.setActive(null); } - await clusterIpc.disconnect.invokeFromRenderer(cluster.id); + await requestMain(clusterDisconnectHandler, cluster.id) } })) } diff --git a/src/renderer/lens-app.tsx b/src/renderer/lens-app.tsx index 5c1b27b13a..4ed92f7ff8 100644 --- a/src/renderer/lens-app.tsx +++ b/src/renderer/lens-app.tsx @@ -13,16 +13,17 @@ import { WhatsNew, whatsNewRoute } from "./components/+whats-new"; import { Notifications } from "./components/notifications"; import { ConfirmDialog } from "./components/confirm-dialog"; import { extensionLoader } from "../extensions/extension-loader"; +import { broadcastMessage } from "../common/ipc"; @observer export class LensApp extends React.Component { static async init() { extensionLoader.loadOnClusterManagerRenderer(); window.addEventListener("offline", () => { - ipcRenderer.send("network:offline") + broadcastMessage("network:offline") }) window.addEventListener("online", () => { - ipcRenderer.send("network:online") + broadcastMessage("network:online") }) } diff --git a/src/renderer/navigation.ts b/src/renderer/navigation.ts index 70073d2ff2..af4b52739f 100644 --- a/src/renderer/navigation.ts +++ b/src/renderer/navigation.ts @@ -7,6 +7,7 @@ import { createObservableHistory } from "mobx-observable-history"; import { createBrowserHistory, LocationDescriptor } from "history"; import logger from "../main/logger"; import { clusterViewRoute, IClusterViewRouteParams } from "./components/cluster-manager/cluster-view.route"; +import { broadcastMessage, subscribeToBroadcast } from "../common/ipc"; export const history = createBrowserHistory(); export const navigation = createObservableHistory(history); @@ -111,19 +112,19 @@ export function getMatchedClusterId(): string { if (process.isMainFrame) { // Keep track of active cluster-id for handling IPC/menus/etc. reaction(() => getMatchedClusterId(), clusterId => { - ipcRenderer.send("cluster-view:current-id", clusterId); + broadcastMessage("cluster-view:current-id", clusterId) }, { fireImmediately: true }) } // 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); navigate(location); -}); +}) // Reload dashboard window -ipcRenderer.on("renderer:reload", () => { +subscribeToBroadcast("renderer:reload", () => { location.reload(); -}); +})