diff --git a/modules/desktop/electron/libs/ipc.ts b/modules/desktop/electron/libs/ipc.ts index 4e166a2..dbef15c 100644 --- a/modules/desktop/electron/libs/ipc.ts +++ b/modules/desktop/electron/libs/ipc.ts @@ -1,5 +1,12 @@ import { ipcMain, app, BrowserWindow } from "electron"; -import { deletePackageFolder, getInstalledPackages, cacheImage } from "./tea-dir"; +import { + deletePackageFolder, + getInstalledPackages, + getInstalledVersionsForPackage, + cacheImage, + startMonitoringTeaDir, + stopMonitoringTeaDir +} from "./tea-dir"; import { readSessionData, writeSessionData, pollAuth } from "./auth"; import type { Packages, Session } from "../../src/libs/types"; import log from "./logger"; @@ -37,6 +44,16 @@ export default function initializeHandlers({ notifyMainWindow }: HandlerOptions) } }); + ipcMain.handle("get-installed-package-versions", async (_, fullName: string) => { + try { + log.info(`getting installed versions for package: ${fullName}`); + return await getInstalledVersionsForPackage(fullName); + } catch (error) { + log.error(error); + return error; + } + }); + ipcMain.handle("get-session", async () => { try { log.info("getting session"); @@ -230,4 +247,22 @@ export default function initializeHandlers({ notifyMainWindow }: HandlerOptions) return false; } }); + + ipcMain.handle("monitor-tea-dir", async () => { + try { + await startMonitoringTeaDir(notifyMainWindow); + } catch (err) { + log.error("Failed to monitor tea dir", err); + return err; + } + }); + + ipcMain.handle("stop-monitor-tea-dir", async () => { + try { + await stopMonitoringTeaDir(); + } catch (err) { + log.error("Failed to stop monitoring tea dir", err); + return err; + } + }); } diff --git a/modules/desktop/electron/libs/tea-dir.ts b/modules/desktop/electron/libs/tea-dir.ts index 4e09d19..ff07eea 100644 --- a/modules/desktop/electron/libs/tea-dir.ts +++ b/modules/desktop/electron/libs/tea-dir.ts @@ -1,4 +1,3 @@ -// import { readDir, BaseDirectory } from '@tauri-apps/api/fs'; import fs from "fs"; import path from "path"; import { app } from "electron"; @@ -8,12 +7,8 @@ import { mkdirp } from "mkdirp"; import fetch from "node-fetch"; import { SemVer, isValidSemVer } from "@tea/libtea"; import { execSync } from "child_process"; - -type Dir = { - name: string; - path: string; - children?: Dir[]; -}; +import chokidar from "chokidar"; +import { MainWindowNotifier } from "./types"; type ParsedVersion = { full_name: string; semVer: SemVer }; @@ -38,9 +33,18 @@ export const getGuiPath = () => { return path.join(getTeaPath(), "tea.xyz/gui"); }; -export async function getInstalledPackages(): Promise { - const pkgsPath = getTeaPath(); +export async function getInstalledVersionsForPackage(fullName: string): Promise { + const pkgsPath = path.join(getTeaPath(), fullName); + const result = await findInstalledVersions(pkgsPath); + const pkg = result.find((v) => v.full_name === fullName); + return pkg ?? { full_name: fullName, installed_versions: [] }; +} +export async function getInstalledPackages(): Promise { + return findInstalledVersions(getTeaPath()); +} + +async function findInstalledVersions(pkgsPath: string): Promise { if (!fs.existsSync(pkgsPath)) { log.info(`packages path ${pkgsPath} does not exist, no installed packages`); return []; @@ -89,33 +93,6 @@ const parseVersionFromPath = (versionPath: string): ParsedVersion | null => { } }; -const semverTest = - /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/g; - -export const getPkgBottles = (packageDir: Dir): string[] => { - log.info("getting installed bottle for ", packageDir); - const bottles: string[] = []; - - const pkg = packageDir.path.split(".tea/")[1]; - const version = pkg.split("/v")[1]; - - const isVersion = semverTest.test(version) || !isNaN(+version) || version === "*"; - - if (version && isVersion) { - bottles.push(pkg); - } else if (packageDir?.children?.length) { - const childBottles = packageDir.children - .map(getPkgBottles) - .reduce((arr, bottles) => [...arr, ...bottles], []); - bottles.push(...childBottles); - } - - const foundBottles = bottles.filter((b) => b !== undefined).sort(); // ie: ["gohugo.io/v*", "gohugo.io/v0", "gohugo.io/v0.108", "gohugo.io/v0.108.0"] - - log.info(`Found ${foundBottles.length} bottles from `, packageDir); - return foundBottles; -}; - export const deepReadDir = async ({ dir, continueDeeper, @@ -181,7 +158,7 @@ export async function deletePackageFolder(fullName, version) { try { const foldPath = path.join(getTeaPath(), fullName, `v${version}`); log.info("rm:", foldPath); - await fs.rmdirSync(foldPath, { recursive: true }); + await fs.rmSync(foldPath, { recursive: true }); } catch (error) { log.error(error); } @@ -217,3 +194,51 @@ export async function cacheImage(url: string): Promise { return `file://${imagePath}`; } + +let watcher: chokidar.FSWatcher | null = null; + +export async function startMonitoringTeaDir(mainWindowNotifier: MainWindowNotifier) { + if (watcher) { + log.info("Watcher already started"); + return; + } + + const dir = path.join(getTeaPath(), "**/v*"); + log.info(`Start monitoring tea dir: ${dir}}`); + + watcher = chokidar.watch(dir, { + ignoreInitial: true, + persistent: true, + followSymlinks: false, + depth: 5, + ignored: ["**/var/pantry/projects/**", "**/local/tmp/**", "**/share/**"] + }); + + watcher + .on("addDir", (pth) => { + const dir = path.dirname(pth); + const version = path.basename(pth); + if (isValidSemVer(version) && !fs.lstatSync(pth).isSymbolicLink()) { + const full_name = dir.split(".tea/")[1]; + log.info(`Monitor - Added Package: ${full_name} v${version}`); + mainWindowNotifier("pkg-modified", { full_name, version, type: "add" }); + } + }) + .on("unlinkDir", (pth) => { + // FIXME: unlinkDir does not always fire, this is a bug in chokidar + const dir = path.dirname(pth); + const version = path.basename(pth); + if (isValidSemVer(version)) { + const full_name = dir.split(".tea/")[1]; + log.info(`Monitor - Removed Package: ${full_name} v${version}`); + mainWindowNotifier("pkg-modified", { full_name, version, type: "remove" }); + } + }) + .on("error", (error) => log.error(`Watcher error: ${error}`)); +} + +export async function stopMonitoringTeaDir() { + log.info("Stop monitoring tea dir"); + await watcher?.close(); + watcher = null; +} diff --git a/modules/desktop/package.json b/modules/desktop/package.json index 53f816d..c2ccebd 100644 --- a/modules/desktop/package.json +++ b/modules/desktop/package.json @@ -95,6 +95,7 @@ "axios": "^1.3.2", "bcryptjs": "^2.4.3", "buffer": "^6.0.3", + "chokidar": "^3.5.3", "custom-electron-titlebar": "4.2.0-beta.0", "dayjs": "^1.11.7", "electron-context-menu": "^3.6.1", diff --git a/modules/desktop/src/libs/native-electron.ts b/modules/desktop/src/libs/native-electron.ts index 348496e..1eb8a2d 100644 --- a/modules/desktop/src/libs/native-electron.ts +++ b/modules/desktop/src/libs/native-electron.ts @@ -48,6 +48,15 @@ export async function getInstalledPackages(): Promise { return pkgs; } +export async function getInstalledVersionsForPackage(fullName: string): Promise { + log.info("getting installed versions for package: ", fullName); + const result = await ipcRenderer.invoke("get-installed-package-versions", fullName); + if (result instanceof Error) { + throw result; + } + return result as InstalledPackage; +} + export async function getPackages(): Promise { const [packages, installedPackages] = await Promise.all([ getDistPackages(), @@ -278,3 +287,17 @@ export const isDev = async () => { return false; } }; + +export const monitorTeaDir = async () => { + const result = await ipcRenderer.invoke("monitor-tea-dir"); + if (result instanceof Error) { + throw result; + } +}; + +export const stopMonitoringTeaDir = async () => { + const result = await ipcRenderer.invoke("stop-monitor-tea-dir"); + if (result instanceof Error) { + throw result; + } +}; diff --git a/modules/desktop/src/libs/native-mock.ts b/modules/desktop/src/libs/native-mock.ts index d9fbd28..3ba5c75 100644 --- a/modules/desktop/src/libs/native-mock.ts +++ b/modules/desktop/src/libs/native-mock.ts @@ -202,6 +202,13 @@ export async function getDistPackages(): Promise { return packages; } +export async function getInstalledVersionsForPackage(full_name: string): Promise { + return (packages.find((pkg) => pkg.full_name === full_name) ?? { + full_name, + installed_versions: [] + }) as Package; +} + export async function getPackages(): Promise { return packages.map((pkg) => { return { @@ -399,3 +406,11 @@ export async function openPackageEntrypointInTerminal(pkg: string) { export const pollDeviceSession = async () => { console.log("do nothing"); }; + +export const monitorTeaDir = async () => { + console.log("do nothing"); +}; + +export const stopMonitoringTeaDir = async () => { + console.log("do nothing"); +}; diff --git a/modules/desktop/src/libs/native/__tests__/teaDir.test.ts b/modules/desktop/src/libs/native/__tests__/teaDir.test.ts deleted file mode 100644 index 31959fe..0000000 --- a/modules/desktop/src/libs/native/__tests__/teaDir.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { getPkgBottles } from "../tea-dir"; - -describe("tea-dir module", () => { - it("should getPkgBottles from nested Dir object/s", () => { - const results = getPkgBottles({ - name: "kkos", - path: "/Users/x/.tea/github.com/kkos", - children: [ - { name: ".DS_Store", path: "/Users/x/.tea/github.com/kkos/.DS_Store" }, - { - name: "oniguruma", - path: "/Users/x/.tea/github.com/kkos/oniguruma", - children: [ - { name: ".DS_Store", path: "/Users/x/.tea/github.com/kkos/oniguruma/.DS_Store" }, - { - path: "/Users/x/.tea/github.com/kkos/oniguruma/v6", - name: "v6", - children: [ - { name: ".DS_Store", path: "/Users/x/.tea/github.com/kkos/oniguruma/v6/.DS_Store" } - ] - }, - { - name: "v*", - path: "/Users/x/.tea/github.com/kkos/oniguruma/v*", - children: [] - }, - { - name: "v6.9.8", - path: "/Users/x/.tea/github.com/kkos/oniguruma/v6.9.8", - children: [] - }, - { - name: "v6.9", - path: "/Users/x/.tea/github.com/kkos/oniguruma/v6.9", - children: [] - } - ] - } - ] - }); - - expect(results).toEqual([ - "github.com/kkos/oniguruma/v*", - "github.com/kkos/oniguruma/v6", - "github.com/kkos/oniguruma/v6.9", - "github.com/kkos/oniguruma/v6.9.8" - ]); - }); -}); diff --git a/modules/desktop/src/libs/native/tea-dir.ts b/modules/desktop/src/libs/native/tea-dir.ts deleted file mode 100644 index 533b5a9..0000000 --- a/modules/desktop/src/libs/native/tea-dir.ts +++ /dev/null @@ -1,32 +0,0 @@ -// import { app } from 'electron'; -// import fs from 'fs'; -// import { join } from 'upath'; - -type Dir = { - name: string; - path: string; - children?: Dir[]; -}; - -const semverTest = - /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/g; - -export const getPkgBottles = (packageDir: Dir): string[] => { - const bottles: string[] = []; - - const pkg = packageDir.path.split(".tea/")[1]; - const version = pkg.split("/v")[1]; - - const isVersion = semverTest.test(version) || !isNaN(+version) || version === "*"; - - if (version && isVersion) { - bottles.push(pkg); - } else if (packageDir?.children?.length) { - const childBottles = packageDir.children - .map(getPkgBottles) - .reduce((arr, bottles) => [...arr, ...bottles], []); - bottles.push(...childBottles); - } - - return bottles.filter((b) => b !== undefined).sort(); // ie: ["gohugo.io/v*", "gohugo.io/v0", "gohugo.io/v0.108", "gohugo.io/v0.108.0"] -}; diff --git a/modules/desktop/src/libs/stores/pkgs.ts b/modules/desktop/src/libs/stores/pkgs.ts index a6da9ad..c4df520 100644 --- a/modules/desktop/src/libs/stores/pkgs.ts +++ b/modules/desktop/src/libs/stores/pkgs.ts @@ -12,7 +12,10 @@ import { writePackageCache, syncPantry, cacheImageURL, - listenToChannel + listenToChannel, + getInstalledVersionsForPackage, + monitorTeaDir, + stopMonitoringTeaDir } from "@native"; import { getReadme, getContributors, getRepoAsPackage } from "$libs/github"; @@ -26,7 +29,6 @@ import withRetry from "$libs/utils/retry"; import log from "$libs/logger"; import { isPackageUpToDate } from "../packages/pkg-utils"; -import withDelay from "$libs/utils/delay"; import { indexPackages, searchPackages } from "$libs/search-index"; @@ -150,6 +152,7 @@ const init = async function () { packageMap.set(cachedPkgs); await refreshPackages(); + await monitorTeaDir(); initialized = true; } log.info("packages store: initialized!"); @@ -201,12 +204,20 @@ const refreshPackages = async () => { refreshTimeoutId = setTimeout(() => refreshPackages(), packageRefreshInterval); // refresh every hour }; +const refreshSinglePackage = async (fullName: string) => { + log.info(`refreshing single package: ${fullName}`); + const result = await getInstalledVersionsForPackage(fullName); + log.info(`package: ${fullName} has installed versions:`, result.installed_versions); + updatePackage(fullName, { installed_versions: result.installed_versions }); +}; + // Destructor for the package store -const destroy = () => { +const destroy = async () => { isDestroyed = true; if (refreshTimeoutId) { clearTimeout(refreshTimeoutId); } + await stopMonitoringTeaDir(); log.info("packages store: destroyed"); }; @@ -218,7 +229,7 @@ const installPkg = async (pkg: GUIPackage, version?: string) => { await installPackage(pkg, versionToInstall); trackInstall(pkg.full_name); - await refreshPackages(); // helps e2e tests might not be the most efficient but helps + await refreshSinglePackage(pkg.full_name); notificationStore.add({ message: `Package ${pkg.full_name} v${versionToInstall} has been installed.` @@ -252,9 +263,7 @@ const uninstallPkg = async (pkg: GUIPackage) => { await deletePkg(pkg, v); } - updatePackage(pkg.full_name, { - installed_versions: [] - }); + await refreshSinglePackage(pkg.full_name); } catch (error) { log.error(error); notificationStore.add({ @@ -300,6 +309,7 @@ listenToChannel("install-progress", ({ full_name, progress }: any) => { updatePackage(full_name, { install_progress_percentage: progress }); }); +// TODO: perhaps this can be combined with pkg-modified? listenToChannel("pkg-installed", ({ full_name, version }: any) => { if (!full_name) { return; @@ -307,6 +317,13 @@ listenToChannel("pkg-installed", ({ full_name, version }: any) => { updatePackage(full_name, {}, version); }); +listenToChannel("pkg-modified", ({ full_name }: any) => { + if (!full_name) { + return; + } + refreshSinglePackage(full_name); +}); + // This is only used for uninstall now export const withFakeLoader = ( pkg: GUIPackage, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac06998..38c1334 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,6 +52,7 @@ importers: axios: ^1.3.2 bcryptjs: ^2.4.3 buffer: ^6.0.3 + chokidar: ^3.5.3 concurrently: ^7.6.0 cross-env: ^7.0.3 custom-electron-titlebar: 4.2.0-beta.0 @@ -115,6 +116,7 @@ importers: axios: 1.4.0 bcryptjs: 2.4.3 buffer: 6.0.3 + chokidar: 3.5.3 custom-electron-titlebar: 4.2.0-beta.0_electron@22.1.0 dayjs: 1.11.7 electron-context-menu: 3.6.1