Files
huanld a235a0c50b
CI / Lint (push) Waiting to run
CI / Type Check (push) Waiting to run
CI / Test (push) Waiting to run
CI / Build (push) Waiting to run
Add automatic update checks
2026-06-05 10:54:22 +07:00

178 lines
4.7 KiB
TypeScript

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<UpdateCheckResult> | null = null;
function createStatus(
phase: UpdateStatus["phase"],
patch: Partial<UpdateStatus> = {},
): 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<UpdateCheckResult> {
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();
}