use libtea instead of installing the cli (#649)

This commit is contained in:
ABevier 2023-06-04 20:06:54 -04:00 committed by GitHub
parent 79bd897146
commit 46c8b26682
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 53 additions and 254 deletions

View file

@ -14,7 +14,7 @@ import initializePushNotification, {
syncPackageTopicSubscriptions syncPackageTopicSubscriptions
} from "./libs/push-notification"; } from "./libs/push-notification";
import init, { initializeTeaCli } from "./libs/initialize"; import init from "./libs/initialize";
import { readSessionData } from "./libs/auth"; import { readSessionData } from "./libs/auth";
import { isDev } from "./libs/auto-updater"; import { isDev } from "./libs/auto-updater";
@ -38,11 +38,10 @@ if (app.isPackaged) {
} }
}); });
Sentry.configureScope(async (scope) => { Sentry.configureScope(async (scope) => {
const [session, cliVersion] = await Promise.all([readSessionData(), initializeTeaCli()]); const session = await readSessionData();
scope.setUser({ scope.setUser({
id: session.device_id, // device_id this should exist in our pg db: developer_id is to many device_id id: session.device_id, // device_id this should exist in our pg db: developer_id is to many device_id
username: session?.user?.login || "", // github username or handler username: session?.user?.login || "" // github username or handler
tea: cliVersion
}); });
}); });
setSentryLogging(Sentry); setSentryLogging(Sentry);

View file

