import { app, BrowserWindow, ipcMain } from "electron"; import { autoUpdater, type ProgressInfo, type UpdateInfo } from "electron-updater"; import type { UpdateCheckResult, UpdateStatus } from "../src/lib/updateStatus"; const DEFAULT_UPDATE_FEED_URL = "https://gittea.softs.business/huanld/openscreen/raw/branch/release-assets%2Flatest"; const AUTO_CHECK_DELAY_MS = 10_000; const AUTO_CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000; let status: UpdateStatus = createStatus("idle"); let handlersRegistered = false; let initialized = false; let checkInFlight: Promise | null = null; function createStatus( phase: UpdateStatus["phase"], patch: Partial = {}, ): UpdateStatus { return { phase, currentVersion: app.getVersion(), updatedAt: new Date().toISOString(), ...patch, }; } function normalizeReleaseNotes(releaseNotes: UpdateInfo["releaseNotes"]): string | undefined { if (typeof releaseNotes === "string") { return releaseNotes; } if (Array.isArray(releaseNotes)) { return releaseNotes .map((note) => note.note) .filter(Boolean) .join("\n\n"); } return undefined; } function updateStatus(next: UpdateStatus) { status = next; for (const window of BrowserWindow.getAllWindows()) { if (!window.isDestroyed()) { window.webContents.send("updates:status", status); } } } function statusFromInfo(phase: UpdateStatus["phase"], info: UpdateInfo): UpdateStatus { return createStatus(phase, { version: info.version, releaseName: info.releaseName ?? undefined, releaseNotes: normalizeReleaseNotes(info.releaseNotes), }); } async function checkForUpdates(): Promise { if (!initialized) { updateStatus( createStatus("unsupported", { error: "Update service is not initialized.", }), ); return { success: false, status, error: status.error }; } if (!app.isPackaged && process.env.OPENSCREEN_ALLOW_DEV_UPDATE_CHECK !== "1") { updateStatus( createStatus("unsupported", { error: "Update checks only run in packaged builds.", }), ); return { success: false, status, error: status.error }; } if (checkInFlight) { return checkInFlight; } updateStatus(createStatus("checking")); checkInFlight = autoUpdater .checkForUpdates() .then(() => ({ success: true, status })) .catch((error) => { const message = error instanceof Error ? error.message : String(error); updateStatus(createStatus("error", { error: message })); return { success: false, status, error: message }; }) .finally(() => { checkInFlight = null; }); return checkInFlight; } function registerUpdateIpcHandlers() { if (handlersRegistered) { return; } handlersRegistered = true; ipcMain.handle("updates:get-status", () => status); ipcMain.handle("updates:check", () => checkForUpdates()); ipcMain.handle("updates:install", () => { if (status.phase !== "downloaded") { return { success: false, status, error: "No downloaded update is ready to install.", }; } setImmediate(() => autoUpdater.quitAndInstall(false, true)); return { success: true, status }; }); } export function initializeAutoUpdates() { registerUpdateIpcHandlers(); if (initialized) { return; } initialized = true; autoUpdater.autoDownload = true; autoUpdater.autoInstallOnAppQuit = true; autoUpdater.logger = console; const feedUrl = process.env.OPENSCREEN_UPDATE_FEED_URL?.trim() || DEFAULT_UPDATE_FEED_URL; const updateToken = process.env.OPENSCREEN_UPDATE_TOKEN?.trim(); if (updateToken) { autoUpdater.requestHeaders = { Authorization: `token ${updateToken}`, }; } autoUpdater.setFeedURL({ provider: "generic", url: feedUrl, }); autoUpdater.on("checking-for-update", () => { updateStatus(createStatus("checking")); }); autoUpdater.on("update-available", (info) => { updateStatus(statusFromInfo("available", info)); }); autoUpdater.on("update-not-available", (info) => { updateStatus(statusFromInfo("not-available", info)); }); autoUpdater.on("download-progress", (progress: ProgressInfo) => { updateStatus( createStatus("downloading", { version: status.version, releaseName: status.releaseName, releaseNotes: status.releaseNotes, percent: progress.percent, bytesPerSecond: progress.bytesPerSecond, transferred: progress.transferred, total: progress.total, }), ); }); autoUpdater.on("update-downloaded", (info) => { updateStatus(statusFromInfo("downloaded", info)); }); autoUpdater.on("error", (error) => { const message = error instanceof Error ? error.message : String(error); updateStatus(createStatus("error", { error: message })); }); setTimeout(() => { void checkForUpdates(); }, AUTO_CHECK_DELAY_MS); setInterval(() => { void checkForUpdates(); }, AUTO_CHECK_INTERVAL_MS).unref(); }