From d2385a33bc6c08ff6cc03de6c327bd266c6299a4 Mon Sep 17 00:00:00 2001 From: Rongjian Zhang Date: Thu, 15 Feb 2024 18:14:21 +0800 Subject: [PATCH] refactor: with results --- package.json | 1 + src/main/utils.ts | 28 ---------- src/platforms/linux.ts | 119 +++++++++++++++++++++------------------- src/platforms/macos.ts | 52 ++++++++++-------- src/platforms/utils.ts | 5 +- src/platforms/win.ts | 119 +++++++++++++++++++++------------------- src/reducers/app.ts | 10 ++-- src/reducers/session.ts | 7 ++- yarn.lock | 8 +++ 9 files changed, 174 insertions(+), 175 deletions(-) diff --git a/package.json b/package.json index f572e7d..5c89334 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "react-dom": "^18.2.0", "react-redux": "^9.1.0", "react-use": "^17.5.0", + "ts-results": "^3.3.0", "typescript": "^5.3.3", "universal-analytics": "^0.5.3" }, diff --git a/src/main/utils.ts b/src/main/utils.ts index 0f7bb9c..35056fd 100644 --- a/src/main/utils.ts +++ b/src/main/utils.ts @@ -1,36 +1,8 @@ -import fs from "fs"; import { machineId } from "node-machine-id"; import os from "os"; // import { setUpdateNotification } from 'electron-update-notification' // TODO: import ua from "universal-analytics"; -export async function readdirSafe(p: string) { - try { - return await fs.promises.readdir(p); - } catch (err) { - console.error(err); - return []; - } -} - -export async function readFileSafe(p: string) { - try { - return await fs.promises.readFile(p, { encoding: "utf8" }); - } catch (err) { - console.error(err); - return ""; - } -} - -export async function readFileAsBufferSafe(p: string) { - try { - return await fs.promises.readFile(p); - } catch (err) { - console.error(err); - return; - } -} - export async function setReporter() { try { const id = await machineId(); diff --git a/src/platforms/linux.ts b/src/platforms/linux.ts index 63b3301..4454287 100644 --- a/src/platforms/linux.ts +++ b/src/platforms/linux.ts @@ -1,71 +1,80 @@ -import { readdirSafe, readFileSafe } from "../main/utils"; -import type { AppInfo } from "../reducers/app"; import type { AppReader } from "./utils"; import fs from "fs"; import ini from "ini"; import path from "path"; +import { Result } from "ts-results"; const desktopFilesDir = "/usr/share/applications"; -async function readAppInfo(desktopFile: string): Promise { - const content = await readFileSafe(desktopFile); +const readAppInfo = (desktopFile: string) => + Result.wrapAsync(async () => { + const content = await fs.promises.readFile(desktopFile, { + encoding: "utf-8", + }); + const entry = ini.parse(content)["Desktop Entry"] as + | { + Name?: string; + Icon?: string; + Exec?: string; + } + | undefined; - const entry = ini.parse(content)["Desktop Entry"] as { - Name?: string; - Icon?: string; - Exec?: string; - }; - if (!entry || !entry.Exec) return; + if (!entry?.Exec) throw new Error("Exec not found"); - let exePath = ""; - if (entry.Exec.startsWith('"')) { - exePath = entry.Exec.replace(/^"(.*)".*/, "$1"); - } else { - // Remove arg - exePath = entry.Exec.split(/\s+/)[0] ?? ""; - } - - if (!exePath.startsWith("/")) return; - - if (!fs.existsSync(path.join(exePath, "../resources/electron.asar"))) return; + let exePath = ""; + if (entry.Exec.startsWith('"')) { + exePath = entry.Exec.replace(/^"(.*)".*/, "$1"); + } else { + // Remove arg + exePath = entry.Exec.split(/\s+/)[0] ?? ""; + } - let icon = ""; - if (entry.Icon) { - try { - const iconBuffer = await fs.promises.readFile( - `/usr/share/icons/hicolor/1024x1024/apps/${entry.Icon}.png`, - ); - icon = "data:image/png;base64," + iconBuffer.toString("base64"); - } catch (err) { - console.error(err); + if (!exePath.startsWith("/")) { + throw new Error("Exec path invalid"); + } + if (!fs.existsSync(path.join(exePath, "../resources/electron.asar"))) { + throw new Error("resources/electron.asar not exists"); } - } - return { - id: exePath, - icon: icon, // TODO: Read icon - name: entry.Name || path.basename(exePath), - exePath: exePath, - }; -} + let icon = ""; + if (entry.Icon) { + try { + const iconBuffer = await fs.promises.readFile( + `/usr/share/icons/hicolor/1024x1024/apps/${entry.Icon}.png`, + ); + icon = "data:image/png;base64," + iconBuffer.toString("base64"); + } catch (err) { + console.error(err); + } + } -export const adapter: AppReader = { - async readAll() { - const files = await readdirSafe(desktopFilesDir); - const apps = await Promise.all( - files.map((file) => { - if (!file.endsWith(".desktop")) return; - return readAppInfo(path.join(desktopFilesDir, file)); - }), - ); - return apps; - }, - async readByPath(p: string) { return { - id: p, - name: path.basename(p), - icon: "", - exePath: p, + id: exePath, + icon: icon, // TODO: Read icon + name: entry.Name || path.basename(exePath), + exePath: exePath, }; - }, + }); + +export const adapter: AppReader = { + readAll: () => + Result.wrapAsync(async () => { + const files = await fs.promises.readdir(desktopFilesDir); + const apps = await Promise.all( + files + .filter((f) => f.endsWith(".desktop")) + .map((file) => readAppInfo(path.join(desktopFilesDir, file))), + ); + return apps.flatMap((app) => (app.ok ? [app.unwrap()] : [])); + }), + readByPath: (p: string) => + Result.wrapAsync(async () => { + // TODO: + return { + id: p, + name: path.basename(p), + icon: "", + exePath: p, + }; + }), }; diff --git a/src/platforms/macos.ts b/src/platforms/macos.ts index 346fc21..6f8aa37 100644 --- a/src/platforms/macos.ts +++ b/src/platforms/macos.ts @@ -1,8 +1,8 @@ -import { readdirSafe, readFileAsBufferSafe } from "../main/utils"; import type { AppReader } from "./utils"; import fs from "fs"; import path from "path"; import plist from "simple-plist"; +import { Result } from "ts-results"; interface MacosAppInfo { CFBundleIdentifier: string; @@ -26,7 +26,7 @@ async function readPlistFile(path: string) { } async function readIcnsAsImageUri(file: string) { - let buf = await readFileAsBufferSafe(file); + let buf = await fs.promises.readFile(file); if (!buf) return ""; const totalSize = buf.readInt32BE(4) - 8; @@ -55,28 +55,32 @@ async function readIcnsAsImageUri(file: string) { } export const adapter: AppReader = { - async readAll() { - const dir = "/Applications"; - const appPaths = await readdirSafe(dir); - return Promise.all(appPaths.map((p) => this.readByPath(path.join(dir, p)))); - }, - async readByPath(p: string) { - const isElectronBased = fs.existsSync( - path.join(p, "Contents/Frameworks/Electron Framework.framework"), - ); - if (!isElectronBased) return; + readAll: () => + Result.wrapAsync(async () => { + const dir = "/Applications"; + const appPaths = await fs.promises.readdir(dir); + const apps = await Promise.all( + appPaths.map((p) => adapter.readByPath(path.join(dir, p))), + ); + return apps.flatMap((app) => (app.ok ? [app.unwrap()] : [])); + }), + readByPath: (p: string) => + Result.wrapAsync(async () => { + const isElectronBased = fs.existsSync( + path.join(p, "Contents/Frameworks/Electron Framework.framework"), + ); + if (!isElectronBased) throw new Error("Not an electron app"); - const info = await readPlistFile(path.join(p, "Contents/Info.plist")); + const info = await readPlistFile(path.join(p, "Contents/Info.plist")); + const icon = await readIcnsAsImageUri( + path.join(p, "Contents/Resources", info.CFBundleIconFile), + ); - const icon = await readIcnsAsImageUri( - path.join(p, "Contents/Resources", info.CFBundleIconFile), - ); - - return { - id: info.CFBundleIdentifier, - name: info.CFBundleName, - icon, - exePath: path.resolve(p, "Contents/MacOS", info.CFBundleExecutable), - }; - }, + return { + id: info.CFBundleIdentifier, + name: info.CFBundleName, + icon, + exePath: path.resolve(p, "Contents/MacOS", info.CFBundleExecutable), + }; + }), }; diff --git a/src/platforms/utils.ts b/src/platforms/utils.ts index 383a0d9..1be1790 100644 --- a/src/platforms/utils.ts +++ b/src/platforms/utils.ts @@ -1,6 +1,7 @@ import type { AppInfo } from "../reducers/app"; +import { Result } from "ts-results"; export interface AppReader { - readAll(): Promise<(AppInfo | undefined)[]>; - readByPath(p: string): Promise; + readAll(): Promise>; + readByPath(p: string): Promise>; } diff --git a/src/platforms/win.ts b/src/platforms/win.ts index 6892cee..bafe07a 100644 --- a/src/platforms/win.ts +++ b/src/platforms/win.ts @@ -1,4 +1,3 @@ -import { readdirSafe } from "../main/utils"; import type { AppInfo } from "../reducers/app"; import type { AppReader } from "./utils"; import fs from "fs"; @@ -11,6 +10,7 @@ import { enumerateKeys, enumerateValues, } from "registry-js"; +import { Result } from "ts-results"; async function getAppInfoByExePath( exePath: string, @@ -34,11 +34,9 @@ async function getAppInfoByExePath( }; } -async function getAppInfoFromRegeditItemValues( +const getAppInfoFromRegeditItemValues = async ( values: readonly RegistryValue[], -): Promise { - if (values.length === 0) return; - +) => { let iconPath = ""; // Try to find executable path of Electron app @@ -50,7 +48,9 @@ async function getAppInfoFromRegeditItemValues( if (displayIcon) { const [icon] = displayIcon.data.split(","); if (icon?.toLowerCase().endsWith(".exe")) { - if (!isElectronApp(path.dirname(icon))) return; + if (!isElectronApp(path.dirname(icon))) { + throw new Error("not and electron app"); + } return getAppInfoByExePath(icon, iconPath, values); } else if (icon?.toLowerCase().endsWith(".ico")) { iconPath = icon; @@ -70,22 +70,24 @@ async function getAppInfoFromRegeditItemValues( installDir = path.dirname(iconPath); } - if (!installDir) return; - - const exeFile = await findExeFile(installDir); - if (exeFile) { - return getAppInfoByExePath(exeFile, iconPath, values); - } else { - const files = await readdirSafe(installDir); - const semverDir = files.find((file) => /\d+\.\d+\.\d+/.test(file)); - if (!semverDir) return; - - const exeFile = await findExeFile(path.join(installDir, semverDir)); - if (!exeFile) return; - - return getAppInfoByExePath(exeFile, iconPath, values); + if (installDir) { + const exeFile = await findExeFile(installDir); + if (exeFile) { + return getAppInfoByExePath(exeFile, iconPath, values); + } else { + const files = await fs.promises.readdir(installDir); + const semverDir = files.find((file) => /\d+\.\d+\.\d+/.test(file)); + if (semverDir) { + const exeFile = await findExeFile(path.join(installDir, semverDir)); + if (exeFile) { + return getAppInfoByExePath(exeFile, iconPath, values); + } + } + } } -} + + throw new Error("app not found"); +}; function isElectronApp(installDir: string) { return ( @@ -98,7 +100,7 @@ function isElectronApp(installDir: string) { async function findExeFile(dir: string) { if (isElectronApp(dir)) { - const files = await readdirSafe(dir); + const files = await fs.promises.readdir(dir); const [exeFile] = files.filter((file) => { const lc = file.toLowerCase(); return ( @@ -111,40 +113,43 @@ async function findExeFile(dir: string) { } export const adapter: AppReader = { - async readAll() { - const enumRegeditItems = (key: HKEY, subkey: string) => { - return enumerateKeys(key, subkey).map((k) => - enumerateValues(key, subkey + "\\" + k), + readAll: () => + Result.wrapAsync(async () => { + const enumRegeditItems = (key: HKEY, subkey: string) => { + return enumerateKeys(key, subkey).map((k) => + enumerateValues(key, subkey + "\\" + k), + ); + }; + + const items = [ + ...enumRegeditItems( + HKEY.HKEY_LOCAL_MACHINE, + "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall", + ), + ...enumRegeditItems( + HKEY.HKEY_LOCAL_MACHINE, + "Software\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall", + ), + ...enumRegeditItems( + HKEY.HKEY_CURRENT_USER, + "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall", + ), + ]; + return Promise.all( + items.map((itemValues) => getAppInfoFromRegeditItemValues(itemValues)), ); - }; - - const items = [ - ...enumRegeditItems( - HKEY.HKEY_LOCAL_MACHINE, - "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall", - ), - ...enumRegeditItems( - HKEY.HKEY_LOCAL_MACHINE, - "Software\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall", - ), - ...enumRegeditItems( - HKEY.HKEY_CURRENT_USER, - "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall", - ), - ]; - return Promise.all( - items.map((itemValues) => getAppInfoFromRegeditItemValues(itemValues)), - ); - }, - - async readByPath(p: string) { - if (path.extname(p).toLowerCase() != ".exe") return; - - return { - id: p, - name: path.basename(p, ".exe"), - icon: "", - exePath: p, - }; - }, + }), + + readByPath: (p: string) => + Result.wrapAsync(async () => { + if (path.extname(p).toLowerCase() === ".exe") + throw new Error("should be suffixed with exe"); + + return { + id: p, + name: path.basename(p, ".exe"), + icon: "", + exePath: p, + }; + }), }; diff --git a/src/reducers/app.ts b/src/reducers/app.ts index 1e2a827..359a31a 100644 --- a/src/reducers/app.ts +++ b/src/reducers/app.ts @@ -29,12 +29,10 @@ export const appSlice = buildCreateSlice({ async () => { if (IN_MAIN_PROCESS) { const { adapter } = await importByPlatform(); - const apps = adapter - .readAll() - .then((apps) => - apps.filter((a): a is AppInfo => typeof a !== "undefined"), - ); - return apps; + const apps = await adapter.readAll(); + + if (!apps.ok) throw new Error("Failed to read apps"); + return apps.unwrap(); } else { return []; } diff --git a/src/reducers/session.ts b/src/reducers/session.ts index e3aef04..1c43a8d 100644 --- a/src/reducers/session.ts +++ b/src/reducers/session.ts @@ -145,9 +145,10 @@ export const sessionSlice = buildCreateSlice({ if (IN_MAIN_PROCESS) { const { adapter } = await importByPlatform(); const current = await adapter.readByPath(p); - if (current) { - dispatch(appSlice.actions.addTemp(current)); // TODO: Remove it after session closed - dispatch(sessionSlice.actions.debug(current)); + + if (current.ok) { + dispatch(appSlice.actions.addTemp(current.unwrap())); // TODO: Remove it after session closed + dispatch(sessionSlice.actions.debug(current.unwrap())); } else { alert( "Invalid application path: " + diff --git a/yarn.lock b/yarn.lock index 770d67e..339214e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2793,6 +2793,7 @@ __metadata: react-use: "npm:^17.5.0" registry-js: "npm:^1.15.1" simple-plist: "npm:1.3.1" + ts-results: "npm:^3.3.0" typescript: "npm:^5.3.3" universal-analytics: "npm:^0.5.3" uuid: "npm:^9.0.1" @@ -8369,6 +8370,13 @@ __metadata: languageName: node linkType: hard +"ts-results@npm:^3.3.0": + version: 3.3.0 + resolution: "ts-results@npm:3.3.0" + checksum: 10c0/507659005733dd102895e868fe1de6f4c39998bcf188863bdc2d18dc5e8ced57be2e83fe9ee53adbabc414fd53e26e8654c7fa0273d2668decc1731d274dbf74 + languageName: node + linkType: hard + "tslib@npm:^2.0.0, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.6.2, tslib@npm:~2.6.2": version: 2.6.2 resolution: "tslib@npm:2.6.2"