Add update install progress feedback and collector metrics line

This commit is contained in:
Sucukdeluxe
2026-03-01 03:33:18 +01:00
parent cb1e4bb0c1
commit 508977e70b
10 changed files with 317 additions and 37 deletions
+3 -2
View File
@@ -9,6 +9,7 @@ import {
StartConflictResolutionResult,
UiSnapshot,
UpdateCheckResult,
UpdateInstallProgress,
UpdateInstallResult
} from "../shared/types";
import { importDlcContainers } from "./container";
@@ -139,12 +140,12 @@ export class AppController {
return result;
}
public async installUpdate(): Promise<UpdateInstallResult> {
public async installUpdate(onProgress?: (progress: UpdateInstallProgress) => void): Promise<UpdateInstallResult> {
const cacheAgeMs = Date.now() - this.lastUpdateCheckAt;
const cached = this.lastUpdateCheck && !this.lastUpdateCheck.error && cacheAgeMs <= 10 * 60 * 1000
? this.lastUpdateCheck
: undefined;
const result = await installLatestUpdate(this.settings.updateRepo, cached);
const result = await installLatestUpdate(this.settings.updateRepo, cached, onProgress);
if (result.started) {
this.lastUpdateCheck = null;
this.lastUpdateCheckAt = 0;
+7 -2
View File
@@ -1,6 +1,6 @@
import path from "node:path";
import { app, BrowserWindow, clipboard, dialog, ipcMain, IpcMainInvokeEvent, Menu, shell, Tray } from "electron";
import { AddLinksPayload, AppSettings } from "../shared/types";
import { AddLinksPayload, AppSettings, UpdateInstallProgress } from "../shared/types";
import { AppController } from "./app-controller";
import { IPC_CHANNELS } from "../shared/ipc";
import { logger } from "./logger";
@@ -224,7 +224,12 @@ function registerIpcHandlers(): void {
ipcMain.handle(IPC_CHANNELS.GET_VERSION, () => controller.getVersion());
ipcMain.handle(IPC_CHANNELS.CHECK_UPDATES, async () => controller.checkUpdates());
ipcMain.handle(IPC_CHANNELS.INSTALL_UPDATE, async () => {
const result = await controller.installUpdate();
const result = await controller.installUpdate((progress: UpdateInstallProgress) => {
if (!mainWindow || mainWindow.isDestroyed()) {
return;
}
mainWindow.webContents.send(IPC_CHANNELS.UPDATE_INSTALL_PROGRESS, progress);
});
if (result.started) {
setTimeout(() => {
app.quit();
+123 -11
View File
@@ -7,8 +7,8 @@ import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
import { ReadableStream as NodeReadableStream } from "node:stream/web";
import { APP_VERSION, DEFAULT_UPDATE_REPO } from "./constants";
import { UpdateCheckResult, UpdateInstallResult } from "../shared/types";
import { compactErrorText } from "./utils";
import { UpdateCheckResult, UpdateInstallProgress, UpdateInstallResult } from "../shared/types";
import { compactErrorText, humanSize } from "./utils";
import { logger } from "./logger";
const RELEASE_FETCH_TIMEOUT_MS = 12000;
@@ -28,6 +28,19 @@ type ReleaseAsset = {
digest: string;
};
type UpdateProgressCallback = (progress: UpdateInstallProgress) => void;
function safeEmitProgress(onProgress: UpdateProgressCallback | undefined, progress: UpdateInstallProgress): void {
if (!onProgress) {
return;
}
try {
onProgress(progress);
} catch {
// ignore renderer callback errors
}
}
export function normalizeUpdateRepo(repo: string): string {
const raw = String(repo || "").trim();
if (!raw) {
@@ -401,7 +414,7 @@ export async function checkGitHubUpdate(repo: string): Promise<UpdateCheckResult
}
}
async function downloadFile(url: string, targetPath: string): Promise<void> {
async function downloadFile(url: string, targetPath: string, onProgress?: UpdateProgressCallback): Promise<void> {
const shutdownSignal = activeUpdateAbortController?.signal;
if (shutdownSignal?.aborted) {
throw new Error("aborted:update_shutdown");
@@ -424,6 +437,34 @@ async function downloadFile(url: string, targetPath: string): Promise<void> {
throw new Error(`Update Download fehlgeschlagen (HTTP ${response.status})`);
}
const totalBytesRaw = Number(response.headers.get("content-length") || NaN);
const totalBytes = Number.isFinite(totalBytesRaw) && totalBytesRaw > 0
? Math.max(0, Math.floor(totalBytesRaw))
: null;
let downloadedBytes = 0;
let lastProgressAt = 0;
const emitDownloadProgress = (force: boolean): void => {
const now = Date.now();
if (!force && now - lastProgressAt < 160) {
return;
}
lastProgressAt = now;
const percent = totalBytes && totalBytes > 0
? Math.max(0, Math.min(100, Math.floor((downloadedBytes / totalBytes) * 100)))
: null;
const message = totalBytes && percent !== null
? `Update wird heruntergeladen: ${percent}% (${humanSize(downloadedBytes)} / ${humanSize(totalBytes)})`
: `Update wird heruntergeladen (${humanSize(downloadedBytes)})`;
safeEmitProgress(onProgress, {
stage: "downloading",
percent,
downloadedBytes,
totalBytes,
message
});
};
emitDownloadProgress(true);
await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });
const source = Readable.fromWeb(response.body as unknown as NodeReadableStream<Uint8Array>);
const target = fs.createWriteStream(targetPath);
@@ -448,8 +489,10 @@ async function downloadFile(url: string, targetPath: string): Promise<void> {
idleTimer = setTimeout(onIdleTimeout, idleTimeoutMs);
};
const onSourceData = (): void => {
const onSourceData = (chunk: string | Buffer): void => {
downloadedBytes += typeof chunk === "string" ? Buffer.byteLength(chunk) : chunk.byteLength;
resetIdleTimer();
emitDownloadProgress(false);
};
const onSourceDone = (): void => {
clearIdleTimer();
@@ -488,6 +531,7 @@ async function downloadFile(url: string, targetPath: string): Promise<void> {
target.off("close", onSourceDone);
target.off("error", onSourceDone);
}
emitDownloadProgress(true);
logger.info(`Update-Download abgeschlossen: ${targetPath}`);
}
@@ -516,7 +560,7 @@ async function sleep(ms: number, signal?: AbortSignal): Promise<void> {
});
}
async function downloadWithRetries(url: string, targetPath: string): Promise<void> {
async function downloadWithRetries(url: string, targetPath: string, onProgress?: UpdateProgressCallback): Promise<void> {
const shutdownSignal = activeUpdateAbortController?.signal;
let lastError: unknown;
for (let attempt = 1; attempt <= RETRIES_PER_CANDIDATE; attempt += 1) {
@@ -524,7 +568,7 @@ async function downloadWithRetries(url: string, targetPath: string): Promise<voi
throw new Error("aborted:update_shutdown");
}
try {
await downloadFile(url, targetPath);
await downloadFile(url, targetPath, onProgress);
return;
} catch (error) {
lastError = error;
@@ -544,7 +588,7 @@ async function downloadWithRetries(url: string, targetPath: string): Promise<voi
throw lastError;
}
async function downloadFromCandidates(candidates: string[], targetPath: string): Promise<void> {
async function downloadFromCandidates(candidates: string[], targetPath: string, onProgress?: UpdateProgressCallback): Promise<void> {
const shutdownSignal = activeUpdateAbortController?.signal;
let lastError: unknown = new Error("Update Download fehlgeschlagen");
@@ -554,8 +598,15 @@ async function downloadFromCandidates(candidates: string[], targetPath: string):
throw new Error("aborted:update_shutdown");
}
const candidate = candidates[index];
safeEmitProgress(onProgress, {
stage: "downloading",
percent: null,
downloadedBytes: 0,
totalBytes: null,
message: `Update-Download: Quelle ${index + 1}/${candidates.length}`
});
try {
await downloadWithRetries(candidate, targetPath);
await downloadWithRetries(candidate, targetPath, onProgress);
return;
} catch (error) {
lastError = error;
@@ -570,8 +621,19 @@ async function downloadFromCandidates(candidates: string[], targetPath: string):
throw lastError;
}
export async function installLatestUpdate(repo: string, prechecked?: UpdateCheckResult): Promise<UpdateInstallResult> {
export async function installLatestUpdate(
repo: string,
prechecked?: UpdateCheckResult,
onProgress?: UpdateProgressCallback
): Promise<UpdateInstallResult> {
if (activeUpdateAbortController && !activeUpdateAbortController.signal.aborted) {
safeEmitProgress(onProgress, {
stage: "error",
percent: null,
downloadedBytes: 0,
totalBytes: null,
message: "Update-Download läuft bereits"
});
return { started: false, message: "Update-Download läuft bereits" };
}
const updateAbortController = new AbortController();
@@ -583,9 +645,23 @@ export async function installLatestUpdate(repo: string, prechecked?: UpdateCheck
: await checkGitHubUpdate(safeRepo);
if (check.error) {
safeEmitProgress(onProgress, {
stage: "error",
percent: null,
downloadedBytes: 0,
totalBytes: null,
message: check.error
});
return { started: false, message: check.error };
}
if (!check.updateAvailable) {
safeEmitProgress(onProgress, {
stage: "error",
percent: null,
downloadedBytes: 0,
totalBytes: null,
message: "Kein neues Update verfügbar"
});
return { started: false, message: "Kein neues Update verfügbar" };
}
@@ -617,14 +693,35 @@ export async function installLatestUpdate(repo: string, prechecked?: UpdateCheck
const targetPath = path.join(os.tmpdir(), "rd-update", `${Date.now()}-${process.pid}-${crypto.randomUUID()}-${fileName}`);
try {
safeEmitProgress(onProgress, {
stage: "starting",
percent: 0,
downloadedBytes: 0,
totalBytes: null,
message: "Update wird vorbereitet"
});
if (updateAbortController.signal.aborted) {
throw new Error("aborted:update_shutdown");
}
await downloadFromCandidates(candidates, targetPath);
await downloadFromCandidates(candidates, targetPath, onProgress);
if (updateAbortController.signal.aborted) {
throw new Error("aborted:update_shutdown");
}
safeEmitProgress(onProgress, {
stage: "verifying",
percent: 100,
downloadedBytes: 0,
totalBytes: null,
message: "Prüfe Installer-Integrität"
});
await verifyDownloadedInstaller(targetPath, String(effectiveCheck.setupAssetDigest || ""));
safeEmitProgress(onProgress, {
stage: "launching",
percent: 100,
downloadedBytes: 0,
totalBytes: null,
message: "Starte Update-Installer"
});
const child = spawn(targetPath, [], {
detached: true,
stdio: "ignore"
@@ -633,6 +730,13 @@ export async function installLatestUpdate(repo: string, prechecked?: UpdateCheck
logger.error(`Update-Installer Start fehlgeschlagen: ${compactErrorText(spawnError)}`);
});
child.unref();
safeEmitProgress(onProgress, {
stage: "done",
percent: 100,
downloadedBytes: 0,
totalBytes: null,
message: "Update-Installer gestartet"
});
return { started: true, message: "Update-Installer gestartet" };
} catch (error) {
try {
@@ -642,7 +746,15 @@ export async function installLatestUpdate(repo: string, prechecked?: UpdateCheck
}
const releaseUrl = String(effectiveCheck.releaseUrl || "").trim();
const hint = releaseUrl ? ` Manuell: ${releaseUrl}` : "";
return { started: false, message: `${compactErrorText(error)}${hint}` };
const message = `${compactErrorText(error)}${hint}`;
safeEmitProgress(onProgress, {
stage: "error",
percent: null,
downloadedBytes: 0,
totalBytes: null,
message
});
return { started: false, message };
} finally {
if (activeUpdateAbortController === updateAbortController) {
activeUpdateAbortController = null;