real progress bar (#490)

This commit is contained in:
ABevier 2023-04-23 21:15:51 -04:00 committed by GitHub
parent 518a2d1f70
commit 8d0378656a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 153 additions and 32 deletions

View file

@ -46,7 +46,7 @@ let macWindowClosed = false;
setupTitlebar();
initializeHandlers();
initializeHandlers({ notifyMainWindow });
function createWindow() {
const windowState = windowStateManager({
@ -193,3 +193,9 @@ app.on("open-url", (event, url) => {
createMainWindow();
}
});
function notifyMainWindow(channel: string, data: unknown) {
if (mainWindow) {
mainWindow.webContents.send(channel, data);
}
}

View file

@ -4,18 +4,111 @@ import fs from "fs";
import path from "path";
import initializeTeaCli from "./initialize";
import { app } from "electron";
import * as log from "electron-log";
import { MainWindowNotifier } from "./types";
const destinationDirectory = getGuiPath();
export const cliBinPath = path.join(destinationDirectory, "tea");
export async function installPackage(full_name: string) {
export async function installPackage(
full_name: string,
version: string,
notifyMainWindow: MainWindowNotifier
) {
const teaVersion = await initializeTeaCli();
const progressNotifier = newInstallProgressNotifier(full_name, notifyMainWindow);
if (!teaVersion) throw new Error("no tea");
log.info(`installing package ${full_name}`);
await asyncExec(`cd ${destinationDirectory} && ./tea --env=false --sync +${full_name} true`);
const qualifedPackage = `${full_name}@${version}`;
log.info(`installing package ${qualifedPackage}`);
let stdout = "";
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(
`${destinationDirectory}/tea`,
["--env=false", "--sync", "--json", `+${qualifedPackage}`],
opts
);
child.stdout.on("data", (data) => {
stdout += data.toString().trim();
});
child.stderr.on("data", (data) => {
try {
const msg = JSON.parse(data.toString().trim());
progressNotifier(msg);
} catch (err) {
//swallow it
}
stderr += data.toString().trim();
});
child.on("exit", (code) => {
console.log("stdout:", stdout);
if (code !== 0) {
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) {
// 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
let numberOfPackages = 1;
// the current package number - this is incremented by the "installed" or "downloaded" message
let currentPackageNumber = 0;
return function (msg: any) {
if (msg.status !== "downloading") {
log.info("cli:", msg);
}
if (msg.status === "resolved") {
numberOfPackages = msg.pkgs?.length ?? 1;
log.info(`installing ${numberOfPackages} packages`);
} else if (msg.status === "downloading") {
counter++;
if (counter % 10 !== 0) return;
const { received = 0, "content-size": contentSize = 0 } = msg;
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).toFixed(2);
notifyMainWindow("install-progress", { full_name, progress });
}
} else if (msg.status === "installed") {
currentPackageNumber++;
const progress = ((currentPackageNumber / numberOfPackages) * 100).toFixed(2);
notifyMainWindow("install-progress", { full_name, progress });
}
};
}
export async function openTerminal(cmd: string) {

View file

@ -4,6 +4,10 @@ import * as log from "electron-log";
import semver from "semver";
import { cliBinPath, asyncExec } from "./cli";
import { createInitialSessionFile } from "./auth";
import semverCompare from "semver/functions/compare";
// Versions before this do not support the --json flag
const MINIMUM_TEA_VERSION = "0.28.3";
const destinationDirectory = getGuiPath();
@ -14,8 +18,9 @@ const binaryUrl = "https://tea.xyz/$(uname)/$(uname -m)";
export async function initializeTeaCli(): Promise<string> {
try {
let version = "";
let binCheck = "";
let needsUpdate = false;
// Create the destination directory if it doesn't exist
if (!fs.existsSync(destinationDirectory)) {
fs.mkdirSync(destinationDirectory, { recursive: true });
@ -23,10 +28,19 @@ export async function initializeTeaCli(): Promise<string> {
const curlCommand = `curl -L -o "${cliBinPath}" "${binaryUrl}"`;
if (fs.existsSync(cliBinPath)) {
const exists = fs.existsSync(cliBinPath);
if (exists) {
log.info("binary tea already exists at", cliBinPath);
binCheck = await asyncExec(`cd ${destinationDirectory} && ./tea --version`);
} else {
const teaVersion = binCheck.toString().split(" ")[1];
if (semverCompare(teaVersion, MINIMUM_TEA_VERSION) < 0) {
log.info("binary tea version is too old, updating");
needsUpdate = true;
}
}
if (!exists || needsUpdate) {
try {
await asyncExec(curlCommand);
log.info("Binary downloaded and saved to", cliBinPath);
@ -37,7 +51,8 @@ export async function initializeTeaCli(): Promise<string> {
log.error("Error setting-up tea binary:", error);
}
}
version = binCheck.toString().split(" ")[1];
const version = binCheck.toString().split(" ")[1];
log.info("binary tea version:", version);
return semver.valid(version.trim()) ? version : "";
} catch (error) {

View file

@ -12,13 +12,19 @@ import { getAutoUpdateStatus, getUpdater } from "./auto-updater";
import { loadPackageCache, writePackageCache } from "./package";
import { nanoid } from "nanoid";
let teaProtocolPath = ""; // this should be empty string
import { MainWindowNotifier } from "./types";
export type HandlerOptions = {
// A function to call back to the current main
notifyMainWindow: MainWindowNotifier;
};
let teaProtocolPath = ""; // this should be empty string
export const setProtocolPath = (path: string) => {
teaProtocolPath = path;
};
export default function initializeHandlers() {
export default function initializeHandlers({ notifyMainWindow }: HandlerOptions) {
ipcMain.handle("get-installed-packages", async () => {
try {
log.info("getting installed packages");
@ -52,11 +58,9 @@ export default function initializeHandlers() {
}
});
ipcMain.handle("install-package", async (_, data) => {
ipcMain.handle("install-package", async (_, { full_name, version }) => {
try {
log.info("installing package:", data.full_name);
const result = await installPackage(data.full_name);
return result;
return await installPackage(full_name, version, notifyMainWindow);
} catch (error) {
log.error(error);
return error;
@ -123,7 +127,6 @@ export default function initializeHandlers() {
});
ipcMain.handle("set-badge-count", async (_, { count }) => {
console.log("set badge count:", count);
if (count) {
app.dock.setBadge(count.toString());
} else {

View file

@ -0,0 +1 @@
export type MainWindowNotifier = (channel: string, data: unknown) => void;

View file

@ -23,7 +23,6 @@ import {
import * as mock from "./native-mock";
import { PackageStates, type InstalledPackage } from "./types";
import { installPackageCommand } from "./native/cli";
import { get as apiGet } from "$libs/v1-client";
import axios from "axios";
@ -94,8 +93,16 @@ export async function getPackageReviews(full_name: string): Promise<Review[]> {
export async function installPackage(pkg: GUIPackage, version?: string) {
const latestVersion = pkg.version;
const specificVersion = version || latestVersion;
log.info(`installing package: ${pkg.name} version: ${specificVersion}`);
await installPackageCommand(pkg.full_name + (specificVersion ? `@${specificVersion}` : ""));
const res = await ipcRenderer.invoke("install-package", {
full_name: pkg.full_name,
version: specificVersion
});
if (res instanceof Error) {
throw res;
}
}
export async function syncPantry() {

View file

@ -1,8 +0,0 @@
const { ipcRenderer } = window.require("electron");
export async function installPackageCommand(full_name: string) {
const res = await ipcRenderer.invoke("install-package", { full_name });
if (res instanceof Error) {
throw res;
}
}

View file

@ -13,7 +13,8 @@ import {
loadPackageCache,
writePackageCache,
syncPantry,
cacheImageURL
cacheImageURL,
listenToChannel
} from "@native";
import { getReadme, getContributors, getRepoAsPackage } from "$libs/github";
@ -166,7 +167,6 @@ To read more about this package go to [${guiPkg.homepage}](${guiPkg.homepage}).
};
const installPkg = async (pkg: GUIPackage, version?: string) => {
let fakeTimer: NodeJS.Timer | null = null;
const originalState = pkg.state;
const versionToInstall = version || pkg.version;
@ -178,10 +178,6 @@ To read more about this package go to [${guiPkg.homepage}](${guiPkg.homepage}).
updatePackage(pkg.full_name, { state });
fakeTimer = withFakeLoader(pkg, (progress) => {
updatePackage(pkg.full_name, { install_progress_percentage: progress });
});
await installPackage(pkg, versionToInstall);
trackInstall(pkg.full_name);
@ -206,7 +202,6 @@ To read more about this package go to [${guiPkg.homepage}](${guiPkg.homepage}).
type: NotificationType.ERROR
});
} finally {
fakeTimer && clearTimeout(fakeTimer);
updatePackage(pkg.full_name, { install_progress_percentage: 100 });
}
};
@ -268,6 +263,14 @@ To read more about this package go to [${guiPkg.homepage}](${guiPkg.homepage}).
}
};
listenToChannel("install-progress", (data: any) => {
const { full_name, progress } = data;
if (!full_name) {
return;
}
updatePackage(full_name, { install_progress_percentage: progress });
});
return {
packageList,
syncProgress,
@ -290,6 +293,7 @@ To read more about this package go to [${guiPkg.homepage}](${guiPkg.homepage}).
};
}
// This is only used for uninstall now
export const withFakeLoader = (
pkg: GUIPackage,
callback: (progress: number) => void