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:
parent
8b352f6c6b
commit
d08cac81c4
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
@ -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.)
|
||||||
|
|||||||
@ -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];
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user