@ -1,139 +1,76 @@
import { spawn, exec } from "child_process"; import { spawn } from "child_process";
import { getGuiPath, getTeaPath } from "./tea-dir"; import { getGuiPath } from "./tea-dir";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import { initializeTeaCli } from "./initialize"; import { hooks } from "@teaxyz/lib";
import { app } from "electron";
import log from "./logger"; import log from "./logger";
import { MainWindowNotifier } from "./types"; import { MainWindowNotifier } from "./types";
import { Installation, Package, porcelain } from "@teaxyz/lib";
// Be careful with globbing when passing this to a shell which might expand it. Either escape it or quote it. import type { Resolution } from "@teaxyz/lib/script/src/plumbing/resolve";
export const cliBinPath = path.join(getTeaPath(), "tea.xyz/v*/bin/tea");
export async function installPackage( export async function installPackage(
full_name: string, full_name: string,
version: string, version: string,
notifyMainWindow: MainWindowNotifier notifyMainWindow: MainWindowNotifier
) { ) {
const teaVersion = await initializeTeaCli(); const notifier = newInstallProgressNotifier(full_name, notifyMainWindow);
const progressNotifier = newInstallProgressNotifier(full_name, notifyMainWindow);
if (!teaVersion) throw new Error("no tea");
const qualifedPackage = `${full_name}@${version}`; const qualifedPackage = `${full_name}@${version}`;
log.info(`installing package ${qualifedPackage}`); log.info(`installing package ${qualifedPackage}`);
const result = await porcelain.install(qualifedPackage, notifier);
let stdout = ""; console.log(`successfully installed ${qualifedPackage}`, result);
let stderr = "";
await new Promise((resolve, reject) => {
// tea requires HOME to be set.
const opts = { env: { HOME: app.getPath("home"), NO_COLOR: "1" } };
const child = spawn(
cliBinPath,
["--env=false", "--sync", "--json", `+${qualifedPackage}`],
opts
);
child.stdout.on("data", (data) => {
stdout += data.toString().trim();
});
child.stderr.on("data", (data) => {
try {
data
.toString()
.split("\n")
.map((s: string) => s.trim())
.filter((s: string) => s.length > 0)
.forEach((line) => {
try {
const msg = JSON.parse(line.trim());
progressNotifier(msg);
} catch (err) {
log.error("handling cli notification line", line, err);
}
});
stderr += data.toString();
} catch (err) {
log.error("error processing cli data", data.toString(), err);
}
});
child.on("exit", (code) => {
log.info("cli exited with code:", code);
log.info("cli stdout:", stdout);
if (code !== 0) {
log.info("cli stderr:", stderr);
reject(new Error("tea exited with non-zero code: " + code));
} else {
resolve(null);
}
});
child.on("error", () => {
reject(new Error(stderr));
});
});
} }
// This is hacky and kind of complex because of the output we get from the CLI. When the CLI
// gives better output this definitely should get looked at.
function newInstallProgressNotifier(full_name: string, notifyMainWindow: MainWindowNotifier) { function newInstallProgressNotifier(full_name: string, notifyMainWindow: MainWindowNotifier) {
// the install progress is super spammy, only send every 10th update
let counter = 0;
// the totall number of packages to install - this is set by the "resolved" message // the totall number of packages to install - this is set by the "resolved" message
let numberOfPackages = 1; let numberOfPackages = 1;
// the current package number - this is incremented by the "installed" or "downloaded" message // the current package number - this is incremented by the "installed" or "downloaded" message
let currentPackageNumber = 0; let currentPackageNumber = 0;
return function (msg: any) { return {
if (msg.status !== "downloading" && msg.status !== "installing") { resolved: ({ pending }: Resolution) => {
log.info("cli:", msg); numberOfPackages = pending.length ?? 1;
} log.info(`resolved ${numberOfPackages} packages to install`);
},
if (msg.status === "resolved") { installing: ({ pkg, progress }: { pkg: Package; progress: number | undefined }) => {
numberOfPackages = msg.pkgs?.length ?? 1; log.info(`installing ${pkg.project}@${pkg.version} - ${progress}`);
log.info(`installing ${numberOfPackages} packages`); if (progress && progress > 0) {
} else if (msg.status === "downloading") { // how many total packages are completed
counter++; const completedProgress = (currentPackageNumber / numberOfPackages) * 100;
if (counter % 10 !== 0) return; // overallProgress is the total packages completed plus the percentage of the current package
const overallProgress = completedProgress + (progress / numberOfPackages) * 100;
const { received = 0, "content-size": contentSize = 0 } = msg; notifyMainWindow("install-progress", { full_name, progress: overallProgress });
if (contentSize > 0) {
// how many total pacakges are completed
const overallProgress = (currentPackageNumber / numberOfPackages) * 100;
// how much of the current package is completed
const packageProgress = (received / contentSize) * 100;
// progress is the total packages completed plus the percentage of the current package
const progress = overallProgress + packageProgress / numberOfPackages;
notifyMainWindow("install-progress", { full_name, progress });
} }
} else if (msg.status === "installed") { },
installed: (installation: Installation) => {
log.info("installed", installation);
const { project, version } = installation.pkg;
currentPackageNumber++; currentPackageNumber++;
const progress = (currentPackageNumber / numberOfPackages) * 100; const progress = (currentPackageNumber / numberOfPackages) * 100;
notifyMainWindow("install-progress", { full_name, progress }); notifyMainWindow("install-progress", { full_name, progress });
notifyPackageInstalled(msg.pkg, notifyMainWindow); notifyMainWindow("pkg-installed", { full_name: project, version: version.toString() });
} }
}; };
} }
const notifyPackageInstalled = (rawPkg: string, notifyMainWindow: MainWindowNotifier) => { // the tea cli package is needed to open any other package in the terminal, so make sure it's installed and return the path
try { async function installTeaCli() {
const [full_name, version] = rawPkg.split("="); const installations = await porcelain.install("tea.xyz");
notifyMainWindow("pkg-installed", { full_name, version }); const teaPkg = installations.find((i) => i.pkg.project === "tea.xyz");
} catch (err) { if (!teaPkg) {
log.error("failed to notify package installed", err); throw new Error("could not find or install tea cli!");
} }
};
return teaPkg.path.join("bin/tea");
}
export async function openPackageEntrypointInTerminal(pkg: string) { export async function openPackageEntrypointInTerminal(pkg: string) {
const cliBinPath = await installTeaCli();
log.info(`opening package ${pkg} with tea cli at ${cliBinPath}`);
let sh = `"${cliBinPath}" --sync --env=false +${pkg} `; let sh = `"${cliBinPath}" --sync --env=false +${pkg} `;
switch (pkg) { switch (pkg) {
case "github.com/AUTOMATIC1111/stable-diffusion-webui": case "github.com/AUTOMATIC1111/stable-diffusion-webui":
@ -194,24 +131,8 @@ const createCommandScriptFile = async (cmd: string): Promise<string> => {
return tmpFilePath; return tmpFilePath;
}; };
export async function asyncExec(cmd: string): Promise<string> {
return new Promise((resolve, reject) => {
exec(cmd, (err, stdout) => {
if (err) {
console.log("err:", err);
reject(err);
return;
}
console.log("stdout:", stdout);
resolve(stdout);
});
});
}
export async function syncPantry() { export async function syncPantry() {
const teaVersion = await initializeTeaCli(); log.info("syncing pantry");
await hooks.useSync();
if (!teaVersion) throw new Error("no tea"); log.info("syncing pantry completed");
log.info("Syncing pantry", teaVersion);
await asyncExec(`DEBUG=1 "${cliBinPath}" --sync --env=false`);
} }

View file

@ -1,10 +1,4 @@
import fs from "fs";
import { getTeaPath } from "./tea-dir";
import { authFileState } from "./auth"; import { authFileState } from "./auth";
import * as https from "https";
import { spawn } from "child_process";
import path from "path";
import { semver } from "@teaxyz/lib";
type InitState = "NOT_INITIALIZED" | "PENDING" | "INITIALIZED"; type InitState = "NOT_INITIALIZED" | "PENDING" | "INITIALIZED";
@ -63,99 +57,6 @@ export class InitWatcher<T> {
return this.initState; return this.initState;
} }
} }
export default async function initialize() {
// Be careful with globbing when passing this to a shell which might expand it. Either escape it or quote it. await authFileState.observe();
const teaCliPrefix = path.join(getTeaPath(), "tea.xyz/v*");
export const cliInitializationState = new InitWatcher<string>(async () => {
if (!fs.existsSync(path.join(teaCliPrefix, "bin/tea"))) {
return installTeaCli();
} else {
const dir = fs.readlinkSync(teaCliPrefix);
const v = semver.parse(dir)?.toString();
if (!v) throw new Error(`couldn't parse to semver: ${dir}`);
return v;
}
});
cliInitializationState.initialize();
export async function initializeTeaCli(): Promise<string> {
if (
cliInitializationState.getState() === "INITIALIZED" &&
!fs.existsSync(path.join(teaCliPrefix, "bin/tea"))
) {
cliInitializationState.reset();
}
return cliInitializationState.observe();
}
//NOTE copy pasta from https://github.com/teaxyz/setup/blob/main/action.js
//FIXME ideally we'd not copy pasta this
//NOTE using `tar` is not ideal ∵ Windows and even though tar is POSIX it's still not guaranteed to be available
async function installTeaCli() {
const PREFIX = `${process.env.HOME}/.tea`;
const midfix = (() => {
switch (process.arch) {
case "arm64":
return `${process.platform}/aarch64`;
case "x64":
return `${process.platform}/x86-64`;
default:
throw new Error(`unsupported platform: ${process.platform}/${process.arch}`);
}
})();
/// versions.txt is guaranteed semver-sorted
const v: string | undefined = await new Promise((resolve, reject) => {
https
.get(`https://dist.tea.xyz/tea.xyz/${midfix}/versions.txt`, (rsp) => {
if (rsp.statusCode != 200) return reject(rsp.statusCode);
rsp.setEncoding("utf8");
const chunks: string[] = [];
rsp.on("data", (x) => chunks.push(x));
rsp.on("end", () => {
resolve(chunks.join("").trim().split("\n").at(-1));
});
})
.on("error", reject);
});
if (!v) throw new Error(`invalid versions.txt for tea/cli`);
fs.mkdirSync(PREFIX, { recursive: true });
const exitcode = await new Promise((resolve, reject) => {
https
.get(`https://dist.tea.xyz/tea.xyz/${midfix}/v${v}.tar.gz`, (rsp) => {
if (rsp.statusCode != 200) return reject(rsp.statusCode);
const tar = spawn("tar", ["xzf", "-"], {
stdio: ["pipe", "inherit", "inherit"],
cwd: PREFIX
});
rsp.pipe(tar.stdin);
tar.on("close", resolve);
})
.on("error", reject);
});
if (exitcode != 0) {
throw new Error(`tar: ${exitcode}`);
}
const oldwd = process.cwd();
process.chdir(`${PREFIX}/tea.xyz`);
if (fs.existsSync(`v*`)) fs.unlinkSync(`v*`);
fs.symlinkSync(`v${v}`, `v*`, "dir");
if (fs.existsSync(`v0`)) fs.unlinkSync(`v0`);
fs.symlinkSync(`v${v}`, `v0`, "dir"); //FIXME
process.chdir(oldwd);
return v;
}
export default async function initialize(): Promise<string> {
const [version] = await Promise.all([initializeTeaCli(), authFileState.observe()]);
return version;
} }

