From c2a359295b3474dba54ad820ad8e48fd77625191 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 1 Feb 2022 10:43:06 -0500 Subject: [PATCH] Fix extensions not being able to be installed in some cases - Specifically, when an empty folder exists with the name that would be used to install it - Make extensions and IPC more injected, so that ExtensionInstallationStateStore can be removed - Add test to cover bug Signed-off-by: Sebastian Malton --- src/common/__tests__/base-store.test.ts | 2 +- src/common/__tests__/cluster-store.test.ts | 2 +- src/common/__tests__/hotbar-store.test.ts | 2 +- src/common/__tests__/user-store.test.ts | 2 +- .../app-path-channel-injection-token.ts | 12 + .../app-paths/app-path-injection-token.ts | 9 +- src/common/app-paths/app-paths.test.ts | 27 +- .../{app-path-names.ts => app-paths.ts} | 1 + .../directory-for-binaries.injectable.ts | 3 +- .../directory-for-kube-configs.injectable.ts | 3 +- .../communication/broadcast.injectable.ts | 13 + src/common/communication/channel.ts | 18 ++ src/common/communication/emitter.ts | 10 + .../ipc-on-event-injection-token.ts | 10 + .../register-emitter.injectable.ts | 33 +++ .../register-event-sink.injectable.ts | 33 +++ src/common/fs/remove-dir.injectable.ts | 15 ++ src/common/ipc-channel/channel.ts | 8 - .../create-channel/create-channel.ts | 10 - src/common/logger.ts | 11 +- src/common/logger/base-logger.injectable.ts | 13 + .../logger/create-child-logger.injectable.ts | 33 +++ src/common/utils/unique-id.injectable.ts | 13 + .../__tests__/extension-loader.test.ts | 2 +- .../extension-discovery.injectable.ts | 35 +-- .../extension-discovery.test.ts | 11 +- .../extension-discovery.ts | 11 +- ...ion-installation-state-store.injectable.ts | 13 - .../extension-installation-state-store.ts | 244 ------------------ .../get-installed-extension.injectable.ts | 17 ++ .../installation-state/logger.injectable.ts | 17 ++ .../installation-state/state-channels.ts | 16 ++ src/extensions/installation-state/state.ts | 13 + .../__tests__/page-registry.test.ts | 2 +- src/main/__test__/cluster.test.ts | 2 +- src/main/__test__/context-handler.test.ts | 2 +- src/main/__test__/kube-auth-proxy.test.ts | 2 +- src/main/__test__/kubeconfig-manager.test.ts | 2 +- .../app-paths/app-paths-channel.injectable.ts | 26 ++ src/main/app-paths/app-paths.injectable.ts | 63 ++--- src/main/app-paths/get-app-paths.ts | 10 +- .../get-electron-app-path.test.ts | 4 +- .../get-electron-app-path.ts | 2 +- .../register-channel.injectable.ts | 17 -- .../register-channel/register-channel.ts | 18 -- .../set-electron-app-path.injectable.ts | 11 +- .../__test__/kubeconfig-sync.test.ts | 2 +- .../communication/ipc-handle.injectable.ts | 20 ++ .../ipc-main.injectable.ts | 0 src/main/communication/ipc-on.injectable.ts | 22 ++ .../register-channel.injectable.ts | 31 +++ .../clear-installing-channel.injectable.ts | 21 ++ .../set-installing-channel.injectable.ts | 21 ++ src/main/getDiForUnitTesting.ts | 37 ++- src/main/ipc/emit-event.ts | 19 ++ src/main/lens-binary.ts | 6 +- src/main/menu/electron-menu-items.test.ts | 2 +- .../protocol-handler/__test__/router.test.ts | 2 +- src/main/tray/tray-menu-items.test.ts | 2 +- .../app-paths/app-paths-channel.injectable.ts | 19 ++ .../app-paths/app-paths.injectable.ts | 14 +- ...alue-from-registered-channel.injectable.ts | 16 -- .../get-value-from-registered-channel.ts | 17 -- src/renderer/bootstrap.tsx | 5 - .../communication/ipc-invoke.injectable.ts | 17 ++ .../communication/ipc-on.injectable.ts | 22 ++ .../ipc-renderer.injectable.ts | 0 .../register-channel.injectable.ts | 27 ++ .../+catalog/__tests__/custom-columns.test.ts | 4 +- .../+catalog/__tests__/custom-views.test.ts | 4 +- .../components/+catalog/catalog.test.tsx | 2 +- .../__tests__/attempt-install.test.ts | 51 ++++ .../+extensions/__tests__/extensions.test.tsx | 2 +- .../attempt-install-by-info.injectable.ts | 5 +- .../attempt-install-by-info.tsx | 21 +- .../attempt-install.injectable.ts | 14 +- .../attempt-install/attempt-install.tsx | 80 ++---- .../unpack-extension.injectable.tsx | 10 +- .../unpack-extension/unpack-extension.tsx | 145 +++++------ .../attempt-installs.injectable.ts | 45 ++++ .../attempt-installs.injectable.ts | 18 -- .../attempt-installs/attempt-installs.ts | 28 -- .../components/+extensions/extensions.tsx | 31 +-- .../install-from-input.injectable.tsx | 72 ++++++ .../install-from-input.injectable.ts | 23 -- .../install-from-input/install-from-input.tsx | 53 ---- ...tall-from-select-file-dialog.injectable.ts | 31 ++- .../+extensions/install-on-drop.injectable.ts | 28 ++ .../install-on-drop.injectable.ts | 18 -- .../install-on-drop/install-on-drop.tsx | 16 -- .../components/+extensions/install.tsx | 132 +++++----- .../+extensions/installed-extensions.tsx | 150 +++++------ .../uninstall-extension.injectable.ts | 10 +- .../uninstall-extension.tsx | 72 +++--- .../__tests__/dialog.test.tsx | 2 +- .../+role-bindings/__tests__/dialog.test.tsx | 2 +- .../+welcome/__test__/welcome.test.tsx | 2 +- .../__tests__/pod-tolerations.test.tsx | 2 +- .../__tests__/delete-cluster-dialog.test.tsx | 2 +- .../dock/__test__/dock-tabs.test.tsx | 2 +- .../__test__/log-resource-selector.test.tsx | 2 +- .../dock/logs/__test__/log-search.test.tsx | 2 +- .../__tests__/hotbar-remove-command.test.tsx | 4 +- .../kube-object-menu.test.tsx | 2 +- .../layout/top-bar/top-bar-win-linux.test.tsx | 2 +- .../layout/top-bar/top-bar.test.tsx | 2 +- .../components/status-bar/status-bar.test.tsx | 2 +- .../any-installing.injectable.ts | 18 ++ .../any-pre-installing.injectable.ts | 18 ++ .../any-uninstalling.injectable.ts | 18 ++ .../clear-installing.injectable.ts | 60 +++++ .../clear-uninstalling.injectable.ts | 48 ++++ .../get-installation-state.injectable.ts | 38 +++ .../installing.injectable.ts | 13 + .../is-currently-idle.injectable.ts | 26 ++ .../is-installing.injectable.ts | 17 ++ .../is-uninstalling.injectable.ts | 17 ++ .../pre-installing.injectable.ts | 13 + .../set-installing.injectable.ts | 59 +++++ .../set-uninstalling.injectable.ts | 47 ++++ .../start-pre-install.injectable.ts | 48 ++++ .../uninstalling.injectable.ts | 13 + .../init-root-frame.injectable.ts | 2 +- src/renderer/getDiForUnitTesting.tsx | 54 ++-- .../search-store/search-store.test.ts | 2 +- src/test-utils/get-dis-for-unit-testing.ts | 23 +- src/test-utils/override-fs-functions.ts | 32 +++ src/test-utils/override-ipc-bridge.ts | 85 +++--- 128 files changed, 1728 insertions(+), 1161 deletions(-) create mode 100644 src/common/app-paths/app-path-channel-injection-token.ts rename src/common/app-paths/{app-path-names.ts => app-paths.ts} (91%) create mode 100644 src/common/communication/broadcast.injectable.ts create mode 100644 src/common/communication/channel.ts create mode 100644 src/common/communication/emitter.ts create mode 100644 src/common/communication/ipc-on-event-injection-token.ts create mode 100644 src/common/communication/register-emitter.injectable.ts create mode 100644 src/common/communication/register-event-sink.injectable.ts create mode 100644 src/common/fs/remove-dir.injectable.ts delete mode 100644 src/common/ipc-channel/channel.ts delete mode 100644 src/common/ipc-channel/create-channel/create-channel.ts create mode 100644 src/common/logger/base-logger.injectable.ts create mode 100644 src/common/logger/create-child-logger.injectable.ts create mode 100644 src/common/utils/unique-id.injectable.ts delete mode 100644 src/extensions/extension-installation-state-store/extension-installation-state-store.injectable.ts delete mode 100644 src/extensions/extension-installation-state-store/extension-installation-state-store.ts create mode 100644 src/extensions/extension-loader/get-installed-extension.injectable.ts create mode 100644 src/extensions/installation-state/logger.injectable.ts create mode 100644 src/extensions/installation-state/state-channels.ts create mode 100644 src/extensions/installation-state/state.ts create mode 100644 src/main/app-paths/app-paths-channel.injectable.ts delete mode 100644 src/main/app-paths/register-channel/register-channel.injectable.ts delete mode 100644 src/main/app-paths/register-channel/register-channel.ts create mode 100644 src/main/communication/ipc-handle.injectable.ts rename src/main/{app-paths/register-channel/ipc-main => communication}/ipc-main.injectable.ts (100%) create mode 100644 src/main/communication/ipc-on.injectable.ts create mode 100644 src/main/communication/register-channel.injectable.ts create mode 100644 src/main/extensions/installation-state/clear-installing-channel.injectable.ts create mode 100644 src/main/extensions/installation-state/set-installing-channel.injectable.ts create mode 100644 src/main/ipc/emit-event.ts create mode 100644 src/renderer/app-paths/app-paths-channel.injectable.ts delete mode 100644 src/renderer/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.injectable.ts delete mode 100644 src/renderer/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.ts create mode 100644 src/renderer/communication/ipc-invoke.injectable.ts create mode 100644 src/renderer/communication/ipc-on.injectable.ts rename src/renderer/{app-paths/get-value-from-registered-channel/ipc-renderer => communication}/ipc-renderer.injectable.ts (100%) create mode 100644 src/renderer/communication/register-channel.injectable.ts create mode 100644 src/renderer/components/+extensions/__tests__/attempt-install.test.ts create mode 100644 src/renderer/components/+extensions/attempt-installs.injectable.ts delete mode 100644 src/renderer/components/+extensions/attempt-installs/attempt-installs.injectable.ts delete mode 100644 src/renderer/components/+extensions/attempt-installs/attempt-installs.ts create mode 100644 src/renderer/components/+extensions/install-from-input.injectable.tsx delete mode 100644 src/renderer/components/+extensions/install-from-input/install-from-input.injectable.ts delete mode 100644 src/renderer/components/+extensions/install-from-input/install-from-input.tsx create mode 100644 src/renderer/components/+extensions/install-on-drop.injectable.ts delete mode 100644 src/renderer/components/+extensions/install-on-drop/install-on-drop.injectable.ts delete mode 100644 src/renderer/components/+extensions/install-on-drop/install-on-drop.tsx create mode 100644 src/renderer/extensions/installation-state/any-installing.injectable.ts create mode 100644 src/renderer/extensions/installation-state/any-pre-installing.injectable.ts create mode 100644 src/renderer/extensions/installation-state/any-uninstalling.injectable.ts create mode 100644 src/renderer/extensions/installation-state/clear-installing.injectable.ts create mode 100644 src/renderer/extensions/installation-state/clear-uninstalling.injectable.ts create mode 100644 src/renderer/extensions/installation-state/get-installation-state.injectable.ts create mode 100644 src/renderer/extensions/installation-state/installing.injectable.ts create mode 100644 src/renderer/extensions/installation-state/is-currently-idle.injectable.ts create mode 100644 src/renderer/extensions/installation-state/is-installing.injectable.ts create mode 100644 src/renderer/extensions/installation-state/is-uninstalling.injectable.ts create mode 100644 src/renderer/extensions/installation-state/pre-installing.injectable.ts create mode 100644 src/renderer/extensions/installation-state/set-installing.injectable.ts create mode 100644 src/renderer/extensions/installation-state/set-uninstalling.injectable.ts create mode 100644 src/renderer/extensions/installation-state/start-pre-install.injectable.ts create mode 100644 src/renderer/extensions/installation-state/uninstalling.injectable.ts create mode 100644 src/test-utils/override-fs-functions.ts diff --git a/src/common/__tests__/base-store.test.ts b/src/common/__tests__/base-store.test.ts index f35ed7721f..8fcd14d49b 100644 --- a/src/common/__tests__/base-store.test.ts +++ b/src/common/__tests__/base-store.test.ts @@ -78,7 +78,7 @@ describe("BaseStore", () => { let store: TestStore; beforeEach(async () => { - const dis = getDisForUnitTesting({ doGeneralOverrides: true }); + const dis = await getDisForUnitTesting({ doGeneralOverrides: true }); dis.mainDi.override(directoryForUserDataInjectable, () => "some-user-data-directory"); diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index f11bd2d593..8e145a0de5 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -80,7 +80,7 @@ describe("cluster-store", () => { let createCluster: (model: ClusterModel) => Cluster; beforeEach(async () => { - const dis = getDisForUnitTesting({ doGeneralOverrides: true }); + const dis = await getDisForUnitTesting({ doGeneralOverrides: true }); mockFs(); diff --git a/src/common/__tests__/hotbar-store.test.ts b/src/common/__tests__/hotbar-store.test.ts index b9fc9d1e99..7c1c288474 100644 --- a/src/common/__tests__/hotbar-store.test.ts +++ b/src/common/__tests__/hotbar-store.test.ts @@ -113,7 +113,7 @@ const awsCluster = getMockCatalogEntity({ describe("HotbarStore", () => { beforeEach(async () => { - const di = getDiForUnitTesting({ doGeneralOverrides: true }); + const di = await getDiForUnitTesting({ doGeneralOverrides: true }); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); diff --git a/src/common/__tests__/user-store.test.ts b/src/common/__tests__/user-store.test.ts index 9201b25bd7..568eb2491c 100644 --- a/src/common/__tests__/user-store.test.ts +++ b/src/common/__tests__/user-store.test.ts @@ -40,7 +40,7 @@ describe("user store tests", () => { let mainDi: DependencyInjectionContainer; beforeEach(async () => { - const dis = getDisForUnitTesting({ doGeneralOverrides: true }); + const dis = await getDisForUnitTesting({ doGeneralOverrides: true }); mockFs(); diff --git a/src/common/app-paths/app-path-channel-injection-token.ts b/src/common/app-paths/app-path-channel-injection-token.ts new file mode 100644 index 0000000000..3d75fc86c4 --- /dev/null +++ b/src/common/app-paths/app-path-channel-injection-token.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { AppPaths } from "./app-paths"; +import type { Channel } from "../communication/channel"; + +export type AppPathsChannel = Channel<[], AppPaths>; + +export const appPathsInjectionChannelToken = getInjectionToken(); +export const appPathsIpcChannel = "app-paths"; diff --git a/src/common/app-paths/app-path-injection-token.ts b/src/common/app-paths/app-path-injection-token.ts index 9a50acd1a7..f008a47e91 100644 --- a/src/common/app-paths/app-path-injection-token.ts +++ b/src/common/app-paths/app-path-injection-token.ts @@ -3,13 +3,6 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectionToken } from "@ogre-tools/injectable"; -import type { PathName } from "./app-path-names"; -import { createChannel } from "../ipc-channel/create-channel/create-channel"; - -export type AppPaths = Record; +import type { AppPaths } from "./app-paths"; export const appPathsInjectionToken = getInjectionToken(); - -export const appPathsIpcChannel = createChannel("app-paths"); - - diff --git a/src/common/app-paths/app-paths.test.ts b/src/common/app-paths/app-paths.test.ts index e7652024e0..93c2ffd302 100644 --- a/src/common/app-paths/app-paths.test.ts +++ b/src/common/app-paths/app-paths.test.ts @@ -3,22 +3,22 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import type { DependencyInjectionContainer } from "@ogre-tools/injectable"; -import { AppPaths, appPathsInjectionToken } from "./app-path-injection-token"; import getElectronAppPathInjectable from "../../main/app-paths/get-electron-app-path/get-electron-app-path.injectable"; import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing"; -import type { PathName } from "./app-path-names"; +import type { AppPaths, PathName } from "./app-paths"; import setElectronAppPathInjectable from "../../main/app-paths/set-electron-app-path/set-electron-app-path.injectable"; import appNameInjectable from "../../main/app-paths/app-name/app-name.injectable"; import directoryForIntegrationTestingInjectable from "../../main/app-paths/directory-for-integration-testing/directory-for-integration-testing.injectable"; import path from "path"; +import { appPathsInjectionToken } from "./app-path-injection-token"; describe("app-paths", () => { let mainDi: DependencyInjectionContainer; let rendererDi: DependencyInjectionContainer; - let runSetups: () => Promise; + let runSetups: () => Promise; - beforeEach(() => { - const dis = getDisForUnitTesting({ doGeneralOverrides: true }); + beforeEach(async () => { + const dis = await getDisForUnitTesting({ doGeneralOverrides: true }); mainDi = dis.mainDi; rendererDi = dis.rendererDi; @@ -45,17 +45,14 @@ describe("app-paths", () => { mainDi.override( getElectronAppPathInjectable, - () => - (key: PathName): string | null => - defaultAppPathsStub[key], + () => (key: PathName): string | null => defaultAppPathsStub[key], ); mainDi.override( setElectronAppPathInjectable, - () => - (key: PathName, path: string): void => { - defaultAppPathsStub[key] = path; - }, + () => (key: PathName, path: string): void => { + defaultAppPathsStub[key] = path; + }, ); mainDi.override(appNameInjectable, () => "some-app-name"); @@ -123,7 +120,7 @@ describe("app-paths", () => { await runSetups(); }); - it("given in renderer, when injecting path for app data, has integration specific app data path", () => { + it("when in renderer, when injecting path for app data, has integration specific app data path", () => { const { appData, userData } = rendererDi.inject(appPathsInjectionToken); expect({ appData, userData }).toEqual({ @@ -132,8 +129,8 @@ describe("app-paths", () => { }); }); - it("given in main, when injecting path for app data, has integration specific app data path", () => { - const { appData, userData } = rendererDi.inject(appPathsInjectionToken); + it("when in main, when injecting path for app data, has integration specific app data path", () => { + const { appData, userData } = mainDi.inject(appPathsInjectionToken); expect({ appData, userData }).toEqual({ appData: "some-integration-testing-app-data", diff --git a/src/common/app-paths/app-path-names.ts b/src/common/app-paths/app-paths.ts similarity index 91% rename from src/common/app-paths/app-path-names.ts rename to src/common/app-paths/app-paths.ts index 211901dd7a..485bdfe01b 100644 --- a/src/common/app-paths/app-path-names.ts +++ b/src/common/app-paths/app-paths.ts @@ -4,6 +4,7 @@ */ import type { app as electronApp } from "electron"; +export type AppPaths = Record; export type PathName = Parameters[0]; export const pathNames: PathName[] = [ diff --git a/src/common/app-paths/directory-for-binaries/directory-for-binaries.injectable.ts b/src/common/app-paths/directory-for-binaries/directory-for-binaries.injectable.ts index 1857942fa8..1eb8bf32e5 100644 --- a/src/common/app-paths/directory-for-binaries/directory-for-binaries.injectable.ts +++ b/src/common/app-paths/directory-for-binaries/directory-for-binaries.injectable.ts @@ -7,8 +7,7 @@ import path from "path"; import directoryForUserDataInjectable from "../directory-for-user-data/directory-for-user-data.injectable"; const directoryForBinariesInjectable = getInjectable({ - instantiate: (di) => - path.join(di.inject(directoryForUserDataInjectable), "binaries"), + instantiate: (di) => path.join(di.inject(directoryForUserDataInjectable), "binaries"), lifecycle: lifecycleEnum.singleton, }); diff --git a/src/common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable.ts b/src/common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable.ts index 7e4865197d..f2648b677f 100644 --- a/src/common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable.ts +++ b/src/common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable.ts @@ -7,8 +7,7 @@ import directoryForUserDataInjectable from "../directory-for-user-data/directory import path from "path"; const directoryForKubeConfigsInjectable = getInjectable({ - instantiate: (di) => - path.resolve(di.inject(directoryForUserDataInjectable), "kubeconfigs"), + instantiate: (di) => path.resolve(di.inject(directoryForUserDataInjectable), "kubeconfigs"), lifecycle: lifecycleEnum.singleton, }); diff --git a/src/common/communication/broadcast.injectable.ts b/src/common/communication/broadcast.injectable.ts new file mode 100644 index 0000000000..232614d7ac --- /dev/null +++ b/src/common/communication/broadcast.injectable.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { broadcastMessage } from "../ipc"; + +const broadcastInjectable = getInjectable({ + instantiate: () => broadcastMessage as (channel: string, ...args: any[]) => void, + lifecycle: lifecycleEnum.singleton, +}); + +export default broadcastInjectable; diff --git a/src/common/communication/channel.ts b/src/common/communication/channel.ts new file mode 100644 index 0000000000..5360109732 --- /dev/null +++ b/src/common/communication/channel.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +/** + * A Channel represent an link that renderer can request on, given some + * parameters, and get a value back + */ +export type Channel = (...args: Parameters) => Promise; + +export type ChannelValue = T extends Channel + ? Value + : never; + +export type ChannelParameters = T extends Channel + ? Parameters + : never; diff --git a/src/common/communication/emitter.ts b/src/common/communication/emitter.ts new file mode 100644 index 0000000000..dfbaa3ed59 --- /dev/null +++ b/src/common/communication/emitter.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +/** + * An EmitterChannel represents a broadcast point where any side can emit data + * on + */ +export type EmitterChannel = (...args: Parameters) => void; diff --git a/src/common/communication/ipc-on-event-injection-token.ts b/src/common/communication/ipc-on-event-injection-token.ts new file mode 100644 index 0000000000..627caf8be9 --- /dev/null +++ b/src/common/communication/ipc-on-event-injection-token.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getInjectionToken } from "@ogre-tools/injectable"; + +export type IpcOnEvent = (channel: string, ...args: any[]) => void; + +export const ipcOnEventInjectionToken = getInjectionToken(); diff --git a/src/common/communication/register-emitter.injectable.ts b/src/common/communication/register-emitter.injectable.ts new file mode 100644 index 0000000000..f580c81e86 --- /dev/null +++ b/src/common/communication/register-emitter.injectable.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { LensLogger } from "../logger"; +import broadcastInjectable from "./broadcast.injectable"; +import type { EmitterChannel } from "./emitter"; + +interface Dependencies { + broadcast: (name: string, ...args: any[]) => void; +} + +function registerEmitterChannel({ broadcast }: Dependencies) { + return function (name: string, logger?: LensLogger): EmitterChannel { + return (...args) => { + logger?.info(`Broadcasting on ${name}`, { args }); + broadcast(name, ...args); + }; + }; +} + +/** + * This dependency is for registering the source of events + */ +const registerEmitterChannelInjectable = getInjectable({ + instantiate: (di) => registerEmitterChannel({ + broadcast: di.inject(broadcastInjectable), + }), + lifecycle: lifecycleEnum.singleton, +}); + +export default registerEmitterChannelInjectable; diff --git a/src/common/communication/register-event-sink.injectable.ts b/src/common/communication/register-event-sink.injectable.ts new file mode 100644 index 0000000000..3352f768d1 --- /dev/null +++ b/src/common/communication/register-event-sink.injectable.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { EmitterChannel } from "../../common/communication/emitter"; +import type { LensLogger } from "../../common/logger"; +import { ipcOnEventInjectionToken } from "./ipc-on-event-injection-token"; + +interface Depencencies { + onEvent: (channel: string, ...args: any[]) => void; +} + +const registerEventSink = ({ onEvent }: Depencencies) => ( + function (name: string, listener: (...args: Parameters) => void, logger?: LensLogger): EmitterChannel { + onEvent(name, (...args: Parameters) => { + logger?.info(`Received event on ${name}`, { args }); + listener(...args); + }); + + return listener; + } +); + +const registerEventSinkInjectable = getInjectable({ + instantiate: (di) => registerEventSink({ + onEvent: di.inject(ipcOnEventInjectionToken), + }), + lifecycle: lifecycleEnum.singleton, +}); + +export default registerEventSinkInjectable; diff --git a/src/common/fs/remove-dir.injectable.ts b/src/common/fs/remove-dir.injectable.ts new file mode 100644 index 0000000000..37d7ac69d9 --- /dev/null +++ b/src/common/fs/remove-dir.injectable.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import fsInjectable from "./fs.injectable"; + +export type RemoveDir = (dir: string) => Promise; + +const removeDirInjectable = getInjectable({ + instantiate: (di) => di.inject(fsInjectable).remove as RemoveDir, + lifecycle: lifecycleEnum.singleton, +}); + +export default removeDirInjectable; diff --git a/src/common/ipc-channel/channel.ts b/src/common/ipc-channel/channel.ts deleted file mode 100644 index 2153134fff..0000000000 --- a/src/common/ipc-channel/channel.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -export interface Channel { - name: string; - _template: TInstance; -} diff --git a/src/common/ipc-channel/create-channel/create-channel.ts b/src/common/ipc-channel/create-channel/create-channel.ts deleted file mode 100644 index c98709a062..0000000000 --- a/src/common/ipc-channel/create-channel/create-channel.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { Channel } from "../channel"; - -export const createChannel = (name: string): Channel => ({ - name, - _template: null, -}); diff --git a/src/common/logger.ts b/src/common/logger.ts index 504b0e126d..0d180a6838 100644 --- a/src/common/logger.ts +++ b/src/common/logger.ts @@ -10,6 +10,15 @@ import { consoleFormat } from "winston-console-format"; import { isDebugging, isTestEnv } from "./vars"; import BrowserConsole from "winston-transport-browserconsole"; +export interface LensLogger { + error: (...args: any[]) => void; + warn: (...args: any[]) => void; + info: (...args: any[]) => void; + debug: (...args: any[]) => void; + verbose: (...args: any[]) => void; + silly: (...args: any[]) => void; +} + const logLevel = process.env.LOG_LEVEL ? process.env.LOG_LEVEL : isDebugging @@ -67,4 +76,4 @@ if (ipcMain) { export default winston.createLogger({ format: format.simple(), transports, -}); +}) as LensLogger; diff --git a/src/common/logger/base-logger.injectable.ts b/src/common/logger/base-logger.injectable.ts new file mode 100644 index 0000000000..0bd505bc63 --- /dev/null +++ b/src/common/logger/base-logger.injectable.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import logger from "../logger"; + +const baseLoggerInjectable = getInjectable({ + instantiate: () => logger, + lifecycle: lifecycleEnum.singleton, +}); + +export default baseLoggerInjectable; diff --git a/src/common/logger/create-child-logger.injectable.ts b/src/common/logger/create-child-logger.injectable.ts new file mode 100644 index 0000000000..49cc5ba595 --- /dev/null +++ b/src/common/logger/create-child-logger.injectable.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { LensLogger } from "../logger"; +import baseLoggerInjectable from "./base-logger.injectable"; + +interface Dependencies { + baseLogger: LensLogger; +} + +const createChildLogger = ({ baseLogger }: Dependencies) => ( + (prefix: string): LensLogger => { + return { + debug: (message, info) => baseLogger.debug(`${prefix}: ${message}`, info), + warn: (message, info) => baseLogger.warn(`${prefix}: ${message}`, info), + error: (message, info) => baseLogger.error(`${prefix}: ${message}`, info), + verbose: (message, info) => baseLogger.verbose(`${prefix}: ${message}`, info), + info: (message, info) => baseLogger.info(`${prefix}: ${message}`, info), + silly: (message, info) => baseLogger.silly(`${prefix}: ${message}`, info), + }; + } +); + +const createChildLoggerInjectable = getInjectable({ + instantiate: (di) => createChildLogger({ + baseLogger: di.inject(baseLoggerInjectable), + }), + lifecycle: lifecycleEnum.singleton, +}); + +export default createChildLoggerInjectable; diff --git a/src/common/utils/unique-id.injectable.ts b/src/common/utils/unique-id.injectable.ts new file mode 100644 index 0000000000..654e662046 --- /dev/null +++ b/src/common/utils/unique-id.injectable.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { v4 } from "uuid"; + +const uniqueIdInjectable = getInjectable({ + instantiate: () => v4, + lifecycle: lifecycleEnum.singleton, +}); + +export default uniqueIdInjectable; diff --git a/src/extensions/__tests__/extension-loader.test.ts b/src/extensions/__tests__/extension-loader.test.ts index c9e145f8f9..f824ba05ec 100644 --- a/src/extensions/__tests__/extension-loader.test.ts +++ b/src/extensions/__tests__/extension-loader.test.ts @@ -110,7 +110,7 @@ describe("ExtensionLoader", () => { let updateExtensionStateMock: jest.Mock; beforeEach(async () => { - const dis = getDisForUnitTesting({ doGeneralOverrides: true }); + const dis = await getDisForUnitTesting({ doGeneralOverrides: true }); mockFs(); diff --git a/src/extensions/extension-discovery/extension-discovery.injectable.ts b/src/extensions/extension-discovery/extension-discovery.injectable.ts index 114e217270..c46c5e31cb 100644 --- a/src/extensions/extension-discovery/extension-discovery.injectable.ts +++ b/src/extensions/extension-discovery/extension-discovery.injectable.ts @@ -8,34 +8,23 @@ import extensionLoaderInjectable from "../extension-loader/extension-loader.inje import isCompatibleExtensionInjectable from "./is-compatible-extension/is-compatible-extension.injectable"; import isCompatibleBundledExtensionInjectable from "./is-compatible-bundled-extension/is-compatible-bundled-extension.injectable"; import extensionsStoreInjectable from "../extensions-store/extensions-store.injectable"; -import extensionInstallationStateStoreInjectable from "../extension-installation-state-store/extension-installation-state-store.injectable"; import installExtensionInjectable from "../extension-installer/install-extension/install-extension.injectable"; import extensionPackageRootDirectoryInjectable from "../extension-installer/extension-package-root-directory/extension-package-root-directory.injectable"; import installExtensionsInjectable from "../extension-installer/install-extensions/install-extensions.injectable"; +import { clearInstallingChannelInjectionToken, setInstallingChannelInjectionToken } from "../installation-state/state-channels"; const extensionDiscoveryInjectable = getInjectable({ - instantiate: (di) => - new ExtensionDiscovery({ - extensionLoader: di.inject(extensionLoaderInjectable), - extensionsStore: di.inject(extensionsStoreInjectable), - - extensionInstallationStateStore: di.inject( - extensionInstallationStateStoreInjectable, - ), - - isCompatibleBundledExtension: di.inject( - isCompatibleBundledExtensionInjectable, - ), - - isCompatibleExtension: di.inject(isCompatibleExtensionInjectable), - - installExtension: di.inject(installExtensionInjectable), - installExtensions: di.inject(installExtensionsInjectable), - - extensionPackageRootDirectory: di.inject( - extensionPackageRootDirectoryInjectable, - ), - }), + instantiate: (di) => new ExtensionDiscovery({ + extensionLoader: di.inject(extensionLoaderInjectable), + extensionsStore: di.inject(extensionsStoreInjectable), + setInstalling: di.inject(setInstallingChannelInjectionToken), + clearInstalling: di.inject(clearInstallingChannelInjectionToken), + isCompatibleBundledExtension: di.inject(isCompatibleBundledExtensionInjectable), + isCompatibleExtension: di.inject(isCompatibleExtensionInjectable), + installExtension: di.inject(installExtensionInjectable), + installExtensions: di.inject(installExtensionsInjectable), + extensionPackageRootDirectory: di.inject(extensionPackageRootDirectoryInjectable), + }), lifecycle: lifecycleEnum.singleton, }); diff --git a/src/extensions/extension-discovery/extension-discovery.test.ts b/src/extensions/extension-discovery/extension-discovery.test.ts index 84737a50d2..0dce46e567 100644 --- a/src/extensions/extension-discovery/extension-discovery.test.ts +++ b/src/extensions/extension-discovery/extension-discovery.test.ts @@ -11,10 +11,8 @@ import * as fse from "fs-extra"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; import extensionDiscoveryInjectable from "../extension-discovery/extension-discovery.injectable"; import type { ExtensionDiscovery } from "../extension-discovery/extension-discovery"; -import installExtensionInjectable - from "../extension-installer/install-extension/install-extension.injectable"; -import directoryForUserDataInjectable - from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import installExtensionInjectable from "../extension-installer/install-extension/install-extension.injectable"; +import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import mockFs from "mock-fs"; jest.setTimeout(60_000); @@ -49,16 +47,15 @@ describe("ExtensionDiscovery", () => { let extensionDiscovery: ExtensionDiscovery; beforeEach(async () => { - const di = getDiForUnitTesting({ doGeneralOverrides: true }); + const di = await getDiForUnitTesting({ doGeneralOverrides: true }); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); di.override(installExtensionInjectable, () => () => Promise.resolve()); - mockFs(); - await di.runSetups(); extensionDiscovery = di.inject(extensionDiscoveryInjectable); + mockFs(); }); afterEach(() => { diff --git a/src/extensions/extension-discovery/extension-discovery.ts b/src/extensions/extension-discovery/extension-discovery.ts index eb957a4f0c..d9b080db51 100644 --- a/src/extensions/extension-discovery/extension-discovery.ts +++ b/src/extensions/extension-discovery/extension-discovery.ts @@ -17,7 +17,6 @@ import type { ExtensionsStore } from "../extensions-store/extensions-store"; import type { ExtensionLoader } from "../extension-loader"; import type { LensExtensionId, LensExtensionManifest } from "../lens-extension"; import { isProduction } from "../../common/vars"; -import type { ExtensionInstallationStateStore } from "../extension-installation-state-store/extension-installation-state-store"; import type { PackageJson } from "type-fest"; import { extensionDiscoveryStateChannel } from "../../common/ipc/extension-handling"; import { requestInitialExtensionDiscovery } from "../../renderer/ipc"; @@ -25,12 +24,10 @@ import { requestInitialExtensionDiscovery } from "../../renderer/ipc"; interface Dependencies { extensionLoader: ExtensionLoader; extensionsStore: ExtensionsStore; - - extensionInstallationStateStore: ExtensionInstallationStateStore; - + setInstalling: (extId: string) => void; + clearInstalling: (extId: string) => void; isCompatibleBundledExtension: (manifest: LensExtensionManifest) => boolean; isCompatibleExtension: (manifest: LensExtensionManifest) => boolean; - installExtension: (name: string) => Promise; installExtensions: (packageJsonPath: string, packagesJson: PackageJson) => Promise extensionPackageRootDirectory: string; @@ -191,7 +188,7 @@ export class ExtensionDiscovery { if (path.basename(manifestPath) === manifestFilename && isUnderLocalFolderPath) { try { - this.dependencies.extensionInstallationStateStore.setInstallingFromMain(manifestPath); + this.dependencies.setInstalling(manifestPath); const absPath = path.dirname(manifestPath); // this.loadExtensionFromPath updates this.packagesJson @@ -211,7 +208,7 @@ export class ExtensionDiscovery { } catch (error) { logger.error(`${logModule}: failed to add extension: ${error}`, { error }); } finally { - this.dependencies.extensionInstallationStateStore.clearInstallingFromMain(manifestPath); + this.dependencies.clearInstalling(manifestPath); } } }; diff --git a/src/extensions/extension-installation-state-store/extension-installation-state-store.injectable.ts b/src/extensions/extension-installation-state-store/extension-installation-state-store.injectable.ts deleted file mode 100644 index 75bd8b62a4..0000000000 --- a/src/extensions/extension-installation-state-store/extension-installation-state-store.injectable.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import { ExtensionInstallationStateStore } from "./extension-installation-state-store"; - -const extensionInstallationStateStoreInjectable = getInjectable({ - instantiate: () => new ExtensionInstallationStateStore(), - lifecycle: lifecycleEnum.singleton, -}); - -export default extensionInstallationStateStoreInjectable; diff --git a/src/extensions/extension-installation-state-store/extension-installation-state-store.ts b/src/extensions/extension-installation-state-store/extension-installation-state-store.ts deleted file mode 100644 index 1942740690..0000000000 --- a/src/extensions/extension-installation-state-store/extension-installation-state-store.ts +++ /dev/null @@ -1,244 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { action, computed, observable } from "mobx"; -import logger from "../../main/logger"; -import { disposer } from "../../renderer/utils"; -import type { ExtendableDisposer } from "../../renderer/utils"; -import * as uuid from "uuid"; -import { broadcastMessage } from "../../common/ipc"; -import { ipcRenderer } from "electron"; - -export enum ExtensionInstallationState { - INSTALLING = "installing", - UNINSTALLING = "uninstalling", - IDLE = "idle", -} - -const Prefix = "[ExtensionInstallationStore]"; - -export class ExtensionInstallationStateStore { - private InstallingFromMainChannel = - "extension-installation-state-store:install"; - - private ClearInstallingFromMainChannel = - "extension-installation-state-store:clear-install"; - - private PreInstallIds = observable.set(); - private UninstallingExtensions = observable.set(); - private InstallingExtensions = observable.set(); - - bindIpcListeners = () => { - ipcRenderer - .on(this.InstallingFromMainChannel, (event, extId) => { - this.setInstalling(extId); - }) - - .on(this.ClearInstallingFromMainChannel, (event, extId) => { - this.clearInstalling(extId); - }); - }; - - /** - * Strictly transitions an extension from not installing to installing - * @param extId the ID of the extension - * @throws if state is not IDLE - */ - @action setInstalling = (extId: string): void => { - logger.debug(`${Prefix}: trying to set ${extId} as installing`); - - const curState = this.getInstallationState(extId); - - if (curState !== ExtensionInstallationState.IDLE) { - throw new Error( - `${Prefix}: cannot set ${extId} as installing. Is currently ${curState}.`, - ); - } - - this.InstallingExtensions.add(extId); - }; - - /** - * Broadcasts that an extension is being installed by the main process - * @param extId the ID of the extension - */ - setInstallingFromMain = (extId: string): void => { - broadcastMessage(this.InstallingFromMainChannel, extId); - }; - - /** - * Broadcasts that an extension is no longer being installed by the main process - * @param extId the ID of the extension - */ - clearInstallingFromMain = (extId: string): void => { - broadcastMessage(this.ClearInstallingFromMainChannel, extId); - }; - - /** - * Marks the start of a pre-install phase of an extension installation. The - * part of the installation before the tarball has been unpacked and the ID - * determined. - * @returns a disposer which should be called to mark the end of the install phase - */ - @action startPreInstall = (): ExtendableDisposer => { - const preInstallStepId = uuid.v4(); - - logger.debug( - `${Prefix}: starting a new preinstall phase: ${preInstallStepId}`, - ); - this.PreInstallIds.add(preInstallStepId); - - return disposer(() => { - this.PreInstallIds.delete(preInstallStepId); - logger.debug(`${Prefix}: ending a preinstall phase: ${preInstallStepId}`); - }); - }; - - /** - * Strictly transitions an extension from not uninstalling to uninstalling - * @param extId the ID of the extension - * @throws if state is not IDLE - */ - @action setUninstalling = (extId: string): void => { - logger.debug(`${Prefix}: trying to set ${extId} as uninstalling`); - - const curState = this.getInstallationState(extId); - - if (curState !== ExtensionInstallationState.IDLE) { - throw new Error( - `${Prefix}: cannot set ${extId} as uninstalling. Is currently ${curState}.`, - ); - } - - this.UninstallingExtensions.add(extId); - }; - - /** - * Strictly clears the INSTALLING state of an extension - * @param extId The ID of the extension - * @throws if state is not INSTALLING - */ - @action clearInstalling = (extId: string): void => { - logger.debug(`${Prefix}: trying to clear ${extId} as installing`); - - const curState = this.getInstallationState(extId); - - switch (curState) { - case ExtensionInstallationState.INSTALLING: - return void this.InstallingExtensions.delete(extId); - default: - throw new Error( - `${Prefix}: cannot clear INSTALLING state for ${extId}, it is currently ${curState}`, - ); - } - }; - - /** - * Strictly clears the UNINSTALLING state of an extension - * @param extId The ID of the extension - * @throws if state is not UNINSTALLING - */ - @action clearUninstalling = (extId: string): void => { - logger.debug(`${Prefix}: trying to clear ${extId} as uninstalling`); - - const curState = this.getInstallationState(extId); - - switch (curState) { - case ExtensionInstallationState.UNINSTALLING: - return void this.UninstallingExtensions.delete(extId); - default: - throw new Error( - `${Prefix}: cannot clear UNINSTALLING state for ${extId}, it is currently ${curState}`, - ); - } - }; - - /** - * Returns the current state of the extension. IDLE is default value. - * @param extId The ID of the extension - */ - getInstallationState = (extId: string): ExtensionInstallationState => { - if (this.InstallingExtensions.has(extId)) { - return ExtensionInstallationState.INSTALLING; - } - - if (this.UninstallingExtensions.has(extId)) { - return ExtensionInstallationState.UNINSTALLING; - } - - return ExtensionInstallationState.IDLE; - }; - - /** - * Returns true if the extension is currently INSTALLING - * @param extId The ID of the extension - */ - isExtensionInstalling = (extId: string): boolean => - this.getInstallationState(extId) === ExtensionInstallationState.INSTALLING; - - /** - * Returns true if the extension is currently UNINSTALLING - * @param extId The ID of the extension - */ - isExtensionUninstalling = (extId: string): boolean => - this.getInstallationState(extId) === - ExtensionInstallationState.UNINSTALLING; - - /** - * Returns true if the extension is currently IDLE - * @param extId The ID of the extension - */ - isExtensionIdle = (extId: string): boolean => - this.getInstallationState(extId) === ExtensionInstallationState.IDLE; - - /** - * The current number of extensions installing - */ - @computed get installing(): number { - return this.InstallingExtensions.size; - } - - /** - * The current number of extensions uninstalling - */ - get uninstalling(): number { - return this.UninstallingExtensions.size; - } - - /** - * If there is at least one extension currently installing - */ - get anyInstalling(): boolean { - return this.installing > 0; - } - - /** - * If there is at least one extension currently uninstalling - */ - get anyUninstalling(): boolean { - return this.uninstalling > 0; - } - - /** - * The current number of extensions preinstalling - */ - get preinstalling(): number { - return this.PreInstallIds.size; - } - - /** - * If there is at least one extension currently downloading - */ - get anyPreinstalling(): boolean { - return this.preinstalling > 0; - } - - /** - * If there is at least one installing or preinstalling step taking place - */ - get anyPreInstallingOrInstalling(): boolean { - return this.anyInstalling || this.anyPreinstalling; - } -} diff --git a/src/extensions/extension-loader/get-installed-extension.injectable.ts b/src/extensions/extension-loader/get-installed-extension.injectable.ts new file mode 100644 index 0000000000..a25b82cebe --- /dev/null +++ b/src/extensions/extension-loader/get-installed-extension.injectable.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import extensionLoaderInjectable from "./extension-loader.injectable"; + +const getInstalledExtensionInjectable = getInjectable({ + instantiate: (di) => { + const store = di.inject(extensionLoaderInjectable); + + return (extId: string) => store.getExtension(extId); + }, + lifecycle: lifecycleEnum.singleton, +}); + +export default getInstalledExtensionInjectable; diff --git a/src/extensions/installation-state/logger.injectable.ts b/src/extensions/installation-state/logger.injectable.ts new file mode 100644 index 0000000000..14be2fd4cf --- /dev/null +++ b/src/extensions/installation-state/logger.injectable.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import createChildLoggerInjectable from "../../common/logger/create-child-logger.injectable"; + +const installationStateLoggerInjectable = getInjectable({ + instantiate: (di) => { + const createChildLogger = di.inject(createChildLoggerInjectable); + + return createChildLogger("[ExtensionInstallationStore]"); + }, + lifecycle: lifecycleEnum.singleton, +}); + +export default installationStateLoggerInjectable; diff --git a/src/extensions/installation-state/state-channels.ts b/src/extensions/installation-state/state-channels.ts new file mode 100644 index 0000000000..94cf27ebf8 --- /dev/null +++ b/src/extensions/installation-state/state-channels.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { EmitterChannel } from "../../common/communication/emitter"; + +export type SetInstalling = EmitterChannel<[extId: string]>; + +export const setInstallingChannel = "extension-installation-state-store:install"; +export const setInstallingChannelInjectionToken = getInjectionToken(); + +export type ClearInstalling = EmitterChannel<[extId: string]>; + +export const clearInstallingChannel = "extension-installation-state-store:clear-install"; +export const clearInstallingChannelInjectionToken = getInjectionToken(); diff --git a/src/extensions/installation-state/state.ts b/src/extensions/installation-state/state.ts new file mode 100644 index 0000000000..73d527652e --- /dev/null +++ b/src/extensions/installation-state/state.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +/** + * The possible installation states + */ +export enum InstallationState { + INSTALLING = "installing", + UNINSTALLING = "uninstalling", + IDLE = "idle", +} diff --git a/src/extensions/registries/__tests__/page-registry.test.ts b/src/extensions/registries/__tests__/page-registry.test.ts index 1f8d1578eb..2c94bcde45 100644 --- a/src/extensions/registries/__tests__/page-registry.test.ts +++ b/src/extensions/registries/__tests__/page-registry.test.ts @@ -37,7 +37,7 @@ let ext: LensExtension = null; describe("page registry tests", () => { beforeEach(async () => { - const dis = getDisForUnitTesting({ doGeneralOverrides: true }); + const dis = await getDisForUnitTesting({ doGeneralOverrides: true }); mockFs(); diff --git a/src/main/__test__/cluster.test.ts b/src/main/__test__/cluster.test.ts index 4fa5f5cdd3..4c1f0920a2 100644 --- a/src/main/__test__/cluster.test.ts +++ b/src/main/__test__/cluster.test.ts @@ -51,7 +51,7 @@ describe("create clusters", () => { beforeEach(async () => { jest.clearAllMocks(); - const di = getDiForUnitTesting({ doGeneralOverrides: true }); + const di = await getDiForUnitTesting({ doGeneralOverrides: true }); mockFs({ "minikube-config.yml": JSON.stringify({ diff --git a/src/main/__test__/context-handler.test.ts b/src/main/__test__/context-handler.test.ts index 191a129dbe..3fc23438e6 100644 --- a/src/main/__test__/context-handler.test.ts +++ b/src/main/__test__/context-handler.test.ts @@ -74,7 +74,7 @@ describe("ContextHandler", () => { let createContextHandler: (cluster: Cluster) => ContextHandler; beforeEach(async () => { - const di = getDiForUnitTesting({ doGeneralOverrides: true }); + const di = await getDiForUnitTesting({ doGeneralOverrides: true }); mockFs({ "tmp": {}, diff --git a/src/main/__test__/kube-auth-proxy.test.ts b/src/main/__test__/kube-auth-proxy.test.ts index 7a3f4ea082..8d37cab679 100644 --- a/src/main/__test__/kube-auth-proxy.test.ts +++ b/src/main/__test__/kube-auth-proxy.test.ts @@ -89,7 +89,7 @@ describe("kube auth proxy tests", () => { "tmp": {}, }; - const di = getDiForUnitTesting({ doGeneralOverrides: true }); + const di = await getDiForUnitTesting({ doGeneralOverrides: true }); mockFs(mockMinikubeConfig); diff --git a/src/main/__test__/kubeconfig-manager.test.ts b/src/main/__test__/kubeconfig-manager.test.ts index 53808c6ce9..2d85223791 100644 --- a/src/main/__test__/kubeconfig-manager.test.ts +++ b/src/main/__test__/kubeconfig-manager.test.ts @@ -48,7 +48,7 @@ describe("kubeconfig manager tests", () => { let createKubeconfigManager: (cluster: Cluster) => KubeconfigManager; beforeEach(async () => { - const di = getDiForUnitTesting({ doGeneralOverrides: true }); + const di = await getDiForUnitTesting({ doGeneralOverrides: true }); di.override(directoryForTempInjectable, () => "some-directory-for-temp"); diff --git a/src/main/app-paths/app-paths-channel.injectable.ts b/src/main/app-paths/app-paths-channel.injectable.ts new file mode 100644 index 0000000000..15ed2cb8ce --- /dev/null +++ b/src/main/app-paths/app-paths-channel.injectable.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { appPathsInjectionChannelToken, appPathsIpcChannel } from "../../common/app-paths/app-path-channel-injection-token"; +import registerChannelInjectable from "../communication/register-channel.injectable"; +import type { Channel } from "../../common/communication/channel"; +import type { AppPaths } from "../../common/app-paths/app-paths"; +import { appPathsInjectionToken } from "../../common/app-paths/app-path-injection-token"; + +let channel: Channel<[], AppPaths>; + +const appPathsInjectable = getInjectable({ + setup: (di) => { + const appPaths = di.inject(appPathsInjectionToken); + const registerChannel = di.inject(registerChannelInjectable); + + channel = registerChannel(appPathsIpcChannel, () => appPaths); + }, + instantiate: () => channel, + injectionToken: appPathsInjectionChannelToken, + lifecycle: lifecycleEnum.singleton, +}); + +export default appPathsInjectable; diff --git a/src/main/app-paths/app-paths.injectable.ts b/src/main/app-paths/app-paths.injectable.ts index 0c32f79a0b..5ea7cf0a92 100644 --- a/src/main/app-paths/app-paths.injectable.ts +++ b/src/main/app-paths/app-paths.injectable.ts @@ -2,67 +2,38 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { - DependencyInjectionContainer, - getInjectable, - lifecycleEnum, -} from "@ogre-tools/injectable"; - -import { - appPathsInjectionToken, - appPathsIpcChannel, -} from "../../common/app-paths/app-path-injection-token"; - -import registerChannelInjectable from "./register-channel/register-channel.injectable"; +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getAppPaths } from "./get-app-paths"; import getElectronAppPathInjectable from "./get-electron-app-path/get-electron-app-path.injectable"; import setElectronAppPathInjectable from "./set-electron-app-path/set-electron-app-path.injectable"; import path from "path"; import appNameInjectable from "./app-name/app-name.injectable"; import directoryForIntegrationTestingInjectable from "./directory-for-integration-testing/directory-for-integration-testing.injectable"; +import { appPathsInjectionToken } from "../../common/app-paths/app-path-injection-token"; const appPathsInjectable = getInjectable({ - setup: (di) => { - const directoryForIntegrationTesting = di.inject( - directoryForIntegrationTestingInjectable, - ); + instantiate: (di) => { + const directoryForIntegrationTesting = di.inject(directoryForIntegrationTestingInjectable); + const setElectronAppPath = di.inject(setElectronAppPathInjectable); if (directoryForIntegrationTesting) { - setupPathForAppDataInIntegrationTesting(di, directoryForIntegrationTesting); + // TODO: this kludge is here only until we have a proper place to setup integration testing. + setElectronAppPath("appData", directoryForIntegrationTesting); } - setupPathForUserData(di); - registerAppPathsChannel(di); + // Set path for user data + const appName = di.inject(appNameInjectable); + const getAppPath = di.inject(getElectronAppPathInjectable); + const appDataPath = getAppPath("appData"); + + setElectronAppPath("userData", path.join(appDataPath, appName)); + + return getAppPaths({ + getAppPath: di.inject(getElectronAppPathInjectable), + }); }, - - instantiate: (di) => - getAppPaths({ getAppPath: di.inject(getElectronAppPathInjectable) }), - injectionToken: appPathsInjectionToken, lifecycle: lifecycleEnum.singleton, }); export default appPathsInjectable; - -const registerAppPathsChannel = (di: DependencyInjectionContainer) => { - const registerChannel = di.inject(registerChannelInjectable); - - registerChannel(appPathsIpcChannel, () => di.inject(appPathsInjectable)); -}; - -const setupPathForUserData = (di: DependencyInjectionContainer) => { - const setElectronAppPath = di.inject(setElectronAppPathInjectable); - const appName = di.inject(appNameInjectable); - const getAppPath = di.inject(getElectronAppPathInjectable); - - const appDataPath = getAppPath("appData"); - - setElectronAppPath("userData", path.join(appDataPath, appName)); -}; - -// Todo: this kludge is here only until we have a proper place to setup integration testing. -const setupPathForAppDataInIntegrationTesting = (di: DependencyInjectionContainer, appDataPath: string) => { - const setElectronAppPath = di.inject(setElectronAppPathInjectable); - - setElectronAppPath("appData", appDataPath); -}; diff --git a/src/main/app-paths/get-app-paths.ts b/src/main/app-paths/get-app-paths.ts index 5dce5ccaa2..f5fe23c3e0 100644 --- a/src/main/app-paths/get-app-paths.ts +++ b/src/main/app-paths/get-app-paths.ts @@ -2,13 +2,13 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { fromPairs } from "lodash/fp"; -import { pathNames, PathName } from "../../common/app-paths/app-path-names"; -import type { AppPaths } from "../../common/app-paths/app-path-injection-token"; +import { pathNames, PathName, AppPaths } from "../../common/app-paths/app-paths"; +import { fromEntries } from "../../renderer/utils"; interface Dependencies { getAppPath: (name: PathName) => string } -export const getAppPaths = ({ getAppPath }: Dependencies) => - fromPairs(pathNames.map((name) => [name, getAppPath(name)])) as AppPaths; +export function getAppPaths({ getAppPath }: Dependencies): AppPaths { + return fromEntries(pathNames.map((name) => [name, getAppPath(name)])); +} diff --git a/src/main/app-paths/get-electron-app-path/get-electron-app-path.test.ts b/src/main/app-paths/get-electron-app-path/get-electron-app-path.test.ts index 8eeb24ce1f..c7ae557014 100644 --- a/src/main/app-paths/get-electron-app-path/get-electron-app-path.test.ts +++ b/src/main/app-paths/get-electron-app-path/get-electron-app-path.test.ts @@ -6,13 +6,13 @@ import electronAppInjectable from "./electron-app/electron-app.injectable"; import getElectronAppPathInjectable from "./get-electron-app-path.injectable"; import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import type { App } from "electron"; -import registerChannelInjectable from "../register-channel/register-channel.injectable"; +import registerChannelInjectable from "../../communication/register-channel.injectable"; describe("get-electron-app-path", () => { let getElectronAppPath: (name: string) => string | null; beforeEach(async () => { - const di = getDiForUnitTesting({ doGeneralOverrides: false }); + const di = await getDiForUnitTesting({ doGeneralOverrides: false }); const appStub = { name: "some-app-name", diff --git a/src/main/app-paths/get-electron-app-path/get-electron-app-path.ts b/src/main/app-paths/get-electron-app-path/get-electron-app-path.ts index cbd70cc02c..2a475df087 100644 --- a/src/main/app-paths/get-electron-app-path/get-electron-app-path.ts +++ b/src/main/app-paths/get-electron-app-path/get-electron-app-path.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import type { App } from "electron"; -import type { PathName } from "../../../common/app-paths/app-path-names"; +import type { PathName } from "../../../common/app-paths/app-paths"; interface Dependencies { app: App; diff --git a/src/main/app-paths/register-channel/register-channel.injectable.ts b/src/main/app-paths/register-channel/register-channel.injectable.ts deleted file mode 100644 index a52662687c..0000000000 --- a/src/main/app-paths/register-channel/register-channel.injectable.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import ipcMainInjectable from "./ipc-main/ipc-main.injectable"; -import { registerChannel } from "./register-channel"; - -const registerChannelInjectable = getInjectable({ - instantiate: (di) => registerChannel({ - ipcMain: di.inject(ipcMainInjectable), - }), - - lifecycle: lifecycleEnum.singleton, -}); - -export default registerChannelInjectable; diff --git a/src/main/app-paths/register-channel/register-channel.ts b/src/main/app-paths/register-channel/register-channel.ts deleted file mode 100644 index 73f3e13243..0000000000 --- a/src/main/app-paths/register-channel/register-channel.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { IpcMain } from "electron"; -import type { Channel } from "../../../common/ipc-channel/channel"; - -interface Dependencies { - ipcMain: IpcMain; -} - -export const registerChannel = - ({ ipcMain }: Dependencies) => - , TInstance>( - channel: TChannel, - getValue: () => TInstance, - ) => - ipcMain.handle(channel.name, getValue); diff --git a/src/main/app-paths/set-electron-app-path/set-electron-app-path.injectable.ts b/src/main/app-paths/set-electron-app-path/set-electron-app-path.injectable.ts index ade45238cf..67bf3afaea 100644 --- a/src/main/app-paths/set-electron-app-path/set-electron-app-path.injectable.ts +++ b/src/main/app-paths/set-electron-app-path/set-electron-app-path.injectable.ts @@ -3,12 +3,17 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import type { PathName } from "../../../common/app-paths/app-path-names"; +import type { PathName } from "../../../common/app-paths/app-paths"; import electronAppInjectable from "../get-electron-app-path/electron-app/electron-app.injectable"; const setElectronAppPathInjectable = getInjectable({ - instantiate: (di) => (name: PathName, path: string) : void => - di.inject(electronAppInjectable).setPath(name, path), + instantiate: (di) => { + const app = di.inject(electronAppInjectable); + + return (name: PathName, path: string): void => { + app.setPath(name, path); + }; + }, lifecycle: lifecycleEnum.singleton, }); diff --git a/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts b/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts index f04458c3c8..9a15c6e032 100644 --- a/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts +++ b/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts @@ -38,7 +38,7 @@ describe("kubeconfig-sync.source tests", () => { let computeDiff: ReturnType; beforeEach(async () => { - const di = getDiForUnitTesting({ doGeneralOverrides: true }); + const di = await getDiForUnitTesting({ doGeneralOverrides: true }); mockFs(); diff --git a/src/main/communication/ipc-handle.injectable.ts b/src/main/communication/ipc-handle.injectable.ts new file mode 100644 index 0000000000..75c7ec8c7d --- /dev/null +++ b/src/main/communication/ipc-handle.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { IpcMainInvokeEvent } from "electron"; +import ipcMainInjectable from "./ipc-main.injectable"; + +const ipcHandleInjectable = getInjectable({ + instantiate: (di) => { + const ipcMain = di.inject(ipcMainInjectable); + + return (channel: string, listener: (event: IpcMainInvokeEvent, ...args: any[]) => any): void => { + ipcMain.handle(channel, listener); + }; + }, + lifecycle: lifecycleEnum.singleton, +}); + +export default ipcHandleInjectable; diff --git a/src/main/app-paths/register-channel/ipc-main/ipc-main.injectable.ts b/src/main/communication/ipc-main.injectable.ts similarity index 100% rename from src/main/app-paths/register-channel/ipc-main/ipc-main.injectable.ts rename to src/main/communication/ipc-main.injectable.ts diff --git a/src/main/communication/ipc-on.injectable.ts b/src/main/communication/ipc-on.injectable.ts new file mode 100644 index 0000000000..10ff83c76f --- /dev/null +++ b/src/main/communication/ipc-on.injectable.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { IpcMainEvent } from "electron"; +import { ipcOnEventInjectionToken } from "../../common/communication/ipc-on-event-injection-token"; +import ipcMainInjectable from "./ipc-main.injectable"; + +const ipcOnInjectable = getInjectable({ + instantiate: (di) => { + const ipcMain = di.inject(ipcMainInjectable); + + return (channel: string, listener: (event: IpcMainEvent, ...args: any[]) => void): void => { + ipcMain.on(channel, listener); + }; + }, + injectionToken: ipcOnEventInjectionToken, + lifecycle: lifecycleEnum.singleton, +}); + +export default ipcOnInjectable; diff --git a/src/main/communication/register-channel.injectable.ts b/src/main/communication/register-channel.injectable.ts new file mode 100644 index 0000000000..c0cdc8fd4c --- /dev/null +++ b/src/main/communication/register-channel.injectable.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { IpcMainInvokeEvent } from "electron"; +import type { Channel } from "../../common/communication/channel"; +import ipcHandleInjectable from "./ipc-handle.injectable"; + +interface Dependencies { + handle: (channel: string, listener: (event: IpcMainInvokeEvent, ...args: any[]) => void) => void; +} + +function registerChannel({ handle }: Dependencies) { + return function (name: string, getValue: (...args: Parameters) => Value): Channel { + handle(name, async (event, ...args: Parameters) => await getValue(...args)); + + return () => { + throw new Error(`Invoking channel ${name} on main is invalid`); + }; + }; +} + +const registerChannelInjectable = getInjectable({ + instantiate: (di) => registerChannel({ + handle: di.inject(ipcHandleInjectable), + }), + lifecycle: lifecycleEnum.singleton, +}); + +export default registerChannelInjectable; diff --git a/src/main/extensions/installation-state/clear-installing-channel.injectable.ts b/src/main/extensions/installation-state/clear-installing-channel.injectable.ts new file mode 100644 index 0000000000..1e9d051a51 --- /dev/null +++ b/src/main/extensions/installation-state/clear-installing-channel.injectable.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import registerEmitterChannelInjectable from "../../../common/communication/register-emitter.injectable"; +import installationStateLoggerInjectable from "../../../extensions/installation-state/logger.injectable"; +import { clearInstallingChannel, clearInstallingChannelInjectionToken } from "../../../extensions/installation-state/state-channels"; + +const clearInstallingChannelInjectable = getInjectable({ + instantiate: (di) => { + const registerEmitterChannel = di.inject(registerEmitterChannelInjectable); + const logger = di.inject(installationStateLoggerInjectable); + + return registerEmitterChannel(clearInstallingChannel, logger); + }, + injectionToken: clearInstallingChannelInjectionToken, + lifecycle: lifecycleEnum.singleton, +}); + +export default clearInstallingChannelInjectable; diff --git a/src/main/extensions/installation-state/set-installing-channel.injectable.ts b/src/main/extensions/installation-state/set-installing-channel.injectable.ts new file mode 100644 index 0000000000..297bcdf82c --- /dev/null +++ b/src/main/extensions/installation-state/set-installing-channel.injectable.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import registerEmitterChannelInjectable from "../../../common/communication/register-emitter.injectable"; +import installationStateLoggerInjectable from "../../../extensions/installation-state/logger.injectable"; +import { setInstallingChannel, setInstallingChannelInjectionToken } from "../../../extensions/installation-state/state-channels"; + +const setInstallingChannelInjectable = getInjectable({ + instantiate: (di) => { + const registerEmitterChannel = di.inject(registerEmitterChannelInjectable); + const logger = di.inject(installationStateLoggerInjectable); + + return registerEmitterChannel(setInstallingChannel, logger); + }, + injectionToken: setInstallingChannelInjectionToken, + lifecycle: lifecycleEnum.singleton, +}); + +export default setInstallingChannelInjectable; diff --git a/src/main/getDiForUnitTesting.ts b/src/main/getDiForUnitTesting.ts index ae71fb9618..08d2622294 100644 --- a/src/main/getDiForUnitTesting.ts +++ b/src/main/getDiForUnitTesting.ts @@ -11,20 +11,22 @@ import { setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-global import getElectronAppPathInjectable from "./app-paths/get-electron-app-path/get-electron-app-path.injectable"; import setElectronAppPathInjectable from "./app-paths/set-electron-app-path/set-electron-app-path.injectable"; import appNameInjectable from "./app-paths/app-name/app-name.injectable"; -import registerChannelInjectable from "./app-paths/register-channel/register-channel.injectable"; -import writeJsonFileInjectable from "../common/fs/write-json-file.injectable"; -import readJsonFileInjectable from "../common/fs/read-json-file.injectable"; +import registerEventSinkInjectable from "../common/communication/register-event-sink.injectable"; +import registerChannelInjectable from "./communication/register-channel.injectable"; +import { overrideFsFunctions } from "../test-utils/override-fs-functions"; -export const getDiForUnitTesting = ( - { doGeneralOverrides } = { doGeneralOverrides: false }, -) => { +interface DiForTestingOptions { + doGeneralOverrides?: boolean; + doIpcOverrides?: boolean; +} + +export async function getDiForUnitTesting({ doGeneralOverrides = false, doIpcOverrides = true }: DiForTestingOptions = {}) { const di = createContainer(); setLegacyGlobalDiForExtensionApi(di); for (const filePath of getInjectableFilePaths()) { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const injectableInstance = require(filePath).default; + const { default: injectableInstance } = await import(filePath); di.register({ id: filePath, @@ -36,26 +38,21 @@ export const getDiForUnitTesting = ( di.preventSideEffects(); if (doGeneralOverrides) { - di.override( - getElectronAppPathInjectable, - () => (name: string) => `some-electron-app-path-for-${kebabCase(name)}`, - ); + di.override(getElectronAppPathInjectable, () => (name: string) => `some-electron-app-path-for-${kebabCase(name)}`); di.override(setElectronAppPathInjectable, () => () => undefined); di.override(appNameInjectable, () => "some-electron-app-name"); - di.override(registerChannelInjectable, () => () => undefined); - di.override(writeJsonFileInjectable, () => () => { - throw new Error("Tried to write JSON file to file system without specifying explicit override."); - }); + overrideFsFunctions(di); + } - di.override(readJsonFileInjectable, () => () => { - throw new Error("Tried to read JSON file from file system without specifying explicit override."); - }); + if (doIpcOverrides) { + di.override(registerEventSinkInjectable, () => () => () => undefined); + di.override(registerChannelInjectable, () => () => () => undefined); } return di; -}; +} const getInjectableFilePaths = memoize(() => [ ...glob.sync("./**/*.injectable.{ts,tsx}", { cwd: __dirname }), diff --git a/src/main/ipc/emit-event.ts b/src/main/ipc/emit-event.ts new file mode 100644 index 0000000000..206f81d0c4 --- /dev/null +++ b/src/main/ipc/emit-event.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import ipcMainInjectable from "../communication/ipc-main.injectable"; + +const emitEventInjectable = getInjectable({ + instantiate: (di) => { + const ipcMain = di.inject(ipcMainInjectable); + + return (channel: string, ...args: any[]) => { + ipcMain.emit(channel, ...args); + }; + }, + lifecycle: lifecycleEnum.singleton, +}); + +export default emitEventInjectable; diff --git a/src/main/lens-binary.ts b/src/main/lens-binary.ts index 2b8b5c27d3..9625f70696 100644 --- a/src/main/lens-binary.ts +++ b/src/main/lens-binary.ts @@ -9,7 +9,7 @@ import request from "request"; import { ensureDir, pathExists } from "fs-extra"; import * as tar from "tar"; import { isWindows } from "../common/vars"; -import type winston from "winston"; +import type { LensLogger } from "../common/logger"; export type LensBinaryOpts = { version: string; @@ -32,7 +32,7 @@ export class LensBinary { protected arch: string; protected originalBinaryName: string; protected requestOpts: request.Options; - protected logger: Console | winston.Logger; + protected logger: Omit; constructor(opts: LensBinaryOpts) { const baseDir = opts.baseDir; @@ -68,7 +68,7 @@ export class LensBinary { } } - public setLogger(logger: Console | winston.Logger) { + public setLogger(logger: LensLogger) { this.logger = logger; } diff --git a/src/main/menu/electron-menu-items.test.ts b/src/main/menu/electron-menu-items.test.ts index 4eeee205cd..249ade9850 100644 --- a/src/main/menu/electron-menu-items.test.ts +++ b/src/main/menu/electron-menu-items.test.ts @@ -17,7 +17,7 @@ describe("electron-menu-items", () => { let extensionsStub: ObservableMap; beforeEach(async () => { - di = getDiForUnitTesting({ doGeneralOverrides: true }); + di = await getDiForUnitTesting({ doGeneralOverrides: true }); extensionsStub = new ObservableMap(); diff --git a/src/main/protocol-handler/__test__/router.test.ts b/src/main/protocol-handler/__test__/router.test.ts index d96cd342dc..5620ff0e57 100644 --- a/src/main/protocol-handler/__test__/router.test.ts +++ b/src/main/protocol-handler/__test__/router.test.ts @@ -36,7 +36,7 @@ describe("protocol router tests", () => { let extensionsStore: ExtensionsStore; beforeEach(async () => { - const di = getDiForUnitTesting({ doGeneralOverrides: true }); + const di = await getDiForUnitTesting({ doGeneralOverrides: true }); mockFs({ "tmp": {}, diff --git a/src/main/tray/tray-menu-items.test.ts b/src/main/tray/tray-menu-items.test.ts index 72d33cf94a..ce72a5f870 100644 --- a/src/main/tray/tray-menu-items.test.ts +++ b/src/main/tray/tray-menu-items.test.ts @@ -17,7 +17,7 @@ describe("tray-menu-items", () => { let extensionsStub: ObservableMap; beforeEach(async () => { - di = getDiForUnitTesting({ doGeneralOverrides: true }); + di = await getDiForUnitTesting({ doGeneralOverrides: true }); await di.runSetups(); diff --git a/src/renderer/app-paths/app-paths-channel.injectable.ts b/src/renderer/app-paths/app-paths-channel.injectable.ts new file mode 100644 index 0000000000..ba6c8b40b6 --- /dev/null +++ b/src/renderer/app-paths/app-paths-channel.injectable.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { appPathsInjectionChannelToken, appPathsIpcChannel } from "../../common/app-paths/app-path-channel-injection-token"; +import registerChannelInjectable from "../communication/register-channel.injectable"; + +const appPathsChannelInjectable = getInjectable({ + instantiate: (di) => { + const registerChannel = di.inject(registerChannelInjectable); + + return registerChannel(appPathsIpcChannel); + }, + injectionToken: appPathsInjectionChannelToken, + lifecycle: lifecycleEnum.singleton, +}); + +export default appPathsChannelInjectable; diff --git a/src/renderer/app-paths/app-paths.injectable.ts b/src/renderer/app-paths/app-paths.injectable.ts index a645b9a811..0a3398aab1 100644 --- a/src/renderer/app-paths/app-paths.injectable.ts +++ b/src/renderer/app-paths/app-paths.injectable.ts @@ -3,24 +3,20 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import { AppPaths, appPathsInjectionToken, appPathsIpcChannel } from "../../common/app-paths/app-path-injection-token"; -import getValueFromRegisteredChannelInjectable from "./get-value-from-registered-channel/get-value-from-registered-channel.injectable"; +import { appPathsInjectionChannelToken } from "../../common/app-paths/app-path-channel-injection-token"; +import { appPathsInjectionToken } from "../../common/app-paths/app-path-injection-token"; +import type { AppPaths } from "../../common/app-paths/app-paths"; let syncAppPaths: AppPaths; const appPathsInjectable = getInjectable({ setup: async (di) => { - const getValueFromRegisteredChannel = di.inject( - getValueFromRegisteredChannelInjectable, - ); + const appPathsChannel = di.inject(appPathsInjectionChannelToken); - syncAppPaths = await getValueFromRegisteredChannel(appPathsIpcChannel); + syncAppPaths = await appPathsChannel(); }, - instantiate: () => syncAppPaths, - injectionToken: appPathsInjectionToken, - lifecycle: lifecycleEnum.singleton, }); diff --git a/src/renderer/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.injectable.ts b/src/renderer/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.injectable.ts deleted file mode 100644 index e1f6cfec12..0000000000 --- a/src/renderer/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.injectable.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import ipcRendererInjectable from "./ipc-renderer/ipc-renderer.injectable"; -import { getValueFromRegisteredChannel } from "./get-value-from-registered-channel"; - -const getValueFromRegisteredChannelInjectable = getInjectable({ - instantiate: (di) => - getValueFromRegisteredChannel({ ipcRenderer: di.inject(ipcRendererInjectable) }), - - lifecycle: lifecycleEnum.singleton, -}); - -export default getValueFromRegisteredChannelInjectable; diff --git a/src/renderer/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.ts b/src/renderer/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.ts deleted file mode 100644 index 0f3b44c569..0000000000 --- a/src/renderer/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { IpcRenderer } from "electron"; -import type { Channel } from "../../../common/ipc-channel/channel"; - -interface Dependencies { - ipcRenderer: IpcRenderer; -} - -export const getValueFromRegisteredChannel = - ({ ipcRenderer }: Dependencies) => - , TInstance>( - channel: TChannel, - ): Promise => - ipcRenderer.invoke(channel.name); diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 5593134586..796724c64a 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -30,7 +30,6 @@ import { DiContextProvider } from "@ogre-tools/injectable-react"; import type { DependencyInjectionContainer } from "@ogre-tools/injectable"; import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable"; import extensionDiscoveryInjectable from "../extensions/extension-discovery/extension-discovery.injectable"; -import extensionInstallationStateStoreInjectable from "../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable"; import userStoreInjectable from "../common/user-store/user-store.injectable"; import initRootFrameInjectable from "./frames/root-frame/init-root-frame/init-root-frame.injectable"; @@ -114,10 +113,6 @@ export async function bootstrap(di: DependencyInjectionContainer) { WeblinkStore.createInstance(); - const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable); - - extensionInstallationStateStore.bindIpcListeners(); - HelmRepoManager.createInstance(); // initialize the manager // Register additional store listeners diff --git a/src/renderer/communication/ipc-invoke.injectable.ts b/src/renderer/communication/ipc-invoke.injectable.ts new file mode 100644 index 0000000000..ba811ca0c6 --- /dev/null +++ b/src/renderer/communication/ipc-invoke.injectable.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import ipcRendererInjectable from "./ipc-renderer.injectable"; + +const ipcInvokeInjectable = getInjectable({ + instantiate: (di) => { + const ipcRenderer = di.inject(ipcRendererInjectable); + + return (channel: string, ...args: any[]) => ipcRenderer.invoke(channel, ...args); + }, + lifecycle: lifecycleEnum.singleton, +}); + +export default ipcInvokeInjectable; diff --git a/src/renderer/communication/ipc-on.injectable.ts b/src/renderer/communication/ipc-on.injectable.ts new file mode 100644 index 0000000000..146e73a08c --- /dev/null +++ b/src/renderer/communication/ipc-on.injectable.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { IpcRendererEvent } from "electron"; +import { ipcOnEventInjectionToken } from "../../common/communication/ipc-on-event-injection-token"; +import ipcRendererInjectable from "./ipc-renderer.injectable"; + +const ipcOnInjectable = getInjectable({ + instantiate: (di) => { + const ipcRenderer = di.inject(ipcRendererInjectable); + + return (channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void): void => { + ipcRenderer.on(channel, listener); + }; + }, + injectionToken: ipcOnEventInjectionToken, + lifecycle: lifecycleEnum.singleton, +}); + +export default ipcOnInjectable; diff --git a/src/renderer/app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable.ts b/src/renderer/communication/ipc-renderer.injectable.ts similarity index 100% rename from src/renderer/app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable.ts rename to src/renderer/communication/ipc-renderer.injectable.ts diff --git a/src/renderer/communication/register-channel.injectable.ts b/src/renderer/communication/register-channel.injectable.ts new file mode 100644 index 0000000000..b60d76e2c5 --- /dev/null +++ b/src/renderer/communication/register-channel.injectable.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import ipcInvokeInjectable from "./ipc-invoke.injectable"; +import type { Channel } from "../../common/communication/channel"; + +interface Dependencies { + invoke: (channel: string, ...args: any[]) => any; +} + +function registerChannel({ invoke }: Dependencies) { + return function (name: string): Channel { + return (...args: Parameters) => invoke(name, ...args); + }; +} + +const registerChannelInjectable = getInjectable({ + instantiate: (di) => registerChannel({ + invoke: di.inject(ipcInvokeInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default registerChannelInjectable; diff --git a/src/renderer/components/+catalog/__tests__/custom-columns.test.ts b/src/renderer/components/+catalog/__tests__/custom-columns.test.ts index 00fd5e033b..05867369eb 100644 --- a/src/renderer/components/+catalog/__tests__/custom-columns.test.ts +++ b/src/renderer/components/+catalog/__tests__/custom-columns.test.ts @@ -37,8 +37,8 @@ class TestCategory extends CatalogCategory { describe("Custom Category Columns", () => { let di: ConfigurableDependencyInjectionContainer; - beforeEach(() => { - di = getDiForUnitTesting(); + beforeEach(async () => { + di = await getDiForUnitTesting(); }); describe("without extensions", () => { diff --git a/src/renderer/components/+catalog/__tests__/custom-views.test.ts b/src/renderer/components/+catalog/__tests__/custom-views.test.ts index af2dad0f55..fba547fd59 100644 --- a/src/renderer/components/+catalog/__tests__/custom-views.test.ts +++ b/src/renderer/components/+catalog/__tests__/custom-views.test.ts @@ -15,8 +15,8 @@ import customCategoryViewsInjectable from "../custom-views.injectable"; describe("Custom Category Views", () => { let di: ConfigurableDependencyInjectionContainer; - beforeEach(() => { - di = getDiForUnitTesting(); + beforeEach(async () => { + di = await getDiForUnitTesting(); }); it("should order items correctly over all extensions", () => { diff --git a/src/renderer/components/+catalog/catalog.test.tsx b/src/renderer/components/+catalog/catalog.test.tsx index e0ae3d217e..776935e98e 100644 --- a/src/renderer/components/+catalog/catalog.test.tsx +++ b/src/renderer/components/+catalog/catalog.test.tsx @@ -101,7 +101,7 @@ describe("", () => { let render: DiRender; beforeEach(async () => { - di = getDiForUnitTesting({ doGeneralOverrides: true }); + di = await getDiForUnitTesting({ doGeneralOverrides: true }); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); diff --git a/src/renderer/components/+extensions/__tests__/attempt-install.test.ts b/src/renderer/components/+extensions/__tests__/attempt-install.test.ts new file mode 100644 index 0000000000..b9cacb8e48 --- /dev/null +++ b/src/renderer/components/+extensions/__tests__/attempt-install.test.ts @@ -0,0 +1,51 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable"; +import removeDirInjectable from "../../../../common/fs/remove-dir.injectable"; +import getInstalledExtensionInjectable from "../../../../extensions/extension-loader/get-installed-extension.injectable"; +import { InstallationState } from "../../../../extensions/installation-state/state"; +import { getDisForUnitTesting } from "../../../../test-utils/get-dis-for-unit-testing"; +import getInstallationStateInjectable from "../../../extensions/installation-state/get-installation-state.injectable"; +import { noop } from "../../../utils"; +import attemptInstallInjectable from "../attempt-install/attempt-install.injectable"; +import createTempFilesAndValidateInjectable from "../attempt-install/create-temp-files-and-validate/create-temp-files-and-validate.injectable"; +import unpackExtensionInjectable from "../attempt-install/unpack-extension/unpack-extension.injectable"; + +describe("attemptInstall()", () => { + let rendererDi: ConfigurableDependencyInjectionContainer; + + beforeEach(async () => { + const dis = await getDisForUnitTesting({ doGeneralOverrides: true }); + + rendererDi = dis.rendererDi; + + await dis.runSetups(); + }); + + it("should attempt to remove any broken remnants of a previous install", async () => { + const removeDir = jest.fn(); + + rendererDi.override(createTempFilesAndValidateInjectable, () => ({ fileName }) => Promise.resolve({ + fileName, + data: Buffer.from([]), + id: "some-extension-id", + manifest: { + name: "some-extension-name", + version: "1.0.0", + }, + tempFile: "/some-fole-path", + })); + rendererDi.override(getInstallationStateInjectable, () => () => InstallationState.IDLE); + rendererDi.override(getInstalledExtensionInjectable, () => () => undefined); + rendererDi.override(unpackExtensionInjectable, () => () => Promise.resolve()); + rendererDi.override(removeDirInjectable, () => removeDir); + + const attemptInstall = rendererDi.inject(attemptInstallInjectable); + + await attemptInstall({ fileName: "foobar", dataP: Promise.resolve(Buffer.from([])) }, noop); + expect(removeDir).toBeCalledTimes(1); + }); +}); diff --git a/src/renderer/components/+extensions/__tests__/extensions.test.tsx b/src/renderer/components/+extensions/__tests__/extensions.test.tsx index 8589aa80e7..7dd29eabf9 100644 --- a/src/renderer/components/+extensions/__tests__/extensions.test.tsx +++ b/src/renderer/components/+extensions/__tests__/extensions.test.tsx @@ -48,7 +48,7 @@ describe("Extensions", () => { let render: DiRender; beforeEach(async () => { - const di = getDiForUnitTesting({ doGeneralOverrides: true }); + const di = await getDiForUnitTesting({ doGeneralOverrides: true }); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); di.override(directoryForDownloadsInjectable, () => "some-directory-for-downloads"); diff --git a/src/renderer/components/+extensions/attempt-install-by-info/attempt-install-by-info.injectable.ts b/src/renderer/components/+extensions/attempt-install-by-info/attempt-install-by-info.injectable.ts index a554419ba5..429c4be43f 100644 --- a/src/renderer/components/+extensions/attempt-install-by-info/attempt-install-by-info.injectable.ts +++ b/src/renderer/components/+extensions/attempt-install-by-info/attempt-install-by-info.injectable.ts @@ -6,15 +6,14 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { attemptInstallByInfo } from "./attempt-install-by-info"; import attemptInstallInjectable from "../attempt-install/attempt-install.injectable"; import getBaseRegistryUrlInjectable from "../get-base-registry-url/get-base-registry-url.injectable"; -import extensionInstallationStateStoreInjectable - from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; +import startPreInstallInjectable from "../../../extensions/installation-state/start-pre-install.injectable"; const attemptInstallByInfoInjectable = getInjectable({ instantiate: (di) => attemptInstallByInfo({ attemptInstall: di.inject(attemptInstallInjectable), getBaseRegistryUrl: di.inject(getBaseRegistryUrlInjectable), - extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), + startPreInstall: di.inject(startPreInstallInjectable), }), lifecycle: lifecycleEnum.singleton, diff --git a/src/renderer/components/+extensions/attempt-install-by-info/attempt-install-by-info.tsx b/src/renderer/components/+extensions/attempt-install-by-info/attempt-install-by-info.tsx index fad00508c1..d3e15618dc 100644 --- a/src/renderer/components/+extensions/attempt-install-by-info/attempt-install-by-info.tsx +++ b/src/renderer/components/+extensions/attempt-install-by-info/attempt-install-by-info.tsx @@ -2,7 +2,7 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { downloadFile, downloadJson, ExtendableDisposer } from "../../../../common/utils"; +import { Disposer, downloadFile, downloadJson } from "../../../../common/utils"; import { Notifications } from "../../notifications"; import { ConfirmDialog } from "../../confirm-dialog"; import React from "react"; @@ -11,7 +11,6 @@ import { SemVer } from "semver"; import URLParse from "url-parse"; import type { InstallRequest } from "../attempt-install/install-request"; import lodash from "lodash"; -import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store"; export interface ExtensionInfo { name: string; @@ -20,17 +19,19 @@ export interface ExtensionInfo { } interface Dependencies { - attemptInstall: (request: InstallRequest, d: ExtendableDisposer) => Promise; + attemptInstall: (request: InstallRequest, d: Disposer) => Promise; getBaseRegistryUrl: () => Promise; - extensionInstallationStateStore: ExtensionInstallationStateStore + startPreInstall: () => Disposer; } -export const attemptInstallByInfo = ({ attemptInstall, getBaseRegistryUrl, extensionInstallationStateStore }: Dependencies) => async ({ - name, - version, - requireConfirmation = false, -}: ExtensionInfo) => { - const disposer = extensionInstallationStateStore.startPreInstall(); +export const attemptInstallByInfo = ({ attemptInstall, getBaseRegistryUrl, startPreInstall }: Dependencies) => async ( + { + name, + version, + requireConfirmation = false, + }: ExtensionInfo, + disposer = startPreInstall(), +) => { const baseUrl = await getBaseRegistryUrl(); const registryUrl = new URLParse(baseUrl).set("pathname", name).toString(); let json: any; diff --git a/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.ts b/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.ts index a6ddf1c690..072e38a6d8 100644 --- a/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.ts +++ b/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.ts @@ -3,25 +3,25 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable"; import uninstallExtensionInjectable from "../uninstall-extension/uninstall-extension.injectable"; import { attemptInstall } from "./attempt-install"; import unpackExtensionInjectable from "./unpack-extension/unpack-extension.injectable"; -import getExtensionDestFolderInjectable - from "./get-extension-dest-folder/get-extension-dest-folder.injectable"; +import getExtensionDestFolderInjectable from "./get-extension-dest-folder/get-extension-dest-folder.injectable"; import createTempFilesAndValidateInjectable from "./create-temp-files-and-validate/create-temp-files-and-validate.injectable"; -import extensionInstallationStateStoreInjectable - from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; +import removeDirInjectable from "../../../../common/fs/remove-dir.injectable"; +import getInstallationStateInjectable from "../../../extensions/installation-state/get-installation-state.injectable"; +import getInstalledExtensionInjectable from "../../../../extensions/extension-loader/get-installed-extension.injectable"; const attemptInstallInjectable = getInjectable({ instantiate: (di) => attemptInstall({ - extensionLoader: di.inject(extensionLoaderInjectable), + getInstalledExtension: di.inject(getInstalledExtensionInjectable), uninstallExtension: di.inject(uninstallExtensionInjectable), unpackExtension: di.inject(unpackExtensionInjectable), createTempFilesAndValidate: di.inject(createTempFilesAndValidateInjectable), getExtensionDestFolder: di.inject(getExtensionDestFolderInjectable), - extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), + getInstallationState: di.inject(getInstallationStateInjectable), + removeDir: di.inject(removeDirInjectable), }), lifecycle: lifecycleEnum.singleton, diff --git a/src/renderer/components/+extensions/attempt-install/attempt-install.tsx b/src/renderer/components/+extensions/attempt-install/attempt-install.tsx index 97b5f46ad2..eae12a70bd 100644 --- a/src/renderer/components/+extensions/attempt-install/attempt-install.tsx +++ b/src/renderer/components/+extensions/attempt-install/attempt-install.tsx @@ -2,70 +2,51 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { - Disposer, - disposer, - ExtendableDisposer, -} from "../../../../common/utils"; +import type { Disposer } from "../../../../common/utils"; import { Notifications } from "../../notifications"; import { Button } from "../../button"; -import type { ExtensionLoader } from "../../../../extensions/extension-loader"; import type { LensExtensionId } from "../../../../extensions/lens-extension"; import React from "react"; -import { remove as removeDir } from "fs-extra"; import { shell } from "electron"; import type { InstallRequestValidated } from "./create-temp-files-and-validate/create-temp-files-and-validate"; import type { InstallRequest } from "./install-request"; -import { - ExtensionInstallationState, - ExtensionInstallationStateStore, -} from "../../../../extensions/extension-installation-state-store/extension-installation-state-store"; +import { InstallationState } from "../../../../extensions/installation-state/state"; +import type { InstalledExtension } from "../../../../extensions/extension-discovery/extension-discovery"; interface Dependencies { - extensionLoader: ExtensionLoader; + getInstalledExtension: (extId: string) => InstalledExtension | undefined; uninstallExtension: (id: LensExtensionId) => Promise; - - unpackExtension: ( - request: InstallRequestValidated, - disposeDownloading: Disposer, - ) => Promise; - - createTempFilesAndValidate: ( - installRequest: InstallRequest, - ) => Promise; - - getExtensionDestFolder: (name: string) => string - - extensionInstallationStateStore: ExtensionInstallationStateStore + unpackExtension: (request: InstallRequestValidated, disposeDownloading: Disposer) => Promise; + createTempFilesAndValidate: (installRequest: InstallRequest) => Promise; + getExtensionDestFolder: (name: string) => string; + getInstallationState: (extId: string) => InstallationState; + removeDir: (dir: string) => Promise; } export const attemptInstall = ({ - extensionLoader, + getInstalledExtension, uninstallExtension, unpackExtension, createTempFilesAndValidate, getExtensionDestFolder, - extensionInstallationStateStore, + getInstallationState, + removeDir, }: Dependencies) => - async (request: InstallRequest, d?: ExtendableDisposer): Promise => { - const dispose = disposer( - extensionInstallationStateStore.startPreInstall(), - d, - ); - + async (request: InstallRequest, dispose: Disposer): Promise => { const validatedRequest = await createTempFilesAndValidate(request); if (!validatedRequest) { return dispose(); } - const { name, version, description } = validatedRequest.manifest; - const curState = extensionInstallationStateStore.getInstallationState( - validatedRequest.id, - ); + const { + id, + manifest: { name, version, description }, + } = validatedRequest; + const curState = getInstallationState(id); - if (curState !== ExtensionInstallationState.IDLE) { + if (curState !== InstallationState.IDLE) { dispose(); return void Notifications.error( @@ -80,31 +61,26 @@ export const attemptInstall = } const extensionFolder = getExtensionDestFolder(name); - const installedExtension = extensionLoader.getExtension(validatedRequest.id); + const installedExtension = getInstalledExtension(id); if (installedExtension) { - const { version: oldVersion } = installedExtension.manifest; + const { manifest: { version: oldVersion }} = installedExtension; - // confirm to uninstall old version before installing new version + // confirm uninstall and then install new version const removeNotification = Notifications.info(

- Install extension{" "} - - {name}@{version} - - ? + Install extension{name}@{version}?

- Description: {description} + Description: {description}

shell.openPath(extensionFolder)} > - Warning: {name}@{oldVersion} will be removed before - installation. + Warning: {name}@{oldVersion} will be removed before installation.
- - - Pro-Tip: you can drag-n-drop tarball-file to this area - - -); + isCurrentlyIdle, +}: Dependencies & InstallProps) => { + const showAsWaiting = isCurrentlyIdle.get(); + const formats = supportedFormats.join(", "); -export const Install = withInjectables( - observer(NonInjectedInstall), - { - getProps: (di, props) => ({ - extensionInstallationStateStore: di.inject( - extensionInstallationStateStoreInjectable, - ), + return ( +
+ +
+
+ + } + /> +
+
+
+
+ + Pro-Tip: you can drag-n-drop tarball-file to this area + +
+ ); +}); - ...props, - }), - }, -); +export const Install = withInjectables(NonInjectedInstall, { + getProps: (di, props) => ({ + isCurrentlyIdle: di.inject(isCurrentlyIdleInjectable), + installFromInput: di.inject(installFromInputInjectable), + installFromSelectFileDialog: di.inject(installFromSelectFileDialogInjectable), + ...props, + }), +}); diff --git a/src/renderer/components/+extensions/installed-extensions.tsx b/src/renderer/components/+extensions/installed-extensions.tsx index 6780a0df3a..df5ddc666d 100644 --- a/src/renderer/components/+extensions/installed-extensions.tsx +++ b/src/renderer/components/+extensions/installed-extensions.tsx @@ -5,10 +5,7 @@ import styles from "./installed-extensions.module.scss"; import React, { useMemo } from "react"; -import type { - ExtensionDiscovery, - InstalledExtension, -} from "../../../extensions/extension-discovery/extension-discovery"; +import type { ExtensionDiscovery, InstalledExtension } from "../../../extensions/extension-discovery/extension-discovery"; import { Icon } from "../icon"; import { List } from "../list/list"; import { MenuActions, MenuItem } from "../menu"; @@ -17,15 +14,13 @@ import { cssNames } from "../../utils"; import { observer } from "mobx-react"; import type { Row } from "react-table"; import type { LensExtensionId } from "../../../extensions/lens-extension"; -import extensionDiscoveryInjectable - from "../../../extensions/extension-discovery/extension-discovery.injectable"; - +import extensionDiscoveryInjectable from "../../../extensions/extension-discovery/extension-discovery.injectable"; import { withInjectables } from "@ogre-tools/injectable-react"; -import extensionInstallationStateStoreInjectable - from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; -import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store"; +import type { IComputedValue } from "mobx"; +import isUninstallingInjectable from "../../extensions/installation-state/is-uninstalling.injectable"; +import anyExtensionsUninstallingInjectable from "../../extensions/installation-state/any-uninstalling.injectable"; -interface Props { +export interface InstalledExtensionsProps { extensions: InstalledExtension[]; enable: (id: LensExtensionId) => void; disable: (id: LensExtensionId) => void; @@ -34,7 +29,8 @@ interface Props { interface Dependencies { extensionDiscovery: ExtensionDiscovery; - extensionInstallationStateStore: ExtensionInstallationStateStore; + isUninstalling: (extId: string) => boolean; + anyUninstalling: IComputedValue; } function getStatus(extension: InstalledExtension) { @@ -45,7 +41,7 @@ function getStatus(extension: InstalledExtension) { return extension.isEnabled ? "Enabled" : "Disabled"; } -const NonInjectedInstalledExtensions : React.FC = (({ extensionDiscovery, extensionInstallationStateStore, extensions, uninstall, enable, disable }) => { +const NonInjectedInstalledExtensions = observer(({ extensionDiscovery, isUninstalling, anyUninstalling, extensions, uninstall, enable, disable }: Dependencies & InstalledExtensionsProps) => { const filters = [ (extension: InstalledExtension) => extension.manifest.name, (extension: InstalledExtension) => getStatus(extension), @@ -86,65 +82,61 @@ const NonInjectedInstalledExtensions : React.FC = (({ exte ], [], ); - const data = useMemo( - () => { - return extensions.map(extension => { - const { id, isEnabled, isCompatible, manifest } = extension; - const { name, description, version } = manifest; - const isUninstalling = extensionInstallationStateStore.isExtensionUninstalling(id); + const data = useMemo(() => extensions.map(extension => { + const { id, isEnabled, isCompatible, manifest } = extension; + const { name, description, version } = manifest; + const uninstalling = isUninstalling(id); - return { - extension: ( -
-
-
{name}
-
{description}
-
-
- ), - version, - status: ( -
- {getStatus(extension)} -
- ), - actions: ( - - { isCompatible && ( - <> - {isEnabled ? ( - disable(id)} - > - - Disable - - ) : ( - enable(id)} - > - - Enable - - )} - + return { + extension: ( +
+
+
{name}
+
{description}
+
+
+ ), + version, + status: ( +
+ {getStatus(extension)} +
+ ), + actions: ( + + {isCompatible && ( + <> + {isEnabled ? ( + disable(id)} + > + + Disable + + ) : ( + enable(id)} + > + + Enable + )} + + )} - uninstall(extension)} - > - - Uninstall - - - ), - }; - }); - }, [extensions, extensionInstallationStateStore.anyUninstalling], - ); + uninstall(extension)} + > + + Uninstall + +
+ ), + }; + }), [extensions, anyUninstalling.get()]); if (!extensionDiscovery.isLoaded) { return
; @@ -175,15 +167,11 @@ const NonInjectedInstalledExtensions : React.FC = (({ exte ); }); -export const InstalledExtensions = withInjectables( - observer(NonInjectedInstalledExtensions), - - { - getProps: (di, props) => ({ - extensionDiscovery: di.inject(extensionDiscoveryInjectable), - extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), - - ...props, - }), - }, -); +export const InstalledExtensions = withInjectables(NonInjectedInstalledExtensions, { + getProps: (di, props) => ({ + extensionDiscovery: di.inject(extensionDiscoveryInjectable), + isUninstalling: di.inject(isUninstallingInjectable), + anyUninstalling: di.inject(anyExtensionsUninstallingInjectable), + ...props, + }), +}); diff --git a/src/renderer/components/+extensions/uninstall-extension/uninstall-extension.injectable.ts b/src/renderer/components/+extensions/uninstall-extension/uninstall-extension.injectable.ts index 7bd2887842..0edc768242 100644 --- a/src/renderer/components/+extensions/uninstall-extension/uninstall-extension.injectable.ts +++ b/src/renderer/components/+extensions/uninstall-extension/uninstall-extension.injectable.ts @@ -5,17 +5,17 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable"; import { uninstallExtension } from "./uninstall-extension"; -import extensionInstallationStateStoreInjectable - from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; -import extensionDiscoveryInjectable - from "../../../../extensions/extension-discovery/extension-discovery.injectable"; +import extensionDiscoveryInjectable from "../../../../extensions/extension-discovery/extension-discovery.injectable"; +import clearUninstallingInjectable from "../../../extensions/installation-state/clear-uninstalling.injectable"; +import setUninstallingInjectable from "../../../extensions/installation-state/set-uninstalling.injectable"; const uninstallExtensionInjectable = getInjectable({ instantiate: (di) => uninstallExtension({ extensionLoader: di.inject(extensionLoaderInjectable), extensionDiscovery: di.inject(extensionDiscoveryInjectable), - extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), + setUninstalling: di.inject(setUninstallingInjectable), + clearUninstalling: di.inject(clearUninstallingInjectable), }), lifecycle: lifecycleEnum.singleton, diff --git a/src/renderer/components/+extensions/uninstall-extension/uninstall-extension.tsx b/src/renderer/components/+extensions/uninstall-extension/uninstall-extension.tsx index f4ac3e12a7..d1649b05f0 100644 --- a/src/renderer/components/+extensions/uninstall-extension/uninstall-extension.tsx +++ b/src/renderer/components/+extensions/uninstall-extension/uninstall-extension.tsx @@ -10,53 +10,53 @@ import { Notifications } from "../../notifications"; import React from "react"; import { when } from "mobx"; import { getMessageFromError } from "../get-message-from-error/get-message-from-error"; -import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store"; interface Dependencies { - extensionLoader: ExtensionLoader - extensionDiscovery: ExtensionDiscovery - extensionInstallationStateStore: ExtensionInstallationStateStore + extensionLoader: ExtensionLoader; + extensionDiscovery: ExtensionDiscovery; + setUninstalling: (extId: string) => void; + clearUninstalling: (extId: string) => void; } -export const uninstallExtension = - ({ extensionLoader, extensionDiscovery, extensionInstallationStateStore }: Dependencies) => - async (extensionId: LensExtensionId): Promise => { - const { manifest } = extensionLoader.getExtension(extensionId); - const displayName = extensionDisplayName(manifest.name, manifest.version); +export const uninstallExtension = ({ extensionLoader, extensionDiscovery, setUninstalling, clearUninstalling }: Dependencies) => ( + async (extensionId: LensExtensionId): Promise => { + const { manifest } = extensionLoader.getExtension(extensionId); + const displayName = extensionDisplayName(manifest.name, manifest.version); - try { - logger.debug(`[EXTENSIONS]: trying to uninstall ${extensionId}`); - extensionInstallationStateStore.setUninstalling(extensionId); + try { + logger.debug(`[EXTENSIONS]: trying to uninstall ${extensionId}`); + setUninstalling(extensionId); - await extensionDiscovery.uninstallExtension(extensionId); + await extensionDiscovery.uninstallExtension(extensionId); - // wait for the ExtensionLoader to actually uninstall the extension - await when(() => !extensionLoader.userExtensions.has(extensionId)); + // wait for the ExtensionLoader to actually uninstall the extension + await when(() => !extensionLoader.userExtensions.has(extensionId)); - Notifications.ok( -

+ Notifications.ok( +

Extension {displayName} successfully uninstalled! -

, - ); +

, + ); - return true; - } catch (error) { - const message = getMessageFromError(error); + return true; + } catch (error) { + const message = getMessageFromError(error); - logger.info( - `[EXTENSION-UNINSTALL]: uninstalling ${displayName} has failed: ${error}`, - { error }, - ); - Notifications.error( -

+ logger.info( + `[EXTENSION-UNINSTALL]: uninstalling ${displayName} has failed: ${error}`, + { error }, + ); + Notifications.error( +

Uninstalling extension {displayName} has failed:{" "} - {message} -

, - ); + {message} +

, + ); - return false; - } finally { + return false; + } finally { // Remove uninstall state on uninstall failure - extensionInstallationStateStore.clearUninstalling(extensionId); - } - }; + clearUninstalling(extensionId); + } + } +); diff --git a/src/renderer/components/+user-management/+cluster-role-bindings/__tests__/dialog.test.tsx b/src/renderer/components/+user-management/+cluster-role-bindings/__tests__/dialog.test.tsx index fedd40d181..a56a915505 100644 --- a/src/renderer/components/+user-management/+cluster-role-bindings/__tests__/dialog.test.tsx +++ b/src/renderer/components/+user-management/+cluster-role-bindings/__tests__/dialog.test.tsx @@ -17,7 +17,7 @@ describe("ClusterRoleBindingDialog tests", () => { let render: DiRender; beforeEach(async () => { - const di = getDiForUnitTesting({ doGeneralOverrides: true }); + const di = await getDiForUnitTesting({ doGeneralOverrides: true }); await di.runSetups(); diff --git a/src/renderer/components/+user-management/+role-bindings/__tests__/dialog.test.tsx b/src/renderer/components/+user-management/+role-bindings/__tests__/dialog.test.tsx index 470a7006af..5aeadf1edf 100644 --- a/src/renderer/components/+user-management/+role-bindings/__tests__/dialog.test.tsx +++ b/src/renderer/components/+user-management/+role-bindings/__tests__/dialog.test.tsx @@ -20,7 +20,7 @@ describe("RoleBindingDialog tests", () => { let render: DiRender; beforeEach(async () => { - const di = getDiForUnitTesting({ doGeneralOverrides: true }); + const di = await getDiForUnitTesting({ doGeneralOverrides: true }); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); diff --git a/src/renderer/components/+welcome/__test__/welcome.test.tsx b/src/renderer/components/+welcome/__test__/welcome.test.tsx index 5b62bb6140..69b05fcd27 100644 --- a/src/renderer/components/+welcome/__test__/welcome.test.tsx +++ b/src/renderer/components/+welcome/__test__/welcome.test.tsx @@ -31,7 +31,7 @@ describe("", () => { let welcomeBannersStub: WelcomeBannerRegistration[]; beforeEach(async () => { - di = getDiForUnitTesting({ doGeneralOverrides: true }); + di = await getDiForUnitTesting({ doGeneralOverrides: true }); await di.runSetups(); diff --git a/src/renderer/components/+workloads-pods/__tests__/pod-tolerations.test.tsx b/src/renderer/components/+workloads-pods/__tests__/pod-tolerations.test.tsx index 69102ca38b..c518669fc5 100644 --- a/src/renderer/components/+workloads-pods/__tests__/pod-tolerations.test.tsx +++ b/src/renderer/components/+workloads-pods/__tests__/pod-tolerations.test.tsx @@ -37,7 +37,7 @@ describe("", () => { let render: DiRender; beforeEach(async () => { - const di = getDiForUnitTesting({ doGeneralOverrides: true }); + const di = await getDiForUnitTesting({ doGeneralOverrides: true }); di.override( directoryForLensLocalStorageInjectable, diff --git a/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx b/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx index fad58d03c5..01cbb59842 100644 --- a/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx +++ b/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx @@ -89,7 +89,7 @@ describe("", () => { let createCluster: (model: ClusterModel) => Cluster; beforeEach(async () => { - const { mainDi, runSetups } = getDisForUnitTesting({ doGeneralOverrides: true }); + const { mainDi, runSetups } = await getDisForUnitTesting({ doGeneralOverrides: true }); mockFs(); diff --git a/src/renderer/components/dock/__test__/dock-tabs.test.tsx b/src/renderer/components/dock/__test__/dock-tabs.test.tsx index 4be169bf7f..ebde8b640a 100644 --- a/src/renderer/components/dock/__test__/dock-tabs.test.tsx +++ b/src/renderer/components/dock/__test__/dock-tabs.test.tsx @@ -59,7 +59,7 @@ describe("", () => { let render: DiRender; beforeEach(async () => { - const di = getDiForUnitTesting({ doGeneralOverrides: true }); + const di = await getDiForUnitTesting({ doGeneralOverrides: true }); render = renderFor(di); diff --git a/src/renderer/components/dock/logs/__test__/log-resource-selector.test.tsx b/src/renderer/components/dock/logs/__test__/log-resource-selector.test.tsx index b5923b98a0..59ca81f24d 100644 --- a/src/renderer/components/dock/logs/__test__/log-resource-selector.test.tsx +++ b/src/renderer/components/dock/logs/__test__/log-resource-selector.test.tsx @@ -122,7 +122,7 @@ describe("", () => { let render: DiRender; beforeEach(async () => { - const di = getDiForUnitTesting({ doGeneralOverrides: true }); + const di = await getDiForUnitTesting({ doGeneralOverrides: true }); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); di.override(callForLogsInjectable, () => () => Promise.resolve("some-logs")); diff --git a/src/renderer/components/dock/logs/__test__/log-search.test.tsx b/src/renderer/components/dock/logs/__test__/log-search.test.tsx index 24c2001eec..7cf7ebdeaa 100644 --- a/src/renderer/components/dock/logs/__test__/log-search.test.tsx +++ b/src/renderer/components/dock/logs/__test__/log-search.test.tsx @@ -61,7 +61,7 @@ describe("LogSearch tests", () => { let render: DiRender; beforeEach(async () => { - const di = getDiForUnitTesting({ doGeneralOverrides: true }); + const di = await getDiForUnitTesting({ doGeneralOverrides: true }); render = renderFor(di); diff --git a/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx b/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx index 95963844c8..84fba1b48f 100644 --- a/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx +++ b/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx @@ -30,8 +30,8 @@ describe("", () => { let di: DependencyInjectionContainer; let render: DiRender; - beforeEach(() => { - di = getDiForUnitTesting({ doGeneralOverrides: true }); + beforeEach(async () => { + di = await getDiForUnitTesting({ doGeneralOverrides: true }); mockFs(); diff --git a/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx b/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx index ac6b55f36f..5236b070d5 100644 --- a/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx +++ b/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx @@ -75,7 +75,7 @@ describe("kube-object-menu", () => { const someTestExtension = new SomeTestExtension(kubeObjectMenuItems); - di = getDiForUnitTesting({ doGeneralOverrides: true }); + di = await getDiForUnitTesting({ doGeneralOverrides: true }); render = renderFor(di); diff --git a/src/renderer/components/layout/top-bar/top-bar-win-linux.test.tsx b/src/renderer/components/layout/top-bar/top-bar-win-linux.test.tsx index 9b557a2f6c..415511f6fb 100644 --- a/src/renderer/components/layout/top-bar/top-bar-win-linux.test.tsx +++ b/src/renderer/components/layout/top-bar/top-bar-win-linux.test.tsx @@ -35,7 +35,7 @@ describe(" in Windows and Linux", () => { let render: DiRender; beforeEach(async () => { - const di = getDiForUnitTesting({ doGeneralOverrides: true }); + const di = await getDiForUnitTesting({ doGeneralOverrides: true }); mockFs(); diff --git a/src/renderer/components/layout/top-bar/top-bar.test.tsx b/src/renderer/components/layout/top-bar/top-bar.test.tsx index db61d3e8d0..ddc32aa1e3 100644 --- a/src/renderer/components/layout/top-bar/top-bar.test.tsx +++ b/src/renderer/components/layout/top-bar/top-bar.test.tsx @@ -76,7 +76,7 @@ describe("", () => { let render: DiRender; beforeEach(async () => { - di = getDiForUnitTesting({ doGeneralOverrides: true }); + di = await getDiForUnitTesting({ doGeneralOverrides: true }); mockFs(); diff --git a/src/renderer/components/status-bar/status-bar.test.tsx b/src/renderer/components/status-bar/status-bar.test.tsx index 6055edf403..d0fcfee1b2 100644 --- a/src/renderer/components/status-bar/status-bar.test.tsx +++ b/src/renderer/components/status-bar/status-bar.test.tsx @@ -40,7 +40,7 @@ describe("", () => { beforeEach(async () => { statusBarItems = observable.array([]); - di = getDiForUnitTesting({ doGeneralOverrides: true }); + di = await getDiForUnitTesting({ doGeneralOverrides: true }); render = renderFor(di); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); diff --git a/src/renderer/extensions/installation-state/any-installing.injectable.ts b/src/renderer/extensions/installation-state/any-installing.injectable.ts new file mode 100644 index 0000000000..4fcdbaa891 --- /dev/null +++ b/src/renderer/extensions/installation-state/any-installing.injectable.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import extensionsInstallingInjectable from "./installing.injectable"; + +const anyExtensionsInstallingInjectable = getInjectable({ + instantiate: (di) => { + const state = di.inject(extensionsInstallingInjectable); + + return computed(() => state.size > 0); + }, + lifecycle: lifecycleEnum.singleton, +}); + +export default anyExtensionsInstallingInjectable; diff --git a/src/renderer/extensions/installation-state/any-pre-installing.injectable.ts b/src/renderer/extensions/installation-state/any-pre-installing.injectable.ts new file mode 100644 index 0000000000..414c60a219 --- /dev/null +++ b/src/renderer/extensions/installation-state/any-pre-installing.injectable.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import extensionsPreInstallingInjectable from "./pre-installing.injectable"; + +const anyExtensionsPreInstallingInjectable = getInjectable({ + instantiate: (di) => { + const state = di.inject(extensionsPreInstallingInjectable); + + return computed(() => state.size > 0); + }, + lifecycle: lifecycleEnum.singleton, +}); + +export default anyExtensionsPreInstallingInjectable; diff --git a/src/renderer/extensions/installation-state/any-uninstalling.injectable.ts b/src/renderer/extensions/installation-state/any-uninstalling.injectable.ts new file mode 100644 index 0000000000..3a8cd3e216 --- /dev/null +++ b/src/renderer/extensions/installation-state/any-uninstalling.injectable.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import extensionsUninstallingInjectable from "./uninstalling.injectable"; + +const anyExtensionsUninstallingInjectable = getInjectable({ + instantiate: (di) => { + const state = di.inject(extensionsUninstallingInjectable); + + return computed(() => state.size > 0); + }, + lifecycle: lifecycleEnum.singleton, +}); + +export default anyExtensionsUninstallingInjectable; diff --git a/src/renderer/extensions/installation-state/clear-installing.injectable.ts b/src/renderer/extensions/installation-state/clear-installing.injectable.ts new file mode 100644 index 0000000000..5a582d0425 --- /dev/null +++ b/src/renderer/extensions/installation-state/clear-installing.injectable.ts @@ -0,0 +1,60 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import installationStateLoggerInjectable from "../../../extensions/installation-state/logger.injectable"; +import { ClearInstalling, clearInstallingChannel, clearInstallingChannelInjectionToken } from "../../../extensions/installation-state/state-channels"; +import registerEventSinkInjectable from "../../../common/communication/register-event-sink.injectable"; +import type { ObservableSet } from "mobx"; +import type { LensLogger } from "../../../common/logger"; +import { InstallationState } from "../../../extensions/installation-state/state"; +import getInstallationStateInjectable from "./get-installation-state.injectable"; +import extensionsInstallingInjectable from "./installing.injectable"; + +interface Dependencies { + logger: LensLogger; + getInstallationState: (extId: string) => InstallationState; + state: ObservableSet; +} + +const nonInjectedClearInstalling = ({ logger, getInstallationState, state }: Dependencies) => ( + (extId: string): void => { + logger.debug(`trying to clear ${extId} as installing`); + + const curState = getInstallationState(extId); + + switch (curState) { + case InstallationState.INSTALLING: + return void state.delete(extId); + default: + throw new Error(`cannot clear INSTALLING state for ${extId}, it is currently ${curState}`); + } + } +); + +let channel: ClearInstalling; + +/** + * Strictly clears the INSTALLING state of an extension + * @param extId The ID of the extension + * @throws if state is not INSTALLING + */ +const clearInstallingChannelInjectable = getInjectable({ + setup: (di) => { + const registerEventSink = di.inject(registerEventSinkInjectable); + const logger = di.inject(installationStateLoggerInjectable); + const clearInstalling = nonInjectedClearInstalling({ + getInstallationState: di.inject(getInstallationStateInjectable), + state: di.inject(extensionsInstallingInjectable), + logger, + }); + + channel = registerEventSink(clearInstallingChannel, clearInstalling, logger); + }, + instantiate: () => channel, + injectionToken: clearInstallingChannelInjectionToken, + lifecycle: lifecycleEnum.singleton, +}); + +export default clearInstallingChannelInjectable; diff --git a/src/renderer/extensions/installation-state/clear-uninstalling.injectable.ts b/src/renderer/extensions/installation-state/clear-uninstalling.injectable.ts new file mode 100644 index 0000000000..81502d4e25 --- /dev/null +++ b/src/renderer/extensions/installation-state/clear-uninstalling.injectable.ts @@ -0,0 +1,48 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { ObservableSet } from "mobx"; +import type { LensLogger } from "../../../common/logger"; +import installationStateLoggerInjectable from "../../../extensions/installation-state/logger.injectable"; +import { InstallationState } from "../../../extensions/installation-state/state"; +import getInstallationStateInjectable from "./get-installation-state.injectable"; +import extensionsUninstallingInjectable from "./uninstalling.injectable"; + +interface Dependencies { + logger: LensLogger; + getInstallationState: (extId: string) => InstallationState; + state: ObservableSet; +} + +const clearUninstalling = ({ logger, getInstallationState, state }: Dependencies)=> ( + (extId: string): void => { + logger.debug(`trying to clear ${extId} as uninstalling`); + + const curState = getInstallationState(extId); + + switch (curState) { + case InstallationState.UNINSTALLING: + return void state.delete(extId); + default: + throw new Error(`cannot clear UNINSTALLING state for ${extId}, it is currently ${curState}`); + } + } +); + +/** + * Strictly clears the UNINSTALLING state of an extension + * @param extId The ID of the extension + * @throws if state is not UNINSTALLING + */ +const clearUninstallingInjectable = getInjectable({ + instantiate: (di) => clearUninstalling({ + getInstallationState: di.inject(getInstallationStateInjectable), + logger: di.inject(installationStateLoggerInjectable), + state: di.inject(extensionsUninstallingInjectable), + }), + lifecycle: lifecycleEnum.singleton, +}); + +export default clearUninstallingInjectable; diff --git a/src/renderer/extensions/installation-state/get-installation-state.injectable.ts b/src/renderer/extensions/installation-state/get-installation-state.injectable.ts new file mode 100644 index 0000000000..94c36d00df --- /dev/null +++ b/src/renderer/extensions/installation-state/get-installation-state.injectable.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { ObservableSet } from "mobx"; +import { InstallationState } from "../../../extensions/installation-state/state"; +import extensionsInstallingInjectable from "./installing.injectable"; +import extensionsUninstallingInjectable from "./uninstalling.injectable"; + +interface Dependencies { + installingState: ObservableSet; + uninstallingState: ObservableSet; +} + +const getInstallationState = ({ installingState, uninstallingState }: Dependencies) => ( + (extId: string): InstallationState => { + if (installingState.has(extId)) { + return InstallationState.INSTALLING; + } + + if (uninstallingState.has(extId)) { + return InstallationState.UNINSTALLING; + } + + return InstallationState.IDLE; + } +); + +const getInstallationStateInjectable = getInjectable({ + instantiate: (di) => getInstallationState({ + installingState: di.inject(extensionsInstallingInjectable), + uninstallingState: di.inject(extensionsUninstallingInjectable), + }), + lifecycle: lifecycleEnum.singleton, +}); + +export default getInstallationStateInjectable; diff --git a/src/renderer/extensions/installation-state/installing.injectable.ts b/src/renderer/extensions/installation-state/installing.injectable.ts new file mode 100644 index 0000000000..47f201a3d2 --- /dev/null +++ b/src/renderer/extensions/installation-state/installing.injectable.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { observable } from "mobx"; + +const extensionsInstallingInjectable = getInjectable({ + instantiate: () => observable.set(), + lifecycle: lifecycleEnum.singleton, +}); + +export default extensionsInstallingInjectable; diff --git a/src/renderer/extensions/installation-state/is-currently-idle.injectable.ts b/src/renderer/extensions/installation-state/is-currently-idle.injectable.ts new file mode 100644 index 0000000000..a71af9d903 --- /dev/null +++ b/src/renderer/extensions/installation-state/is-currently-idle.injectable.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import anyExtensionsInstallingInjectable from "./any-installing.injectable"; +import anyExtensionsPreInstallingInjectable from "./any-pre-installing.injectable"; +import anyExtensionsUninstallingInjectable from "./any-uninstalling.injectable"; + +const currentlyIdleInjectable = getInjectable({ + instantiate: (di) => { + const anyInstalling = di.inject(anyExtensionsInstallingInjectable); + const anyPreinstalling = di.inject(anyExtensionsPreInstallingInjectable); + const anyUninstalling = di.inject(anyExtensionsUninstallingInjectable); + + return computed(() => ( + anyInstalling.get() + || anyPreinstalling.get() + || anyUninstalling.get() + )); + }, + lifecycle: lifecycleEnum.singleton, +}); + +export default currentlyIdleInjectable; diff --git a/src/renderer/extensions/installation-state/is-installing.injectable.ts b/src/renderer/extensions/installation-state/is-installing.injectable.ts new file mode 100644 index 0000000000..2f62d52a47 --- /dev/null +++ b/src/renderer/extensions/installation-state/is-installing.injectable.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import extensionsInstallingInjectable from "./installing.injectable"; + +const isInstallingInjectable = getInjectable({ + instantiate: (di) => { + const state = di.inject(extensionsInstallingInjectable); + + return (extId: string) => state.has(extId); + }, + lifecycle: lifecycleEnum.singleton, +}); + +export default isInstallingInjectable; diff --git a/src/renderer/extensions/installation-state/is-uninstalling.injectable.ts b/src/renderer/extensions/installation-state/is-uninstalling.injectable.ts new file mode 100644 index 0000000000..e6dab67a42 --- /dev/null +++ b/src/renderer/extensions/installation-state/is-uninstalling.injectable.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import extensionsUninstallingInjectable from "./uninstalling.injectable"; + +const isUninstallingInjectable = getInjectable({ + instantiate: (di) => { + const state = di.inject(extensionsUninstallingInjectable); + + return (extId: string) => state.has(extId); + }, + lifecycle: lifecycleEnum.singleton, +}); + +export default isUninstallingInjectable; diff --git a/src/renderer/extensions/installation-state/pre-installing.injectable.ts b/src/renderer/extensions/installation-state/pre-installing.injectable.ts new file mode 100644 index 0000000000..26f322527a --- /dev/null +++ b/src/renderer/extensions/installation-state/pre-installing.injectable.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { observable } from "mobx"; + +const extensionsPreinstallingInjectable = getInjectable({ + instantiate: () => observable.set(), + lifecycle: lifecycleEnum.singleton, +}); + +export default extensionsPreinstallingInjectable; diff --git a/src/renderer/extensions/installation-state/set-installing.injectable.ts b/src/renderer/extensions/installation-state/set-installing.injectable.ts new file mode 100644 index 0000000000..e3bb75f9cd --- /dev/null +++ b/src/renderer/extensions/installation-state/set-installing.injectable.ts @@ -0,0 +1,59 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import installationStateLoggerInjectable from "../../../extensions/installation-state/logger.injectable"; +import { SetInstalling, setInstallingChannel, setInstallingChannelInjectionToken } from "../../../extensions/installation-state/state-channels"; +import registerEventSinkInjectable from "../../../common/communication/register-event-sink.injectable"; +import type { ObservableSet } from "mobx"; +import type { LensLogger } from "../../../common/logger"; +import { InstallationState } from "../../../extensions/installation-state/state"; +import getInstallationStateInjectable from "./get-installation-state.injectable"; +import extensionsInstallingInjectable from "./installing.injectable"; + +interface Dependencies { + logger: LensLogger; + getInstallationState: (extId: string) => InstallationState; + state: ObservableSet; +} + +const nonInjectedSetInstalling = ({ logger, getInstallationState, state }: Dependencies)=> ( + (extId: string): void => { + logger.debug(`trying to set ${extId} as installing`); + + const curState = getInstallationState(extId); + + if (curState !== InstallationState.IDLE) { + throw new Error(`cannot set ${extId} as installing. Is currently ${curState}.`); + } + + state.add(extId); + } +); + +let channel: SetInstalling; + +/** + * Strictly sets the INSTALLING state of an extension + * @param extId The ID of the extension + * @throws if state is not IDLE + */ +const setInstallingChannelInjectable = getInjectable({ + setup: (di) => { + const registerEventSink = di.inject(registerEventSinkInjectable); + const logger = di.inject(installationStateLoggerInjectable); + const setInstalling = nonInjectedSetInstalling({ + getInstallationState: di.inject(getInstallationStateInjectable), + state: di.inject(extensionsInstallingInjectable), + logger, + }); + + channel = registerEventSink(setInstallingChannel, setInstalling, logger); + }, + instantiate: () => channel, + injectionToken: setInstallingChannelInjectionToken, + lifecycle: lifecycleEnum.singleton, +}); + +export default setInstallingChannelInjectable; diff --git a/src/renderer/extensions/installation-state/set-uninstalling.injectable.ts b/src/renderer/extensions/installation-state/set-uninstalling.injectable.ts new file mode 100644 index 0000000000..71a7c2207d --- /dev/null +++ b/src/renderer/extensions/installation-state/set-uninstalling.injectable.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { ObservableSet } from "mobx"; +import type { LensLogger } from "../../../common/logger"; +import installationStateLoggerInjectable from "../../../extensions/installation-state/logger.injectable"; +import { InstallationState } from "../../../extensions/installation-state/state"; +import getInstallationStateInjectable from "./get-installation-state.injectable"; +import extensionsUninstallingInjectable from "./uninstalling.injectable"; + +interface Dependencies { + logger: LensLogger; + getInstallationState: (extId: string) => InstallationState; + state: ObservableSet; +} + +const setUninstalling = ({ logger, getInstallationState, state }: Dependencies)=> ( + (extId: string): void => { + logger.debug(`trying to set ${extId} as uninstalling`); + + const curState = getInstallationState(extId); + + if (curState !== InstallationState.IDLE) { + throw new Error(`cannot set ${extId} as uninstalling. Is currently ${curState}.`); + } + + state.add(extId); + } +); + +/** + * Strictly sets the UNINSTALLING state of an extension + * @param extId The ID of the extension + * @throws if state is not IDLE + */ +const setUninstallingInjectable = getInjectable({ + instantiate: (di) => setUninstalling({ + getInstallationState: di.inject(getInstallationStateInjectable), + logger: di.inject(installationStateLoggerInjectable), + state: di.inject(extensionsUninstallingInjectable), + }), + lifecycle: lifecycleEnum.singleton, +}); + +export default setUninstallingInjectable; diff --git a/src/renderer/extensions/installation-state/start-pre-install.injectable.ts b/src/renderer/extensions/installation-state/start-pre-install.injectable.ts new file mode 100644 index 0000000000..197a901e0c --- /dev/null +++ b/src/renderer/extensions/installation-state/start-pre-install.injectable.ts @@ -0,0 +1,48 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { ObservableSet } from "mobx"; +import type { LensLogger } from "../../../common/logger"; +import type { Disposer } from "../../utils"; +import uniqueIdInjectable from "../../../common/utils/unique-id.injectable"; +import installationStateLoggerInjectable from "../../../extensions/installation-state/logger.injectable"; +import extensionsPreinstallingInjectable from "./pre-installing.injectable"; + +interface Dependencies { + getUniqueId: () => string; + state: ObservableSet; + logger: LensLogger; +} + +const startPreInstall = ({ getUniqueId, state, logger }: Dependencies) => ( + (): Disposer => { + const preInstallStepId = getUniqueId(); + + logger.debug(`starting a new preinstall phase: ${preInstallStepId}`); + state.add(preInstallStepId); + + return () => { + logger.debug(`ending a preinstall phase: ${preInstallStepId}`); + state.delete(preInstallStepId); + }; + } +); + +/** + * Marks the start of a pre-install phase of an extension installation. The + * part of the installation before the tarball has been unpacked and the ID + * determined. + * @returns a disposer which should be called to mark the end of the install phase + */ +const startPreInstallInjectable = getInjectable({ + instantiate: (di) => startPreInstall({ + getUniqueId: di.inject(uniqueIdInjectable), + logger: di.inject(installationStateLoggerInjectable), + state: di.inject(extensionsPreinstallingInjectable), + }), + lifecycle: lifecycleEnum.singleton, +}); + +export default startPreInstallInjectable; diff --git a/src/renderer/extensions/installation-state/uninstalling.injectable.ts b/src/renderer/extensions/installation-state/uninstalling.injectable.ts new file mode 100644 index 0000000000..26af9328a9 --- /dev/null +++ b/src/renderer/extensions/installation-state/uninstalling.injectable.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { observable } from "mobx"; + +const extensionsUninstallingInjectable = getInjectable({ + instantiate: () => observable.set(), + lifecycle: lifecycleEnum.singleton, +}); + +export default extensionsUninstallingInjectable; diff --git a/src/renderer/frames/root-frame/init-root-frame/init-root-frame.injectable.ts b/src/renderer/frames/root-frame/init-root-frame/init-root-frame.injectable.ts index 6827bb3008..91dc6d12a2 100644 --- a/src/renderer/frames/root-frame/init-root-frame/init-root-frame.injectable.ts +++ b/src/renderer/frames/root-frame/init-root-frame/init-root-frame.injectable.ts @@ -5,7 +5,7 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { initRootFrame } from "./init-root-frame"; import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable"; -import ipcRendererInjectable from "../../../app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable"; +import ipcRendererInjectable from "../../../communication/ipc-renderer.injectable"; import bindProtocolAddRouteHandlersInjectable from "../../../protocol-handler/bind-protocol-add-route-handlers/bind-protocol-add-route-handlers.injectable"; import lensProtocolRouterRendererInjectable from "../../../protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable"; import catalogEntityRegistryInjectable from "../../../api/catalog-entity-registry/catalog-entity-registry.injectable"; diff --git a/src/renderer/getDiForUnitTesting.tsx b/src/renderer/getDiForUnitTesting.tsx index c353008371..9810dddad3 100644 --- a/src/renderer/getDiForUnitTesting.tsx +++ b/src/renderer/getDiForUnitTesting.tsx @@ -7,51 +7,47 @@ import glob from "glob"; import { memoize } from "lodash/fp"; import { createContainer } from "@ogre-tools/injectable"; import { setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; -import getValueFromRegisteredChannelInjectable from "./app-paths/get-value-from-registered-channel/get-value-from-registered-channel.injectable"; -import writeJsonFileInjectable from "../common/fs/write-json-file.injectable"; -import readJsonFileInjectable from "../common/fs/read-json-file.injectable"; -import readDirInjectable from "../common/fs/read-dir.injectable"; -import readFileInjectable from "../common/fs/read-file.injectable"; +import registerEventSinkInjectable from "../common/communication/register-event-sink.injectable"; +import registerChannelInjectable from "./communication/register-channel.injectable"; +import { overrideFsFunctions } from "../test-utils/override-fs-functions"; -export const getDiForUnitTesting = ({ doGeneralOverrides } = { doGeneralOverrides: false }) => { +interface DiForTestingOptions { + doGeneralOverrides?: boolean; + doIpcOverrides?: boolean; +} + +export async function getDiForUnitTesting({ doGeneralOverrides = false, doIpcOverrides = true }: DiForTestingOptions = {}) { const di = createContainer(); setLegacyGlobalDiForExtensionApi(di); for (const filePath of getInjectableFilePaths()) { - const injectableInstance = require(filePath).default; + const { default: injectableInstance } = await import(filePath); - di.register({ - id: filePath, - ...injectableInstance, - aliases: [injectableInstance, ...(injectableInstance.aliases || [])], - }); + try { + di.register({ + id: filePath, + ...injectableInstance, + aliases: [injectableInstance, ...(injectableInstance.aliases || [])], + }); + } catch (error) { + throw new Error(`Failed to register ${filePath}: ${error}`); + } } di.preventSideEffects(); if (doGeneralOverrides) { - di.override(getValueFromRegisteredChannelInjectable, () => () => undefined); + overrideFsFunctions(di); + } - di.override(readDirInjectable, () => () => { - throw new Error("Tried to read contents of a directory from file system without specifying explicit override."); - }); - - di.override(readFileInjectable, () => () => { - throw new Error("Tried to read a file from file system without specifying explicit override."); - }); - - di.override(writeJsonFileInjectable, () => () => { - throw new Error("Tried to write JSON file to file system without specifying explicit override."); - }); - - di.override(readJsonFileInjectable, () => () => { - throw new Error("Tried to read JSON file from file system without specifying explicit override."); - }); + if (doIpcOverrides) { + di.override(registerEventSinkInjectable, () => () => () => undefined); + di.override(registerChannelInjectable, () => () => () => undefined); } return di; -}; +} const getInjectableFilePaths = memoize(() => [ ...glob.sync("./**/*.injectable.{ts,tsx}", { cwd: __dirname }), diff --git a/src/renderer/search-store/search-store.test.ts b/src/renderer/search-store/search-store.test.ts index f3fc9a4289..445e70a673 100644 --- a/src/renderer/search-store/search-store.test.ts +++ b/src/renderer/search-store/search-store.test.ts @@ -29,7 +29,7 @@ describe("search store tests", () => { let searchStore: SearchStore; beforeEach(async () => { - const di = getDiForUnitTesting({ doGeneralOverrides: true }); + const di = await getDiForUnitTesting({ doGeneralOverrides: true }); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); diff --git a/src/test-utils/get-dis-for-unit-testing.ts b/src/test-utils/get-dis-for-unit-testing.ts index 976117bdf9..89a9905aa2 100644 --- a/src/test-utils/get-dis-for-unit-testing.ts +++ b/src/test-utils/get-dis-for-unit-testing.ts @@ -6,15 +6,28 @@ import { getDiForUnitTesting as getRendererDi } from "../renderer/getDiForUnitTe import { getDiForUnitTesting as getMainDi } from "../main/getDiForUnitTesting"; import { overrideIpcBridge } from "./override-ipc-bridge"; -export const getDisForUnitTesting = ({ doGeneralOverrides } = { doGeneralOverrides: false }) => { - const rendererDi = getRendererDi({ doGeneralOverrides }); - const mainDi = getMainDi({ doGeneralOverrides }); +interface DiForTestingOptions { + doGeneralOverrides?: boolean; +} + +export async function getDisForUnitTesting({ doGeneralOverrides = false }: DiForTestingOptions = {}) { + const rendererDi = await getRendererDi({ + doGeneralOverrides, + doIpcOverrides: false, + }); + const mainDi = await getMainDi({ + doGeneralOverrides, + doIpcOverrides: false, + }); overrideIpcBridge({ rendererDi, mainDi }); return { rendererDi, mainDi, - runSetups: () => Promise.all([rendererDi.runSetups(), mainDi.runSetups()]), + runSetups: async () => { + await mainDi.runSetups(); + await rendererDi.runSetups(); + }, }; -}; +} diff --git a/src/test-utils/override-fs-functions.ts b/src/test-utils/override-fs-functions.ts new file mode 100644 index 0000000000..8e47c014e8 --- /dev/null +++ b/src/test-utils/override-fs-functions.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { DependencyInjectionContainer } from "@ogre-tools/injectable"; +import readDirInjectable from "../common/fs/read-dir.injectable"; +import readFileInjectable from "../common/fs/read-file.injectable"; +import readJsonFileInjectable from "../common/fs/read-json-file.injectable"; +import removeDirInjectable from "../common/fs/remove-dir.injectable"; +import writeJsonFileInjectable from "../common/fs/write-json-file.injectable"; + +export function overrideFsFunctions(di: DependencyInjectionContainer): void { + di.override(removeDirInjectable, () => () => { + throw new Error("Tried to remove a directory from file system without specifying explicit override."); + }); + + di.override(readDirInjectable, () => () => { + throw new Error("Tried to read contents of a directory from file system without specifying explicit override."); + }); + + di.override(readFileInjectable, () => () => { + throw new Error("Tried to read a file from file system without specifying explicit override."); + }); + + di.override(writeJsonFileInjectable, () => () => { + throw new Error("Tried to write JSON file to file system without specifying explicit override."); + }); + + di.override(readJsonFileInjectable, () => () => { + throw new Error("Tried to read JSON file from file system without specifying explicit override."); + }); +} diff --git a/src/test-utils/override-ipc-bridge.ts b/src/test-utils/override-ipc-bridge.ts index b497d3091b..5c382132d1 100644 --- a/src/test-utils/override-ipc-bridge.ts +++ b/src/test-utils/override-ipc-bridge.ts @@ -3,64 +3,39 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import type { DependencyInjectionContainer } from "@ogre-tools/injectable"; -import type { Channel } from "../common/ipc-channel/channel"; -import getValueFromRegisteredChannelInjectable from "../renderer/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.injectable"; -import registerChannelInjectable from "../main/app-paths/register-channel/register-channel.injectable"; -import asyncFn from "@async-fn/jest"; +import EventEmitter from "events"; +import ipcHandleInjectable from "../main/communication/ipc-handle.injectable"; +import ipcMainOnInjectable from "../main/communication/ipc-on.injectable"; +import ipcRendererOnInjectable from "../renderer/communication/ipc-on.injectable"; +import ipcInvokeInjectable from "../renderer/communication/ipc-invoke.injectable"; +import broadcastInjectable from "../common/communication/broadcast.injectable"; -export const overrideIpcBridge = ({ - rendererDi, - mainDi, -}: { +interface OverrideIpcBridgeContainers { rendererDi: DependencyInjectionContainer; mainDi: DependencyInjectionContainer; -}) => { - const fakeChannelMap = new Map< - Channel, - { promise: Promise; resolve: (arg0: any) => Promise } - >(); +} - const mainIpcRegistrations = { - set: , TInstance>( - key: TChannel, - callback: () => TChannel["_template"], - ) => { - if (!fakeChannelMap.has(key)) { - const mockInstance = asyncFn(); +export function overrideIpcBridge({ rendererDi, mainDi }: OverrideIpcBridgeContainers) { + const fakeChannelMap = new Map Promise>(); + const fakeEmitter = new EventEmitter(); - fakeChannelMap.set(key, { - promise: mockInstance(), - resolve: mockInstance.resolve, - }); - } - - return fakeChannelMap.get(key).resolve(callback); - }, - - get: , TInstance>(key: TChannel) => { - if (!fakeChannelMap.has(key)) { - const mockInstance = asyncFn(); - - fakeChannelMap.set(key, { - promise: mockInstance(), - resolve: mockInstance.resolve, - }); - } - - return fakeChannelMap.get(key).promise; - }, - }; - - rendererDi.override( - getValueFromRegisteredChannelInjectable, - () => async (channel) => { - const callback = await mainIpcRegistrations.get(channel); - - return callback(); - }, - ); - - mainDi.override(registerChannelInjectable, () => (channel, callback) => { - mainIpcRegistrations.set(channel, callback); + rendererDi.override(ipcInvokeInjectable, () => (name, ...args) => { + if (fakeChannelMap.has(name)) { + return fakeChannelMap.get(name)(...args); + } else { + throw new Error(`Channel ${name} has not been handled`); + } }); -}; + rendererDi.override(broadcastInjectable, () => (channel, ...args) => fakeEmitter.emit(channel, ...args)); + rendererDi.override(ipcRendererOnInjectable, () => (name, listener) => fakeEmitter.on(name, listener)); + + mainDi.override(broadcastInjectable, () => (channel, ...args) => fakeEmitter.emit(channel, ...args)); + mainDi.override(ipcMainOnInjectable, () => (name, listener) => fakeEmitter.on(name, listener)); + mainDi.override(ipcHandleInjectable, () => (name, callback) => { + if (fakeChannelMap.has(name)) { + throw new Error(`Channel ${name} has already been handled`); + } else { + fakeChannelMap.set(name, async (...args: Parameters) => await callback(...args)); + } + }); +}