watch directory for newly installed packages (#624)

This commit is contained in:
ABevier 2023-05-26 00:56:59 -04:00 committed by GitHub
parent 6f4725d21c
commit ec166132c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 163 additions and 126 deletions

View file

@ -1,5 +1,12 @@
import { ipcMain, app, BrowserWindow } from "electron"; 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 { readSessionData, writeSessionData, pollAuth } from "./auth";
import type { Packages, Session } from "../../src/libs/types"; import type { Packages, Session } from "../../src/libs/types";
import log from "./logger"; 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 () => { ipcMain.handle("get-session", async () => {
try { try {
log.info("getting session"); log.info("getting session");
@ -230,4 +247,22 @@ export default function initializeHandlers({ notifyMainWindow }: HandlerOptions)
return false; 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;
}
});
} }

View file

@ -1,4 +1,3 @@
// import { readDir, BaseDirectory } from '@tauri-apps/api/fs';
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import { app } from "electron"; import { app } from "electron";
@ -8,12 +7,8 @@ import { mkdirp } from "mkdirp";
import fetch from "node-fetch"; import fetch from "node-fetch";
import { SemVer, isValidSemVer } from "@tea/libtea"; import { SemVer, isValidSemVer } from "@tea/libtea";
import { execSync } from "child_process"; import { execSync } from "child_process";
import chokidar from "chokidar";
type Dir = { import { MainWindowNotifier } from "./types";
name: string;
path: string;
children?: Dir[];
};
type ParsedVersion = { full_name: string; semVer: SemVer }; type ParsedVersion = { full_name: string; semVer: SemVer };
@ -38,9 +33,18 @@ export const getGuiPath = () => {
return path.join(getTeaPath(), "tea.xyz/gui"); return path.join(getTeaPath(), "tea.xyz/gui");
}; };
export async function getInstalledPackages(): Promise<InstalledPackage[]> { export async function getInstalledVersionsForPackage(fullName: string): Promise<InstalledPackage> {
const pkgsPath = getTeaPath(); 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<InstalledPackage[]> {
return findInstalledVersions(getTeaPath());
}
async function findInstalledVersions(pkgsPath: string): Promise<InstalledPackage[]> {
if (!fs.existsSync(pkgsPath)) { if (!fs.existsSync(pkgsPath)) {
log.info(`packages path ${pkgsPath} does not exist, no installed packages`); log.info(`packages path ${pkgsPath} does not exist, no installed packages`);
return []; 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 ({ export const deepReadDir = async ({
dir, dir,
continueDeeper, continueDeeper,
@ -181,7 +158,7 @@ export async function deletePackageFolder(fullName, version) {
try { try {
const foldPath = path.join(getTeaPath(), fullName, `v${version}`); const foldPath = path.join(getTeaPath(), fullName, `v${version}`);
log.info("rm:", foldPath); log.info("rm:", foldPath);
await fs.rmdirSync(foldPath, { recursive: true }); await fs.rmSync(foldPath, { recursive: true });
} catch (error) { } catch (error) {
log.error(error); log.error(error);
} }
@ -217,3 +194,51 @@ export async function cacheImage(url: string): Promise<string> {
return `file://${imagePath}`; 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;
}

View file

@ -95,6 +95,7 @@
"axios": "^1.3.2", "axios": "^1.3.2",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"chokidar": "^3.5.3",
"custom-electron-titlebar": "4.2.0-beta.0", "custom-electron-titlebar": "4.2.0-beta.0",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"electron-context-menu": "^3.6.1", "electron-context-menu": "^3.6.1",

View file

@ -48,6 +48,15 @@ export async function getInstalledPackages(): Promise<InstalledPackage[]> {
return pkgs; return pkgs;
} }
export async function getInstalledVersionsForPackage(fullName: string): Promise<InstalledPackage> {
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<GUIPackage[]> { export async function getPackages(): Promise<GUIPackage[]> {
const [packages, installedPackages] = await Promise.all([ const [packages, installedPackages] = await Promise.all([
getDistPackages(), getDistPackages(),
@ -278,3 +287,17 @@ export const isDev = async () => {
return false; 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;
}
};

View file

@ -202,6 +202,13 @@ export async function getDistPackages(): Promise<Package[]> {
return packages; return packages;
} }
export async function getInstalledVersionsForPackage(full_name: string): Promise<Package> {
return (packages.find((pkg) => pkg.full_name === full_name) ?? {
full_name,
installed_versions: []
}) as Package;
}
export async function getPackages(): Promise<GUIPackage[]> { export async function getPackages(): Promise<GUIPackage[]> {
return packages.map((pkg) => { return packages.map((pkg) => {
return { return {
@ -399,3 +406,11 @@ export async function openPackageEntrypointInTerminal(pkg: string) {
export const pollDeviceSession = async () => { export const pollDeviceSession = async () => {
console.log("do nothing"); console.log("do nothing");
}; };
export const monitorTeaDir = async () => {
console.log("do nothing");
};
export const stopMonitoringTeaDir = async () => {
console.log("do nothing");
};

View file

@ -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"
]);
});
});

View file

@ -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"]
};

View file

@ -12,7 +12,10 @@ import {
writePackageCache, writePackageCache,
syncPantry, syncPantry,
cacheImageURL, cacheImageURL,
listenToChannel listenToChannel,
getInstalledVersionsForPackage,
monitorTeaDir,
stopMonitoringTeaDir
} from "@native"; } from "@native";
import { getReadme, getContributors, getRepoAsPackage } from "$libs/github"; import { getReadme, getContributors, getRepoAsPackage } from "$libs/github";
@ -26,7 +29,6 @@ import withRetry from "$libs/utils/retry";
import log from "$libs/logger"; import log from "$libs/logger";
import { isPackageUpToDate } from "../packages/pkg-utils"; import { isPackageUpToDate } from "../packages/pkg-utils";
import withDelay from "$libs/utils/delay";
import { indexPackages, searchPackages } from "$libs/search-index"; import { indexPackages, searchPackages } from "$libs/search-index";
@ -150,6 +152,7 @@ const init = async function () {
packageMap.set(cachedPkgs); packageMap.set(cachedPkgs);
await refreshPackages(); await refreshPackages();
await monitorTeaDir();
initialized = true; initialized = true;
} }
log.info("packages store: initialized!"); log.info("packages store: initialized!");
@ -201,12 +204,20 @@ const refreshPackages = async () => {
refreshTimeoutId = setTimeout(() => refreshPackages(), packageRefreshInterval); // refresh every hour 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 // Destructor for the package store
const destroy = () => { const destroy = async () => {
isDestroyed = true; isDestroyed = true;
if (refreshTimeoutId) { if (refreshTimeoutId) {
clearTimeout(refreshTimeoutId); clearTimeout(refreshTimeoutId);
} }
await stopMonitoringTeaDir();
log.info("packages store: destroyed"); log.info("packages store: destroyed");
}; };
@ -218,7 +229,7 @@ const installPkg = async (pkg: GUIPackage, version?: string) => {
await installPackage(pkg, versionToInstall); await installPackage(pkg, versionToInstall);
trackInstall(pkg.full_name); trackInstall(pkg.full_name);
await refreshPackages(); // helps e2e tests might not be the most efficient but helps await refreshSinglePackage(pkg.full_name);
notificationStore.add({ notificationStore.add({
message: `Package ${pkg.full_name} v${versionToInstall} has been installed.` message: `Package ${pkg.full_name} v${versionToInstall} has been installed.`
@ -252,9 +263,7 @@ const uninstallPkg = async (pkg: GUIPackage) => {
await deletePkg(pkg, v); await deletePkg(pkg, v);
} }
updatePackage(pkg.full_name, { await refreshSinglePackage(pkg.full_name);
installed_versions: []
});
} catch (error) { } catch (error) {
log.error(error); log.error(error);
notificationStore.add({ notificationStore.add({
@ -300,6 +309,7 @@ listenToChannel("install-progress", ({ full_name, progress }: any) => {
updatePackage(full_name, { install_progress_percentage: progress }); updatePackage(full_name, { install_progress_percentage: progress });
}); });
// TODO: perhaps this can be combined with pkg-modified?
listenToChannel("pkg-installed", ({ full_name, version }: any) => { listenToChannel("pkg-installed", ({ full_name, version }: any) => {
if (!full_name) { if (!full_name) {
return; return;
@ -307,6 +317,13 @@ listenToChannel("pkg-installed", ({ full_name, version }: any) => {
updatePackage(full_name, {}, version); updatePackage(full_name, {}, version);
}); });
listenToChannel("pkg-modified", ({ full_name }: any) => {
if (!full_name) {
return;
}
refreshSinglePackage(full_name);
});
// This is only used for uninstall now // This is only used for uninstall now
export const withFakeLoader = ( export const withFakeLoader = (
pkg: GUIPackage, pkg: GUIPackage,

View file

@ -52,6 +52,7 @@ importers:
axios: ^1.3.2 axios: ^1.3.2
bcryptjs: ^2.4.3 bcryptjs: ^2.4.3
buffer: ^6.0.3 buffer: ^6.0.3
chokidar: ^3.5.3
concurrently: ^7.6.0 concurrently: ^7.6.0
cross-env: ^7.0.3 cross-env: ^7.0.3
custom-electron-titlebar: 4.2.0-beta.0 custom-electron-titlebar: 4.2.0-beta.0
@ -115,6 +116,7 @@ importers:
axios: 1.4.0 axios: 1.4.0
bcryptjs: 2.4.3 bcryptjs: 2.4.3
buffer: 6.0.3 buffer: 6.0.3
chokidar: 3.5.3
custom-electron-titlebar: 4.2.0-beta.0_electron@22.1.0 custom-electron-titlebar: 4.2.0-beta.0_electron@22.1.0
dayjs: 1.11.7 dayjs: 1.11.7
electron-context-menu: 3.6.1 electron-context-menu: 3.6.1