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

more work

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-02-22 09:20:01 -05:00
parent 3cf0538905
commit 170e19e7cd
7 changed files with 109 additions and 107 deletions

View File

@ -11,7 +11,7 @@ import { appEventBus } from "./event-bus";
import { dumpConfigYaml } from "./kube-helpers"; import { dumpConfigYaml } from "./kube-helpers";
import { saveToAppFiles } from "./utils/saveToAppFiles"; import { saveToAppFiles } from "./utils/saveToAppFiles";
import { KubeConfig } from "@kubernetes/client-node"; import { KubeConfig } from "@kubernetes/client-node";
import { handleRequest, requestMain, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc"; import { createTypedInvoker, isEmptyArgs, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc";
import _ from "lodash"; import _ from "lodash";
import move from "array-move"; import move from "array-move";
import type { WorkspaceId } from "./workspace-store"; import type { WorkspaceId } from "./workspace-store";
@ -90,6 +90,24 @@ export interface ClusterPrometheusPreferences {
}; };
} }
interface ClusterStateSync {
id: string;
state: ClusterState;
}
function ClusterStoreStateHandler(): ClusterStateSync[] {
return clusterStore.clustersList.map(cluster => ({
state: cluster.getState(),
id: cluster.id,
}));
}
const clusterStoreStateRequest = createTypedInvoker({
channel: "cluster:states",
handler: ClusterStoreStateHandler,
verifier: isEmptyArgs,
});
export class ClusterStore extends BaseStore<ClusterStoreModel> { export class ClusterStore extends BaseStore<ClusterStoreModel> {
static getCustomKubeConfigPath(clusterId: ClusterId): string { static getCustomKubeConfigPath(clusterId: ClusterId): string {
return path.resolve((app || remote.app).getPath("userData"), "kubeconfigs", clusterId); return path.resolve((app || remote.app).getPath("userData"), "kubeconfigs", clusterId);
@ -108,8 +126,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
@observable removedClusters = observable.map<ClusterId, Cluster>(); @observable removedClusters = observable.map<ClusterId, Cluster>();
@observable clusters = observable.map<ClusterId, Cluster>(); @observable clusters = observable.map<ClusterId, Cluster>();
private static stateRequestChannel = "cluster:states";
private constructor() { private constructor() {
super({ super({
configName: "lens-cluster-store", configName: "lens-cluster-store",
@ -125,35 +141,13 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
async load() { async load() {
await super.load(); await super.load();
type clusterStateSync = {
id: string;
state: ClusterState;
};
if (ipcRenderer) { if (ipcRenderer) {
logger.info("[CLUSTER-STORE] requesting initial state sync"); logger.info("[CLUSTER-STORE] requesting initial state sync");
const clusterStates: clusterStateSync[] = await requestMain(ClusterStore.stateRequestChannel);
clusterStates.forEach((clusterState) => { for (const { id, state } of await clusterStoreStateRequest.invoke()) {
const cluster = this.getById(clusterState.id); this.getById(id)?.setState(state);
if (cluster) {
cluster.setState(clusterState.state);
} }
});
} else {
handleRequest(ClusterStore.stateRequestChannel, (): clusterStateSync[] => {
const states: clusterStateSync[] = [];
this.clustersList.forEach((cluster) => {
states.push({
state: cluster.getState(),
id: cluster.id
});
});
return states;
});
} }
} }

View File

@ -6,8 +6,13 @@ import { ipcMain, ipcRenderer, webContents, remote } from "electron";
import { toJS } from "mobx"; import { toJS } from "mobx";
import logger from "../../main/logger"; import logger from "../../main/logger";
import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames"; import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames";
import { createTypedInvoker, isEmptyArgs } from "./type-enforced-ipc";
const subFramesChannel = "ipc:get-sub-frames"; const subFrames = createTypedInvoker({
channel: "ipc:get-sub-frames",
handler: getSubFrames,
verifier: isEmptyArgs,
});
export function handleRequest(channel: string, listener: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any) { export function handleRequest(channel: string, listener: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any) {
ipcMain.handle(channel, listener); ipcMain.handle(channel, listener);
@ -39,11 +44,11 @@ export async function broadcastMessage(channel: string, ...args: any[]) {
view.send(channel, ...args); view.send(channel, ...args);
try { try {
const subFrames: ClusterFrameInfo[] = ipcRenderer const childFrames: ClusterFrameInfo[] = ipcRenderer
? await requestMain(subFramesChannel) ? await subFrames.invoke()
: getSubFrames(); : getSubFrames();
for (const frameInfo of subFrames) { for (const frameInfo of childFrames) {
view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args); view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args);
} }
} catch (error) { } catch (error) {
@ -77,9 +82,3 @@ export function unsubscribeAllFromBroadcast(channel: string) {
ipcMain.removeAllListeners(channel); ipcMain.removeAllListeners(channel);
} }
} }
export function bindBroadcastHandlers() {
handleRequest(subFramesChannel, () => {
return getSubFrames();
});
}

View File

@ -8,6 +8,10 @@ export type ListVerifier<T extends any[]> = (args: unknown[]) => args is T;
export type Rest<T> = T extends [any, ...infer R] ? R : []; export type Rest<T> = T extends [any, ...infer R] ? R : [];
export type IpcListener<E extends Event, Args extends any[]> = (e: E, ...args: Args) => void; export type IpcListener<E extends Event, Args extends any[]> = (e: E, ...args: Args) => void;
export function isEmptyArgs(args: unknown[]): args is [] {
return args.length === 0;
}
/** /**
* Adds a listener to `source` that waits for the first IPC message with the correct * Adds a listener to `source` that waits for the first IPC message with the correct
* argument data is sent. * argument data is sent.
@ -210,7 +214,7 @@ export function createTypedSender<
source: ipcMain ?? ipcRenderer, source: ipcMain ?? ipcRenderer,
channel, channel,
listener, listener,
verifier: verifier as any, // safety: this verifier is correct, TS just doesn't equate Rest<..> correctly verifier: verifier as ListVerifier<Rest<[e: Event, ...args: Args]>>,
}); });
}, },
once(listener) { once(listener) {
@ -218,7 +222,7 @@ export function createTypedSender<
source: ipcMain ?? ipcRenderer, source: ipcMain ?? ipcRenderer,
channel, channel,
listener, listener,
verifier: verifier as any, // safety: this verifier is correct, TS just doesn't equate Rest<..> correctly verifier: verifier as ListVerifier<Rest<[e: Event, ...args: Args]>>,
}); });
} }
}; };

View File

@ -107,8 +107,14 @@ export function isNull(val: unknown): val is null {
* This is useful for when using `hasOptionalProperty` and `hasTypedProperty` * This is useful for when using `hasOptionalProperty` and `hasTypedProperty`
* @param fn A typescript user predicate function to be bound * @param fn A typescript user predicate function to be bound
* @param boundArgs the set of arguments to be passed to `fn` in the new function * @param boundArgs the set of arguments to be passed to `fn` in the new function
*
* Example:
* ```
* bindTypeGuard(isTypedArray, isString); // Predicate<string[]>
* bindTypeGuard(isRecord, isString, isBoolean); // Predicate<Record<string, boolean>>
* ```
*/ */
export function bindTypeGuard<FnArgs extends any[], T>(fn: (arg1: unknown, ...args: FnArgs) => arg1 is T, ...boundArgs: FnArgs): (arg1: unknown) => arg1 is T { export function bindTypeGuard<FnArgs extends any[], T>(fn: (arg1: unknown, ...args: FnArgs) => arg1 is T, ...boundArgs: FnArgs): Predicate<T> {
return (arg1: unknown): arg1 is T => fn(arg1, ...boundArgs); return (arg1: unknown): arg1 is T => fn(arg1, ...boundArgs);
} }
@ -122,6 +128,11 @@ type OrReturnPredicateType<T extends Predicate<any>[]> = ReturnPredicateType<Fir
* Create a new type-guard for the union of the types that each of the * Create a new type-guard for the union of the types that each of the
* predicates are type-guarding for * predicates are type-guarding for
* @param predicates a list of predicates that should be executed in order * @param predicates a list of predicates that should be executed in order
*
* Example:
* ```
* createUnionGuard(isString, isBoolean); // Predicate<string | boolean>
* ```
*/ */
export function createUnionGuard<Predicates extends Predicate<any>[]>(...predicates: Predicates): Predicate<OrReturnPredicateType<Predicates>> { export function createUnionGuard<Predicates extends Predicate<any>[]>(...predicates: Predicates): Predicate<OrReturnPredicateType<Predicates>> {
return (arg: unknown): arg is OrReturnPredicateType<Predicates> => { return (arg: unknown): arg is OrReturnPredicateType<Predicates> => {

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 { broadcastMessage, handleRequest, requestMain } from "../common/ipc"; import { broadcastMessage, createTypedInvoker, isEmptyArgs } from "../common/ipc";
import logger from "../main/logger"; import logger from "../main/logger";
import type { ClusterId } from "./cluster-store"; import type { ClusterId } from "./cluster-store";
@ -141,9 +141,26 @@ export class Workspace implements WorkspaceModel, WorkspaceState {
} }
} }
interface WorkspaceStateSync {
id: string;
state: WorkspaceState;
}
function WorkspaceStoreStateHandler(): WorkspaceStateSync[] {
return clusterStore.clustersList.map(cluster => ({
state: cluster.getState(),
id: cluster.id,
}));
}
const workspaceStoreStateRequest = createTypedInvoker({
channel: "workspace:states",
handler: WorkspaceStoreStateHandler,
verifier: isEmptyArgs,
});
export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> { export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
static readonly defaultId: WorkspaceId = "default"; static readonly defaultId: WorkspaceId = "default";
private static stateRequestChannel = "workspace:states";
@observable currentWorkspaceId = WorkspaceStore.defaultId; @observable currentWorkspaceId = WorkspaceStore.defaultId;
@observable workspaces = observable.map<WorkspaceId, Workspace>(); @observable workspaces = observable.map<WorkspaceId, Workspace>();
@ -161,35 +178,13 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
async load() { async load() {
await super.load(); await super.load();
type workspaceStateSync = {
id: string;
state: WorkspaceState;
};
if (ipcRenderer) { if (ipcRenderer) {
logger.info("[WORKSPACE-STORE] requesting initial state sync"); logger.info("[WORKSPACE-STORE] requesting initial state sync");
const workspaceStates: workspaceStateSync[] = await requestMain(WorkspaceStore.stateRequestChannel);
workspaceStates.forEach((workspaceState) => { for (const { id, state } of await workspaceStoreStateRequest.invoke()) {
const workspace = this.getById(workspaceState.id); this.getById(id)?.setState(state);
if (workspace) {
workspace.setState(workspaceState.state);
} }
});
} else {
handleRequest(WorkspaceStore.stateRequestChannel, (): workspaceStateSync[] => {
const states: workspaceStateSync[] = [];
this.workspacesList.forEach((workspace) => {
states.push({
state: workspace.getState(),
id: workspace.id
});
});
return states;
});
} }
} }

View File

@ -5,8 +5,9 @@ import fs from "fs-extra";
import { observable, reaction, toJS, when } from "mobx"; import { observable, reaction, toJS, when } from "mobx";
import os from "os"; import os from "os";
import path from "path"; import path from "path";
import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc"; import { createTypedInvoker, createTypedSender, isEmptyArgs } from "../common/ipc";
import { getBundledExtensions } from "../common/utils/app-version"; import { getBundledExtensions } from "../common/utils/app-version";
import { hasTypedProperty, isBoolean } from "../common/utils/type-narrowing";
import logger from "../main/logger"; import logger from "../main/logger";
import { extensionInstaller, PackageJson } from "./extension-installer"; import { extensionInstaller, PackageJson } from "./extension-installer";
import { extensionsStore } from "./extensions-store"; import { extensionsStore } from "./extensions-store";
@ -31,15 +32,35 @@ const logModule = "[EXTENSION-DISCOVERY]";
export const manifestFilename = "package.json"; export const manifestFilename = "package.json";
interface ExtensionDiscoveryChannelMessage { type DiscoveryLoadingState = [isLoaded: boolean];
isLoaded: boolean;
function isExtensionDiscoveryChannelMessage(args: unknown[]): args is DiscoveryLoadingState {
return hasTypedProperty(args, 0, isBoolean)
&& args.length === 0;
} }
/** /**
* Returns true if the lstat is for a directory-like file (e.g. isDirectory or symbolic link) * Returns true if the lstat is for a directory-like file (e.g. isDirectory or symbolic link)
* @param lstat the stats to compare * @param lstat the stats to compare
*/ */
const isDirectoryLike = (lstat: fs.Stats) => lstat.isDirectory() || lstat.isSymbolicLink(); function isDirectoryLike(lstat: fs.Stats): boolean {
return lstat.isDirectory() || lstat.isSymbolicLink();
}
const extensionDiscoveryState = createTypedSender({
channel: "extension-discovery:state",
verifier: isExtensionDiscoveryChannelMessage,
});
function ExtensionDiscoveryInitState(): boolean {
return extensionDiscovery.isLoaded;
}
const extensionDiscoveryInitState = createTypedInvoker({
channel: "extension-discovery:init-state",
handler: ExtensionDiscoveryInitState,
verifier: isEmptyArgs,
});
/** /**
* Discovers installed bundled and local extensions from the filesystem. * Discovers installed bundled and local extensions from the filesystem.
@ -61,9 +82,6 @@ export class ExtensionDiscovery {
@observable isLoaded = false; @observable isLoaded = false;
whenLoaded = when(() => this.isLoaded); whenLoaded = when(() => this.isLoaded);
// IPC channel to broadcast changes to extension-discovery from main
protected static readonly extensionDiscoveryChannel = "extension-discovery:main";
public events: EventEmitter; public events: EventEmitter;
constructor() { constructor() {
@ -95,29 +113,16 @@ export class ExtensionDiscovery {
*/ */
async init() { async init() {
if (ipcRenderer) { if (ipcRenderer) {
await this.initRenderer(); extensionDiscoveryState.on((event, isLoaded) => {
} else {
await this.initMain();
}
}
async initRenderer() {
const onMessage = ({ isLoaded }: ExtensionDiscoveryChannelMessage) => {
this.isLoaded = isLoaded; this.isLoaded = isLoaded;
}; });
requestMain(ExtensionDiscovery.extensionDiscoveryChannel).then(onMessage); this.isLoaded = await extensionDiscoveryInitState.invoke();
subscribeToBroadcast(ExtensionDiscovery.extensionDiscoveryChannel, (_event, message: ExtensionDiscoveryChannelMessage) => { } else {
onMessage(message); reaction(() => this.toJSON(), loadingState => {
extensionDiscoveryState.broadcast(...loadingState);
}); });
} }
async initMain() {
handleRequest(ExtensionDiscovery.extensionDiscoveryChannel, () => this.toJSON());
reaction(() => this.toJSON(), () => {
this.broadcast();
});
} }
/** /**
@ -450,17 +455,13 @@ export class ExtensionDiscovery {
return this.getByManifest(manifestPath, { isBundled }); return this.getByManifest(manifestPath, { isBundled });
} }
toJSON(): ExtensionDiscoveryChannelMessage { toJSON(): DiscoveryLoadingState {
return toJS({ return toJS([
isLoaded: this.isLoaded this.isLoaded
}, { ], {
recurseEverything: true recurseEverything: true
}); });
} }
broadcast() {
broadcastMessage(ExtensionDiscovery.extensionDiscoveryChannel, this.toJSON());
}
} }
export const extensionDiscovery = new ExtensionDiscovery(); export const extensionDiscovery = new ExtensionDiscovery();

View File

@ -26,8 +26,8 @@ import type { LensExtensionId } from "../extensions/lens-extension";
import { installDeveloperTools } from "./developer-tools"; import { installDeveloperTools } from "./developer-tools";
import { filesystemProvisionerStore } from "./extension-filesystem"; import { filesystemProvisionerStore } from "./extension-filesystem";
import { getAppVersion, getAppVersionFromProxyServer } from "../common/utils"; import { getAppVersion, getAppVersionFromProxyServer } from "../common/utils";
import { bindBroadcastHandlers } from "../common/ipc";
import { startUpdateChecking } from "./app-updater"; import { startUpdateChecking } from "./app-updater";
import "../common/ipc"; // make sure that the handlers are registered
const workingDir = path.join(app.getPath("appData"), appName); const workingDir = path.join(app.getPath("appData"), appName);
let proxyPort: number; let proxyPort: number;
@ -66,8 +66,6 @@ app.on("ready", async () => {
logger.info("🐚 Syncing shell environment"); logger.info("🐚 Syncing shell environment");
await shellSync(); await shellSync();
bindBroadcastHandlers();
powerMonitor.on("shutdown", () => { powerMonitor.on("shutdown", () => {
app.exit(); app.exit();
}); });