Stop using @electron/remote to obtain app.getPath() (#4078)

* Stop using remote to obtain app.getPath()

- Initialize entire map in bootstrap()

- Updated unit tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Resolve PR comments

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix test

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix build

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Ensure that init can only be called once and catch errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Replace basically all uses of app.getPath() with AppPaths.get()

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix unit tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton authored and GitHub committed 2021-10-27 21:07:41 -04:00
1 parent 65b6908bf1
commit f297407156
45 files changed
+521 -200

No files matched your search

+21 -17
View File
@@ -30,6 +30,7 @@ import { Console } from "console";
import { stdout, stderr } from "process";
import type { ClusterId } from "../cluster-types";
import { getCustomKubeConfigPath } from "../utils";
import { AppPaths } from "../app-paths";
console = new Console(stdout, stderr);
@@ -67,23 +68,26 @@ function embed(clusterId: ClusterId, contents: any): string {
return absPath;
}
jest.mock("electron", () => {
return {
app: {
getVersion: () => "99.99.99",
getPath: () => "tmp",
getLocale: () => "en",
setLoginItemSettings: jest.fn(),
},
ipcMain: {
handle: jest.fn(),
on: jest.fn(),
removeAllListeners: jest.fn(),
off: jest.fn(),
send: jest.fn(),
}
};
});
jest.mock("electron", () => ({
app: {
getVersion: () => "99.99.99",
getName: () => "lens",
setName: jest.fn(),
setPath: jest.fn(),
getPath: () => "tmp",
getLocale: () => "en",
setLoginItemSettings: jest.fn(),
},
ipcMain: {
handle: jest.fn(),
on: jest.fn(),
removeAllListeners: jest.fn(),
off: jest.fn(),
send: jest.fn(),
}
}));
AppPaths.init();
describe("empty config", () => {
beforeEach(async () => {
+18 -10
View File
@@ -22,6 +22,7 @@
import { anyObject } from "jest-mock-extended";
import mockFs from "mock-fs";
import logger from "../../main/logger";
import { AppPaths } from "../app-paths";
import { ClusterStore } from "../cluster-store";
import { HotbarStore } from "../hotbar-store";
@@ -116,16 +117,23 @@ const awsCluster = {
}
};
jest.mock("electron", () => {
return {
app: {
getVersion: () => "99.99.99",
getPath: () => "tmp",
getLocale: () => "en",
setLoginItemSettings: (): void => void 0,
}
};
});
jest.mock("electron", () => ({
app: {
getVersion: () => "99.99.99",
getName: () => "lens",
setName: jest.fn(),
setPath: jest.fn(),
getPath: () => "tmp",
getLocale: () => "en",
setLoginItemSettings: jest.fn(),
},
ipcMain: {
on: jest.fn(),
handle: jest.fn(),
},
}));
AppPaths.init();
describe("HotbarStore", () => {
beforeEach(() => {
+17 -10
View File
@@ -21,16 +21,21 @@
import mockFs from "mock-fs";
jest.mock("electron", () => {
return {
app: {
getVersion: () => "99.99.99",
getPath: () => "tmp",
getLocale: () => "en",
setLoginItemSettings: (): void => void 0,
}
};
});
jest.mock("electron", () => ({
app: {
getVersion: () => "99.99.99",
getName: () => "lens",
setName: jest.fn(),
setPath: jest.fn(),
getPath: () => "tmp",
getLocale: () => "en",
setLoginItemSettings: jest.fn(),
},
ipcMain: {
on: jest.fn(),
handle: jest.fn(),
},
}));
import { UserStore } from "../user-store";
import { Console } from "console";
@@ -39,8 +44,10 @@ import electron from "electron";
import { stdout, stderr } from "process";
import { ThemeStore } from "../../renderer/theme.store";
import type { ClusterStoreModel } from "../cluster-store";
import { AppPaths } from "../app-paths";
console = new Console(stdout, stderr);
AppPaths.init();
describe("user store tests", () => {
describe("for an empty config", () => {
+117
View File
@@ -0,0 +1,117 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { app, ipcMain, ipcRenderer } from "electron";
import { observable, when } from "mobx";
import path from "path";
import logger from "./logger";
import { fromEntries, toJS } from "./utils";
import { isWindows } from "./vars";
export type PathName = Parameters<typeof app["getPath"]>[0];
const pathNames: PathName[] = [
"home",
"appData",
"userData",
"cache",
"temp",
"exe",
"module",
"desktop",
"documents",
"downloads",
"music",
"pictures",
"videos",
"logs",
"crashDumps",
];
if (isWindows) {
pathNames.push("recent");
}
export class AppPaths {
private static paths = observable.box<Record<PathName, string> | undefined>();
private static readonly ipcChannel = "get-app-paths";
/**
* Initializes the local copy of the paths from electron.
*/
static async init(): Promise<void> {
logger.info(`[APP-PATHS]: initializing`);
if (AppPaths.paths.get()) {
return void logger.error("[APP-PATHS]: init called more than once");
}
if (ipcMain) {
AppPaths.initMain();
} else {
await AppPaths.initRenderer();
}
}
private static initMain(): void {
if (process.env.CICD) {
app.setPath("appData", process.env.CICD);
}
app.setPath("userData", path.join(app.getPath("appData"), app.getName()));
AppPaths.paths.set(fromEntries(pathNames.map(pathName => [pathName, app.getPath(pathName)])));
ipcMain.handle(AppPaths.ipcChannel, () => toJS(AppPaths.paths.get()));
}
private static async initRenderer(): Promise<void> {
const paths = await ipcRenderer.invoke(AppPaths.ipcChannel);
if (!paths || typeof paths !== "object") {
throw Object.assign(new Error("[APP-PATHS]: ipc handler returned unexpected data"), { data: paths });
}
AppPaths.paths.set(paths);
}
/**
* An alternative to `app.getPath()` for use in renderer and common.
* This function throws if called before initialization.
* @param name The name of the path field
*/
static get(name: PathName): string {
if (!AppPaths.paths.get()) {
throw new Error("AppPaths.init() has not been called");
}
return AppPaths.paths.get()[name];
}
/**
* An async version of `AppPaths.get()` which waits for `AppPaths.init()` to
* be called before returning
*/
static async getAsync(name: PathName): Promise<string> {
await when(() => Boolean(AppPaths.paths.get()));
return AppPaths.paths.get()[name];
}
}
+2 -2
View File
@@ -30,7 +30,7 @@ import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc";
import isEqual from "lodash/isEqual";
import { isTestEnv } from "./vars";
import { kebabCase } from "lodash";
import { getPath } from "./utils/getPath";
import { AppPaths } from "./app-paths";
export interface BaseStoreParams<T> extends ConfOptions<T> {
syncOptions?: IReactionOptions;
@@ -93,7 +93,7 @@ export abstract class BaseStore<T> extends Singleton {
}
protected cwd() {
return getPath("userData");
return AppPaths.get("userData");
}
protected async saveToFile(model: T) {
+6 -3
View File
@@ -19,13 +19,12 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { ipcMain } from "electron";
import { app, ipcMain } from "electron";
import winston, { format } from "winston";
import type Transport from "winston-transport";
import { consoleFormat } from "winston-console-format";
import { isDebugging, isTestEnv } from "./vars";
import BrowserConsole from "winston-transport-browserconsole";
import { getPath } from "./utils/getPath";
const logLevel = process.env.LOG_LEVEL
? process.env.LOG_LEVEL
@@ -66,7 +65,11 @@ if (ipcMain) {
handleExceptions: false,
level: logLevel,
filename: "lens.log",
dirname: getPath("logs"),
/**
* SAFTEY: the `ipcMain` check above should mean that this is only
* called in the main process
*/
dirname: app.getPath("logs"),
maxsize: 16 * 1024,
maxFiles: 16,
tailable: true,
+2 -2
View File
@@ -33,7 +33,7 @@ import { ObservableToggleSet, toJS } from "../../renderer/utils";
import { DESCRIPTORS, KubeconfigSyncValue, UserPreferencesModel, EditorConfiguration } from "./preferences-helpers";
import logger from "../../main/logger";
import type { monaco } from "react-monaco-editor";
import { getPath } from "../utils/getPath";
import { AppPaths } from "../app-paths";
export interface UserStoreModel {
lastSeenAppVersion: string;
@@ -257,5 +257,5 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
* @returns string
*/
export function getDefaultKubectlDownloadPath(): string {
return path.join(getPath("userData"), "binaries");
return path.join(AppPaths.get("userData"), "binaries");
}
@@ -46,6 +46,11 @@ export function getClusterFrameUrl(clusterId: ClusterId) {
* Get the result of `getClusterIdFromHost` from the current `location.host`
*/
export function getHostedClusterId(): ClusterId | undefined {
// catch being called in main
if (typeof location === "undefined") {
return undefined;
}
return getClusterIdFromHost(location.host);
}
+4 -4
View File
@@ -32,19 +32,21 @@ export * from "./base64";
export * from "./camelCase";
export * from "./cloneJson";
export * from "./cluster-id-url-parsing";
export * from "./convertCpu";
export * from "./convertMemory";
export * from "./debouncePromise";
export * from "./defineGlobal";
export * from "./delay";
export * from "./disposer";
export * from "./downloadFile";
export * from "./formatDuration";
export * from "./escapeRegExp";
export * from "./extended-map";
export * from "./getPath";
export * from "./formatDuration";
export * from "./getRandId";
export * from "./hash-set";
export * from "./local-kubeconfig";
export * from "./n-fircate";
export * from "./objects";
export * from "./openExternal";
export * from "./paths";
export * from "./reject-promise";
@@ -56,8 +58,6 @@ export * from "./toggle-set";
export * from "./toJS";
export * from "./type-narrowing";
export * from "./types";
export * from "./convertMemory";
export * from "./convertCpu";
import * as iter from "./iter";
import * as array from "./array";
+2 -2
View File
@@ -21,11 +21,11 @@
import path from "path";
import * as uuid from "uuid";
import { AppPaths } from "../app-paths";
import type { ClusterId } from "../cluster-types";
import { getPath } from "./getPath";
export function storedKubeConfigFolder(): string {
return path.resolve(getPath("userData"), "kubeconfigs");
return path.resolve(AppPaths.get("userData"), "kubeconfigs");
}
export function getCustomKubeConfigPath(clusterId: ClusterId = uuid.v4()): string {
@@ -19,19 +19,10 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { app, ipcMain } from "electron";
const remote = ipcMain ? null : require("@electron/remote");
/**
* calls getPath either on app or on the remote's app
*
* @deprecated Use a different method for accessing the getPath function
* A better typed version of `Object.fromEntries` where the keys are known to
* be a specific subset
*/
export function getPath(name: Parameters<typeof app["getPath"]>[0]): string {
if (app) {
return app.getPath(name);
}
return remote.app.getPath(name);
export function fromEntries<T, Key extends string>(entries: Iterable<readonly [Key, T]>): { [k in Key]: T } {
return Object.fromEntries(entries) as { [k in Key]: T };
}