Harden resume flows and ship v1.1.23 stability fixes
Build and Release / build (push) Has been cancelled

This commit is contained in:
Sucukdeluxe
2026-02-27 11:04:52 +01:00
parent 583d74fcc9
commit 6d777e2a56
15 changed files with 1790 additions and 236 deletions
+89 -36
View File
@@ -2,11 +2,60 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { spawn } from "node:child_process";
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";
type ReleaseAsset = { name: string; browser_download_url: string };
export function normalizeUpdateRepo(repo: string): string {
const raw = String(repo || "").trim();
if (!raw) {
return DEFAULT_UPDATE_REPO;
}
const normalizeParts = (input: string): string => {
const cleaned = input
.replace(/^https?:\/\/(?:www\.)?github\.com\//i, "")
.replace(/^(?:www\.)?github\.com\//i, "")
.replace(/^git@github\.com:/i, "")
.replace(/\.git$/i, "")
.replace(/^\/+|\/+$/g, "");
const parts = cleaned.split("/").filter(Boolean);
if (parts.length >= 2) {
return `${parts[0]}/${parts[1]}`;
}
return "";
};
try {
const url = new URL(raw);
const host = url.hostname.toLowerCase();
if (host === "github.com" || host === "www.github.com") {
const normalized = normalizeParts(url.pathname);
if (normalized) {
return normalized;
}
}
} catch {
// plain owner/repo value
}
const normalized = normalizeParts(raw);
return normalized || DEFAULT_UPDATE_REPO;
}
function timeoutController(ms: number): { signal: AbortSignal; clear: () => void } {
const controller = new AbortController();
const timer = setTimeout(() => {
controller.abort(new Error(`timeout:${ms}`));
}, ms);
return {
signal: controller.signal,
clear: () => clearTimeout(timer)
};
}
function parseVersionParts(version: string): number[] {
const cleaned = version.replace(/^v/i, "").trim();
@@ -31,7 +80,7 @@ function isRemoteNewer(currentVersion: string, latestVersion: string): boolean {
}
export async function checkGitHubUpdate(repo: string): Promise<UpdateCheckResult> {
const safeRepo = (repo || DEFAULT_UPDATE_REPO).trim() || DEFAULT_UPDATE_REPO;
const safeRepo = normalizeUpdateRepo(repo);
const fallback: UpdateCheckResult = {
updateAvailable: false,
currentVersion: APP_VERSION,
@@ -41,12 +90,19 @@ export async function checkGitHubUpdate(repo: string): Promise<UpdateCheckResult
};
try {
const response = await fetch(`https://api.github.com/repos/${safeRepo}/releases/latest`, {
headers: {
Accept: "application/vnd.github+json",
"User-Agent": "RD-Node-Downloader/1.1.14"
}
});
const timeout = timeoutController(15000);
let response: Response;
try {
response = await fetch(`https://api.github.com/repos/${safeRepo}/releases/latest`, {
headers: {
Accept: "application/vnd.github+json",
"User-Agent": "RD-Node-Downloader/1.1.14"
},
signal: timeout.signal
});
} finally {
timeout.clear();
}
const payload = await response.json().catch(() => null) as Record<string, unknown> | null;
if (!response.ok || !payload) {
const reason = String((payload?.message as string) || `HTTP ${response.status}`);
@@ -57,12 +113,14 @@ export async function checkGitHubUpdate(repo: string): Promise<UpdateCheckResult
const latestVersion = latestTag.replace(/^v/i, "") || APP_VERSION;
const releaseUrl = String(payload.html_url || fallback.releaseUrl);
const assets = Array.isArray(payload.assets) ? payload.assets as Array<Record<string, unknown>> : [];
const setup = assets
const exeAssets = assets
.map((asset) => ({
name: String(asset.name || ""),
browser_download_url: String(asset.browser_download_url || "")
}))
.find((asset) => /\.setup\..*\.exe$/i.test(asset.name));
.filter((asset) => asset.browser_download_url && /\.exe$/i.test(asset.name));
const setup = exeAssets.find((asset) => /setup/i.test(asset.name))
|| exeAssets.find((asset) => !/portable/i.test(asset.name));
return {
updateAvailable: isRemoteNewer(APP_VERSION, latestVersion),
@@ -81,36 +139,26 @@ export async function checkGitHubUpdate(repo: string): Promise<UpdateCheckResult
}
async function downloadFile(url: string, targetPath: string): Promise<void> {
const response = await fetch(url, {
headers: {
"User-Agent": "RD-Node-Downloader/1.1.18"
}
});
const timeout = timeoutController(10 * 60 * 1000);
let response: Response;
try {
response = await fetch(url, {
headers: {
"User-Agent": "RD-Node-Downloader/1.1.18"
},
signal: timeout.signal
});
} finally {
timeout.clear();
}
if (!response.ok || !response.body) {
throw new Error(`Update Download fehlgeschlagen (HTTP ${response.status})`);
}
await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });
const stream = fs.createWriteStream(targetPath);
await new Promise<void>((resolve, reject) => {
const reader = response.body!.getReader();
const pump = (): void => {
void reader.read().then(({ done, value }) => {
if (done) {
stream.end(() => resolve());
return;
}
if (value) {
stream.write(Buffer.from(value));
}
pump();
}).catch((error) => {
stream.destroy();
reject(error);
});
};
pump();
});
const source = Readable.fromWeb(response.body as unknown as NodeReadableStream<Uint8Array>);
const target = fs.createWriteStream(targetPath);
await pipeline(source, target);
}
export async function installLatestUpdate(repo: string): Promise<UpdateInstallResult> {
@@ -127,7 +175,7 @@ export async function installLatestUpdate(repo: string): Promise<UpdateInstallRe
}
const fileName = path.basename(new URL(downloadUrl).pathname || "update.exe") || "update.exe";
const targetPath = path.join(os.tmpdir(), "rd-update", fileName);
const targetPath = path.join(os.tmpdir(), "rd-update", `${Date.now()}-${fileName}`);
try {
await downloadFile(downloadUrl, targetPath);
const child = spawn(targetPath, [], {
@@ -137,6 +185,11 @@ export async function installLatestUpdate(repo: string): Promise<UpdateInstallRe
child.unref();
return { started: true, message: "Update-Installer gestartet" };
} catch (error) {
try {
await fs.promises.rm(targetPath, { force: true });
} catch {
// ignore
}
return { started: false, message: compactErrorText(error) };
}
}