View file

@ -13,8 +13,6 @@ import log from "./logger";
import { syncLogsAt } from "./v1-client"; import { syncLogsAt } from "./v1-client";
import { installPackage, openPackageEntrypointInTerminal, syncPantry } from "./cli"; import { installPackage, openPackageEntrypointInTerminal, syncPantry } from "./cli";
import { initializeTeaCli, cliInitializationState } from "./initialize";
import { getAutoUpdateStatus, getUpdater, isDev } from "./auto-updater"; import { getAutoUpdateStatus, getUpdater, isDev } from "./auto-updater";
import { loadPackageCache, writePackageCache } from "./package"; import { loadPackageCache, writePackageCache } from "./package";
@ -59,8 +57,7 @@ export default function initializeHandlers({ notifyMainWindow }: HandlerOptions)
ipcMain.handle("get-session", async () => { ipcMain.handle("get-session", async () => {
try { try {
log.info("getting session"); log.info("getting session");
const [session, cliVersion] = await Promise.all([readSessionData(), initializeTeaCli()]); const session = await readSessionData();
session.teaVersion = cliVersion;
log.debug(session ? "found session data" : "no session data found"); log.debug(session ? "found session data" : "no session data found");
return session; return session;
} catch (error) { } catch (error) {
@ -168,10 +165,6 @@ export default function initializeHandlers({ notifyMainWindow }: HandlerOptions)
} }
} catch (e) { } catch (e) {
log.error(e); log.error(e);
} finally {
if (fullName === "tea.xyz") {
cliInitializationState.reset();
}
} }
} }
); );
@ -193,21 +186,6 @@ export default function initializeHandlers({ notifyMainWindow }: HandlerOptions)
} }
}); });
ipcMain.handle("get-tea-version", async () => {
try {
log.info("installing tea cli");
const version = await initializeTeaCli();
if (!version) {
throw new Error("failed to install tea cli");
}
return { version, message: "" };
} catch (error) {
log.error(error);
return { version: "", message: error.message };
}
});
ipcMain.handle("topbar-double-click", async (event: Electron.IpcMainInvokeEvent) => { ipcMain.handle("topbar-double-click", async (event: Electron.IpcMainInvokeEvent) => {
const mainWindow = BrowserWindow.fromWebContents(event.sender); const mainWindow = BrowserWindow.fromWebContents(event.sender);
if (mainWindow) { if (mainWindow) {

View file

@ -88,7 +88,7 @@
"@sentry/electron": "^4.4.0", "@sentry/electron": "^4.4.0",
"@sentry/svelte": "^7.47.0", "@sentry/svelte": "^7.47.0",
"@sentry/vite-plugin": "^0.7.2", "@sentry/vite-plugin": "^0.7.2",
"@teaxyz/lib": "^0.2.2", "@teaxyz/lib": "^0.3.0",
"@types/electron": "^1.6.10", "@types/electron": "^1.6.10",
"@types/mousetrap": "^1.6.11", "@types/mousetrap": "^1.6.11",
"@vitest/coverage-c8": "^0.27.1", "@vitest/coverage-c8": "^0.27.1",

View file

@ -30,7 +30,7 @@ importers:
'@sveltejs/adapter-static': ^1.0.0-next.48 '@sveltejs/adapter-static': ^1.0.0-next.48
'@sveltejs/kit': ^1.15.9 '@sveltejs/kit': ^1.15.9
'@tea/ui': workspace:* '@tea/ui': workspace:*
'@teaxyz/lib': ^0.2.2 '@teaxyz/lib': ^0.3.0
'@testing-library/jest-dom': ^5.16.5 '@testing-library/jest-dom': ^5.16.5
'@testing-library/svelte': ^3.2.2 '@testing-library/svelte': ^3.2.2
'@testing-library/webdriverio': ^3.2.1 '@testing-library/webdriverio': ^3.2.1
@ -110,7 +110,7 @@ importers:
'@sentry/electron': 4.5.0 '@sentry/electron': 4.5.0
'@sentry/svelte': 7.51.2_svelte@3.59.1 '@sentry/svelte': 7.51.2_svelte@3.59.1
'@sentry/vite-plugin': 0.7.2 '@sentry/vite-plugin': 0.7.2
'@teaxyz/lib': 0.2.2 '@teaxyz/lib': 0.3.0
'@types/electron': 1.6.10 '@types/electron': 1.6.10
'@types/mousetrap': 1.6.11 '@types/mousetrap': 1.6.11
'@vitest/coverage-c8': 0.27.3_jsdom@21.1.2 '@vitest/coverage-c8': 0.27.3_jsdom@21.1.2
@ -3637,8 +3637,8 @@ packages:
tailwindcss: 3.3.2 tailwindcss: 3.3.2
dev: false dev: false
/@teaxyz/lib/0.2.2: /@teaxyz/lib/0.3.0:
resolution: {integrity: sha512-xChLHuuwbUXZcHMmqMsGIB6RbXqEb1hXgRYJk4Zg3KfozhRllXuiZ6VkUZYPib66ygMjPlndclG7tj2cDlsLkg==} resolution: {integrity: sha512-NFoVdSE4iX5JBdiXXOo0aGk4fsTb5zjBOqkY9ldA8asa8pITSJjPOxoa34H7gkkxuTcjv/zjeiOREveWdibcdA==}
dependencies: dependencies:
'@deno/shim-crypto': 0.3.1 '@deno/shim-crypto': 0.3.1
'@deno/shim-deno': 0.16.1 '@deno/shim-deno': 0.16.1