Files
2026-06-19 18:45:55 +08:00

206 lines
7.7 KiB
TypeScript

import { autoUpdater, UpdateInfo } from "electron-updater";
import { BrowserWindow } from "electron";
import { fetchElectronSystemConfig } from "../systemConfig";
import path from "node:path";
import fs from "node:fs";
import Log from "../log/main";
import { checkResourceBundles, setResourceStatusListener } from "../resourceManager";
import { ProcessCleanupManager } from "../../lib/process-cleanup";
import { isDevelopment } from "../../lib/resource-path";
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = false;
autoUpdater.disableDifferentialDownload = false;
(autoUpdater as any).logger = {
info: (...args: any[]) => Log.info("[UpdaterCore]", args),
warn: (...args: any[]) => Log.warn("[UpdaterCore]", args),
error: (...args: any[]) => Log.error("[UpdaterCore]", args),
debug: (...args: any[]) => Log.debug("[UpdaterCore]", args),
};
let updaterWindow: BrowserWindow | null = null;
let downloadedSetupPath: string | null = null;
let latestUpdaterStatus: any = null;
export function setUpdaterWindow(win: BrowserWindow) {
updaterWindow = win;
}
function sendToRenderer(channel: string, ...args: any[]) {
if (channel === "updater:status" && args.length > 0) {
latestUpdaterStatus = args[0];
}
if (updaterWindow && !updaterWindow.isDestroyed()) {
updaterWindow.webContents.send(channel, ...args);
}
}
const getLatestStatus = () => latestUpdaterStatus;
setResourceStatusListener((data) => {
sendToRenderer("updater:status", data);
});
async function ensureUpdateFeedURL() {
try {
const config = await fetchElectronSystemConfig();
if (config.update_url) {
autoUpdater.setFeedURL({ provider: "generic", url: config.update_url });
Log.info("[Updater] feedURL set:", config.update_url);
}
} catch (e: any) {
Log.error("[Updater] setFeedURL failed:", e.message);
}
}
autoUpdater.on("checking-for-update", () => {
Log.info("[Updater] checking for update...");
sendToRenderer("updater:status", { status: "checking" });
});
autoUpdater.on("update-available", (info: UpdateInfo) => {
Log.info("[Updater] update available:", {
version: info.version,
files: info.files?.map(file => file.url) || [],
});
sendToRenderer("updater:status", {
status: "available",
version: info.version,
releaseNotes: info.releaseNotes,
fileSize: info.files?.[0]?.size || 0,
});
});
autoUpdater.on("update-not-available", () => {
Log.info("[Updater] no update available");
sendToRenderer("updater:status", { status: "not-available" });
});
autoUpdater.on("download-progress", (progress) => {
sendToRenderer("updater:status", {
status: "downloading",
percent: Math.round(progress.percent),
bytesPerSecond: progress.bytesPerSecond,
transferred: progress.transferred,
total: progress.total,
});
});
autoUpdater.on("update-downloaded", () => {
try {
const helper = (autoUpdater as any).downloadedUpdateHelper;
if (helper?.cacheDir) {
const pendingDir = helper.cacheDirForPendingUpdate || path.join(helper.cacheDir, "pending");
const pendingInstallerPath = (() => {
try {
if (!fs.existsSync(pendingDir)) {
return null;
}
const pendingFiles = fs.readdirSync(pendingDir).filter((f: string) => f.endsWith(".exe")).sort();
if (pendingFiles.length > 0) {
return path.join(pendingDir, pendingFiles[pendingFiles.length - 1]);
}
} catch (error: any) {
Log.warn("[Updater] read pending installer failed:", error?.message || error);
}
return null;
})();
if (pendingInstallerPath && fs.existsSync(pendingInstallerPath)) {
downloadedSetupPath = pendingInstallerPath;
} else {
const files = fs.readdirSync(helper.cacheDir).filter((f: string) => f.endsWith(".exe")).sort();
if (files.length > 0) {
downloadedSetupPath = path.join(helper.cacheDir, files[files.length - 1]);
}
}
// Keep a stable old installer cache so the next update can reuse it
// for differential download instead of falling back to a full package.
if (downloadedSetupPath && fs.existsSync(downloadedSetupPath)) {
const canonicalInstallerPath = path.join(helper.cacheDir, "installer.exe");
if (path.resolve(downloadedSetupPath) !== path.resolve(canonicalInstallerPath)) {
fs.copyFileSync(downloadedSetupPath, canonicalInstallerPath);
Log.info("[Updater] cached canonical installer for future differential download:", canonicalInstallerPath);
}
}
}
Log.info("[Updater] downloaded, setup path:", downloadedSetupPath);
} catch (e: any) {
Log.error("[Updater] find downloaded exe failed:", e.message);
}
sendToRenderer("updater:status", { status: "downloaded" });
});
autoUpdater.on("error", (error) => {
Log.error("[Updater] error:", error?.message || error);
sendToRenderer("updater:status", { status: "error", message: error?.message || "Unknown error" });
});
const checkForUpdate = async () => {
try {
if (!isDevelopment()) {
await ensureUpdateFeedURL();
sendToRenderer("updater:status", { status: "resource-checking" });
const resourceCheck = await checkResourceBundles({ installMissing: false });
sendToRenderer("updater:status", {
status: "resource-check-complete",
pending: resourceCheck.pending,
missingRequired: resourceCheck.missingRequired,
installed: resourceCheck.installed,
});
await autoUpdater.checkForUpdates();
}
return { code: 0, msg: "success" };
} catch (e: any) {
Log.error("[Updater] checkForUpdate failed:", e.message);
return { code: -1, msg: `Failed to check update: ${e.message}` };
}
};
const downloadUpdate = async () => {
try {
const result = await autoUpdater.downloadUpdate();
Log.info("[Updater] download result:", JSON.stringify(result));
return { code: 0, msg: "success" };
} catch (e: any) {
Log.error("[Updater] downloadUpdate failed:", e.message);
return { code: -1, msg: `Failed to download update: ${e.message}` };
}
};
const quitAndInstall = async () => {
try {
Log.info("[Updater] Starting quitAndInstall process...");
(global as any).__isUpdating = true;
Log.info("[Updater] Set isUpdating flag");
Log.info("[Updater] Cleaning up child processes (attempt 1)...");
await ProcessCleanupManager.cleanupAllProcesses();
Log.info("[Updater] Cleaning up child processes (attempt 2)...");
await ProcessCleanupManager.cleanupAllProcesses();
Log.info("[Updater] Child processes cleaned up");
await new Promise(resolve => setTimeout(resolve, 2000));
Log.info("[Updater] Starting update installation...", {
downloadedSetupPath,
differentialDownloadDisabled: autoUpdater.disableDifferentialDownload,
});
autoUpdater.quitAndInstall(false, true);
return { code: 0, msg: "success" };
} catch (e: any) {
Log.error("[Updater] quitAndInstall failed:", e.message);
return { code: -1, msg: `Failed to quit and install: ${e.message}` };
}
};
export default {
checkForUpdate,
downloadUpdate,
quitAndInstall,
getLatestStatus,
};