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

Add automatic cleanup on Singleton removal

- simplify WindowsManager

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-05-19 11:27:28 -04:00
parent 8b352f6c6b
commit d08cac81c4
7 changed files with 88 additions and 105 deletions

View File

@ -19,12 +19,16 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { disposer } from "./disposer";
type StaticThis<T, R extends any[]> = { new(...args: R): T }; type StaticThis<T, R extends any[]> = { new(...args: R): T };
export class Singleton { export class Singleton {
private static instances = new WeakMap<object, Singleton>(); private static instances = new WeakMap<object, Singleton>();
private static creating = ""; private static creating = "";
protected disposers = disposer();
constructor() { constructor() {
if (Singleton.creating.length === 0) { if (Singleton.creating.length === 0) {
throw new TypeError("A singleton class must be created by createInstance()"); throw new TypeError("A singleton class must be created by createInstance()");
@ -43,7 +47,7 @@ export class Singleton {
* @param args The constructor arguments for the child class * @param args The constructor arguments for the child class
* @returns An instance of the child class * @returns An instance of the child class
*/ */
static createInstance<T, R extends any[]>(this: StaticThis<T, R>, ...args: R): T { static createInstance<T extends Singleton, R extends any[]>(this: StaticThis<T, R>, ...args: R): T {
if (!Singleton.instances.has(this)) { if (!Singleton.instances.has(this)) {
if (Singleton.creating.length > 0) { if (Singleton.creating.length > 0) {
throw new TypeError("Cannot create a second singleton while creating a first"); throw new TypeError("Cannot create a second singleton while creating a first");
@ -64,7 +68,7 @@ export class Singleton {
* Default: `true` * Default: `true`
* @returns An instance of the child class * @returns An instance of the child class
*/ */
static getInstance<T, R extends any[]>(this: StaticThis<T, R>, strict = true): T | undefined { static getInstance<T extends Singleton, R extends any[]>(this: StaticThis<T, R>, strict = true): T | undefined {
if (!Singleton.instances.has(this) && strict) { if (!Singleton.instances.has(this) && strict) {
throw new TypeError(`instance of ${this.name} is not created`); throw new TypeError(`instance of ${this.name} is not created`);
} }
@ -80,6 +84,7 @@ export class Singleton {
* There is *no* way in JS or TS to prevent globals like that. * There is *no* way in JS or TS to prevent globals like that.
*/ */
static resetInstance() { static resetInstance() {
Singleton.instances.get(this)?.disposers();
Singleton.instances.delete(this); Singleton.instances.delete(this);
} }
} }

View File

@ -42,8 +42,6 @@ const logPrefix = "[KUBECONFIG-SYNC]:";
export class KubeconfigSyncManager extends Singleton { export class KubeconfigSyncManager extends Singleton {
protected sources = observable.map<string, [IComputedValue<CatalogEntity[]>, Disposer]>(); protected sources = observable.map<string, [IComputedValue<CatalogEntity[]>, Disposer]>();
protected syncing = false; protected syncing = false;
protected syncListDisposer?: Disposer;
protected static readonly syncName = "lens:kube-sync"; protected static readonly syncName = "lens:kube-sync";
constructor() { constructor() {
@ -76,28 +74,26 @@ export class KubeconfigSyncManager extends Singleton {
this.startNewSync(filePath); this.startNewSync(filePath);
} }
this.syncListDisposer = observe(UserStore.getInstance().syncKubeconfigEntries, change => { this.disposers.push(
switch (change.type) { observe(UserStore.getInstance().syncKubeconfigEntries, change => {
case "add": switch (change.type) {
this.startNewSync(change.name); case "add":
break; this.startNewSync(change.name);
case "delete": break;
this.stopOldSync(change.name); case "delete":
break; this.stopOldSync(change.name);
break;
}
}),
() => {
for (const filePath of this.sources.keys()) {
this.stopOldSync(filePath);
}
catalogEntityRegistry.removeSource(KubeconfigSyncManager.syncName);
this.syncing = false;
} }
}); );
}
@action
stopSync() {
this.syncListDisposer?.();
for (const filePath of this.sources.keys()) {
this.stopOldSync(filePath);
}
catalogEntityRegistry.removeSource(KubeconfigSyncManager.syncName);
this.syncing = false;
} }
@action @action

View File

@ -27,9 +27,9 @@ import { ClusterStore, getClusterIdFromHost } from "../common/cluster-store";
import type { Cluster } from "./cluster"; import type { Cluster } from "./cluster";
import logger from "./logger"; import logger from "./logger";
import { apiKubePrefix } from "../common/vars"; import { apiKubePrefix } from "../common/vars";
import { Singleton } from "../common/utils"; import { Singleton, toJS } from "../common/utils";
import { catalogEntityRegistry } from "./catalog"; import { catalogEntityRegistry } from "./catalog";
import { KubernetesCluster, KubernetesClusterPrometheusMetrics } from "../common/catalog-entities/kubernetes-cluster"; import { KubernetesCluster } from "../common/catalog-entities/kubernetes-cluster";
export class ClusterManager extends Singleton { export class ClusterManager extends Singleton {
private store = ClusterStore.getInstance(); private store = ClusterStore.getInstance();
@ -37,35 +37,37 @@ export class ClusterManager extends Singleton {
constructor() { constructor() {
super(); super();
makeObservable(this); makeObservable(this);
this.bindEvents();
}
private bindEvents() { this.disposers.push(
// reacting to every cluster's state change and total amount of items reaction(
reaction( () => toJS(this.store.clustersList),
() => this.store.clustersList.map(c => c.getState()), clusters => this.updateCatalog(clusters),
() => this.updateCatalog(this.store.clustersList), { fireImmediately: true },
{ fireImmediately: true, } ),
); reaction(
() => catalogEntityRegistry.getItemsForApiKind<KubernetesCluster>("entity.k8slens.dev/v1alpha1", "KubernetesCluster"),
entities => this.syncClustersFromCatalog(entities)
),
// auto-stop removed clusters
autorun(() => {
const removedClusters = Array.from(this.store.removedClusters.values());
reaction(() => catalogEntityRegistry.getItemsForApiKind<KubernetesCluster>("entity.k8slens.dev/v1alpha1", "KubernetesCluster"), (entities) => { if (removedClusters.length > 0) {
this.syncClustersFromCatalog(entities); const meta = removedClusters.map(cluster => cluster.getMeta());
});
// auto-stop removed clusters logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta);
autorun(() => { removedClusters.forEach(cluster => cluster.disconnect());
const removedClusters = Array.from(this.store.removedClusters.values()); this.store.removedClusters.clear();
}
if (removedClusters.length > 0) { }, {
const meta = removedClusters.map(cluster => cluster.getMeta()); delay: 250
}),
logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta); () => {
removedClusters.forEach(cluster => cluster.disconnect()); for (const cluster of this.store.clusters.values()) {
this.store.removedClusters.clear(); cluster.disconnect();
}
} }
}, { );
delay: 250
});
ipcMain.on("network:offline", this.onNetworkOffline); ipcMain.on("network:offline", this.onNetworkOffline);
ipcMain.on("network:online", this.onNetworkOnline); ipcMain.on("network:online", this.onNetworkOnline);
@ -77,7 +79,7 @@ export class ClusterManager extends Singleton {
const index = catalogEntityRegistry.items.findIndex((entity) => entity.metadata.uid === cluster.id); const index = catalogEntityRegistry.items.findIndex((entity) => entity.metadata.uid === cluster.id);
if (index !== -1) { if (index !== -1) {
const entity = catalogEntityRegistry.items[index] as KubernetesCluster; const entity = catalogEntityRegistry.items[index];
entity.status.phase = cluster.disconnected ? "disconnected" : "connected"; entity.status.phase = cluster.disconnected ? "disconnected" : "connected";
entity.status.active = !cluster.disconnected; entity.status.active = !cluster.disconnected;
@ -86,14 +88,12 @@ export class ClusterManager extends Singleton {
entity.metadata.name = cluster.preferences.clusterName; entity.metadata.name = cluster.preferences.clusterName;
} }
entity.spec.metrics ||= { source: "local" }; entity.spec.metrics ??= { source: "local" };
if (entity.spec.metrics.source === "local") { if (entity.spec.metrics.source === "local") {
const prometheus: KubernetesClusterPrometheusMetrics = entity.spec?.metrics?.prometheus || {}; entity.spec.metrics.prometheus ??= {};
entity.spec.metrics.prometheus.type ??= cluster.preferences.prometheusProvider?.type;
prometheus.type = cluster.preferences.prometheusProvider?.type; entity.spec.metrics.prometheus.address = cluster.preferences.prometheus;
prometheus.address = cluster.preferences.prometheus;
entity.spec.metrics.prometheus = prometheus;
} }
catalogEntityRegistry.items.splice(index, 1, entity); catalogEntityRegistry.items.splice(index, 1, entity);
@ -146,12 +146,6 @@ export class ClusterManager extends Singleton {
}); });
}; };
stop() {
this.store.clusters.forEach((cluster: Cluster) => {
cluster.disconnect();
});
}
getClusterForRequest(req: http.IncomingMessage): Cluster { getClusterForRequest(req: http.IncomingMessage): Cluster {
let cluster: Cluster = null; let cluster: Cluster = null;

View File

@ -26,19 +26,11 @@ import { ClusterManager } from "./cluster-manager";
import logger from "./logger"; import logger from "./logger";
export function exitApp() { export function exitApp() {
console.log("before windowManager");
const windowManager = WindowManager.getInstance(false);
console.log("before clusterManager");
const clusterManager = ClusterManager.getInstance(false);
console.log("after clusterManager");
appEventBus.emit({ name: "service", action: "close" }); appEventBus.emit({ name: "service", action: "close" });
windowManager?.hide();
clusterManager?.stop(); WindowManager.resetInstance();
ClusterManager.resetInstance();
logger.info("SERVICE:QUIT"); logger.info("SERVICE:QUIT");
setTimeout(() => {
app.exit(); setTimeout(() => app.exit(), 1000);
}, 1000);
} }

View File

@ -40,7 +40,7 @@ describe("Helm Service tests", () => {
{ name: "experiment", url: "experimenturl" }, { name: "experiment", url: "experimenturl" },
]; ];
}), }),
}); } as any);
const charts = await helmService.listCharts(); const charts = await helmService.listCharts();
@ -91,7 +91,7 @@ describe("Helm Service tests", () => {
{ name: "bitnami", url: "bitnamiurl" }, { name: "bitnami", url: "bitnamiurl" },
]; ];
}), }),
}); } as any);
const charts = await helmService.listCharts(); const charts = await helmService.listCharts();

View File

@ -148,7 +148,6 @@ app.on("ready", async () => {
const lensProxy = LensProxy.createInstance(handleWsUpgrade); const lensProxy = LensProxy.createInstance(handleWsUpgrade);
ClusterManager.createInstance(); ClusterManager.createInstance();
KubeconfigSyncManager.createInstance();
try { try {
logger.info("🔌 Starting LensProxy"); logger.info("🔌 Starting LensProxy");
@ -194,7 +193,7 @@ app.on("ready", async () => {
ipcMain.on(IpcRendererNavigationEvents.LOADED, () => { ipcMain.on(IpcRendererNavigationEvents.LOADED, () => {
cleanup.push(pushCatalogToRenderer(catalogEntityRegistry)); cleanup.push(pushCatalogToRenderer(catalogEntityRegistry));
KubeconfigSyncManager.getInstance().startSync(); KubeconfigSyncManager.createInstance().startSync();
startUpdateChecking(); startUpdateChecking();
LensProtocolRouterMain.getInstance().rendererLoaded = true; LensProtocolRouterMain.getInstance().rendererLoaded = true;
}); });
@ -252,9 +251,9 @@ app.on("will-quit", (event) => {
// Quit app on Cmd+Q (MacOS) // Quit app on Cmd+Q (MacOS)
logger.info("APP:QUIT"); logger.info("APP:QUIT");
appEventBus.emit({name: "app", action: "close"}); appEventBus.emit({name: "app", action: "close"});
ClusterManager.getInstance(false)?.stop(); // close cluster connections WindowManager.resetInstance();
KubeconfigSyncManager.getInstance(false)?.stopSync(); ClusterManager.resetInstance();
cleanup(); KubeconfigSyncManager.resetInstance();
if (blockQuit) { if (blockQuit) {
event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.) event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.)

View File

@ -34,11 +34,14 @@ import logger from "./logger";
import { productName } from "../common/vars"; import { productName } from "../common/vars";
import { LensProxy } from "./proxy/lens-proxy"; import { LensProxy } from "./proxy/lens-proxy";
function isHideable(window: BrowserWindow | null): boolean {
return Boolean(window && !window.isDestroyed());
}
export class WindowManager extends Singleton { export class WindowManager extends Singleton {
protected mainWindow: BrowserWindow; protected mainWindow: BrowserWindow | null = null;
protected splashWindow: BrowserWindow; protected splashWindow: BrowserWindow | null = null;
protected windowState: windowStateKeeper.State; protected windowState: windowStateKeeper.State;
protected disposers: Record<string, Function> = {};
@observable activeClusterId: ClusterId; @observable activeClusterId: ClusterId;
@ -46,8 +49,9 @@ export class WindowManager extends Singleton {
super(); super();
makeObservable(this); makeObservable(this);
this.bindEvents(); this.bindEvents();
this.initMenu(); this.disposers.push(initMenu(this));
this.initTray(); this.disposers.push(initTray(this));
this.disposers.push(() => this.destroy());
} }
get mainUrl() { get mainUrl() {
@ -131,14 +135,6 @@ export class WindowManager extends Singleton {
} }
} }
protected async initMenu() {
this.disposers.menuAutoUpdater = initMenu(this);
}
protected initTray() {
this.disposers.trayAutoUpdater = initTray(this);
}
protected bindEvents() { protected bindEvents() {
// track visible cluster from ui // track visible cluster from ui
subscribeToBroadcast(IpcRendererNavigationEvents.CLUSTER_VIEW_CURRENT_ID, (event, clusterId: ClusterId) => { subscribeToBroadcast(IpcRendererNavigationEvents.CLUSTER_VIEW_CURRENT_ID, (event, clusterId: ClusterId) => {
@ -206,18 +202,19 @@ export class WindowManager extends Singleton {
} }
hide() { hide() {
if (this.mainWindow && !this.mainWindow.isDestroyed()) this.mainWindow.hide(); if (isHideable(this.mainWindow)) {
if (this.splashWindow && !this.splashWindow.isDestroyed()) this.splashWindow.hide(); this.mainWindow.hide();
}
if (isHideable(this.splashWindow)) {
this.splashWindow.hide();
}
} }
destroy() { private destroy() {
this.mainWindow.destroy(); this.mainWindow.destroy();
this.splashWindow.destroy(); this.splashWindow.destroy();
this.mainWindow = null; this.mainWindow = null;
this.splashWindow = null; this.splashWindow = null;
Object.entries(this.disposers).forEach(([name, dispose]) => {
dispose();
delete this.disposers[name];
});
} }
} }