chore: replace update-electron-app with Gitea-backed updater

This commit is contained in:
MiTHRAL 2026-04-22 17:30:21 -04:00
parent 225c623ecb
commit cc8ba75694
5 changed files with 251 additions and 38 deletions

View file

@ -54,7 +54,6 @@
"discord-rpc": "^4.0.1",
"electron-squirrel-startup": "^1.0.1",
"electron-store": "^10.1.0",
"update-electron-app": "^3.1.2",
"utf-8-validate": "^6.0.5"
},
"packageManager": "pnpm@10.33.0"

34
pnpm-lock.yaml generated
View file

@ -29,9 +29,6 @@ importers:
electron-store:
specifier: ^10.1.0
version: 10.1.0
update-electron-app:
specifier: ^3.1.2
version: 3.1.2
utf-8-validate:
specifier: ^6.0.5
version: 6.0.5
@ -910,56 +907,67 @@ packages:
resolution: {integrity: sha512-kDWSPafToDd8LcBYd1t5jw7bD5Ojcu12S3uT372e5HKPzQt532vW+rGFFOaiR0opxePyUkHrwz8iWYEyH1IIQA==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.52.2':
resolution: {integrity: sha512-gKm7Mk9wCv6/rkzwCiUC4KnevYhlf8ztBrDRT9g/u//1fZLapSRc+eDZj2Eu2wpJ+0RzUKgtNijnVIB4ZxyL+w==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.52.2':
resolution: {integrity: sha512-66lA8vnj5mB/rtDNwPgrrKUOtCLVQypkyDa2gMfOefXK6rcZAxKLO9Fy3GkW8VkPnENv9hBkNOFfGLf6rNKGUg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.52.2':
resolution: {integrity: sha512-s+OPucLNdJHvuZHuIz2WwncJ+SfWHFEmlC5nKMUgAelUeBUnlB4wt7rXWiyG4Zn07uY2Dd+SGyVa9oyLkVGOjA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.52.2':
resolution: {integrity: sha512-8wTRM3+gVMDLLDdaT6tKmOE3lJyRy9NpJUS/ZRWmLCmOPIJhVyXwjBo+XbrrwtV33Em1/eCTd5TuGJm4+DmYjw==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-gnu@4.52.2':
resolution: {integrity: sha512-6yqEfgJ1anIeuP2P/zhtfBlDpXUb80t8DpbYwXQ3bQd95JMvUaqiX+fKqYqUwZXqdJDd8xdilNtsHM2N0cFm6A==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.52.2':
resolution: {integrity: sha512-sshYUiYVSEI2B6dp4jMncwxbrUqRdNApF2c3bhtLAU0qA8Lrri0p0NauOsTWh3yCCCDyBOjESHMExonp7Nzc0w==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.52.2':
resolution: {integrity: sha512-duBLgd+3pqC4MMwBrKkFxaZerUxZcYApQVC5SdbF5/e/589GwVvlRUnyqMFbM8iUSb1BaoX/3fRL7hB9m2Pj8Q==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.52.2':
resolution: {integrity: sha512-tzhYJJidDUVGMgVyE+PmxENPHlvvqm1KILjjZhB8/xHYqAGeizh3GBGf9u6WdJpZrz1aCpIIHG0LgJgH9rVjHQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.52.2':
resolution: {integrity: sha512-opH8GSUuVcCSSyHHcl5hELrmnk4waZoVpgn/4FDao9iyE4WpQhyWJ5ryl5M3ocp4qkRuHfyXnGqg8M9oKCEKRA==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.52.2':
resolution: {integrity: sha512-LSeBHnGli1pPKVJ79ZVJgeZWWZXkEe/5o8kcn23M8eMKCUANejchJbF/JqzM4RRjOJfNRhKJk8FuqL1GKjF5oQ==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openharmony-arm64@4.52.2':
resolution: {integrity: sha512-uPj7MQ6/s+/GOpolavm6BPo+6CbhbKYyZHUDvZ/SmJM7pfDBgdGisFX3bY/CBDMg2ZO4utfhlApkSfZ92yXw7Q==}
@ -1977,9 +1985,6 @@ packages:
resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
engines: {node: '>= 0.4'}
github-url-to-object@4.0.6:
resolution: {integrity: sha512-NaqbYHMUAlPcmWFdrAB7bcxrNIiiJWJe8s/2+iOc9vlcHlwHqSGrPk+Yi3nu6ebTwgsZEa7igz+NH2vEq3gYwQ==}
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@ -2267,9 +2272,6 @@ packages:
resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
engines: {node: '>=10'}
is-url@1.2.4:
resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
is-weakmap@2.0.2:
resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
engines: {node: '>= 0.4'}
@ -3302,9 +3304,6 @@ packages:
peerDependencies:
browserslist: '>= 4.21.0'
update-electron-app@3.1.2:
resolution: {integrity: sha512-htLyPJv7mEoCpaSzCg0W3Hxz7ID0GC7BIhhpK32/ITG7McrWak4aOkLEOjJheKAI94AxtBVTjCk4EFIvyttw2w==}
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
@ -5835,10 +5834,6 @@ snapshots:
es-errors: 1.3.0
get-intrinsic: 1.3.0
github-url-to-object@4.0.6:
dependencies:
is-url: 1.2.4
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
@ -6146,8 +6141,6 @@ snapshots:
is-unicode-supported@0.1.0: {}
is-url@1.2.4: {}
is-weakmap@2.0.2: {}
is-weakref@1.1.1:
@ -7218,11 +7211,6 @@ snapshots:
escalade: 3.2.0
picocolors: 1.1.1
update-electron-app@3.1.2:
dependencies:
github-url-to-object: 4.0.6
ms: 2.1.3
uri-js@4.4.1:
dependencies:
punycode: 2.3.1

View file

@ -1,8 +1,7 @@
import * as fs from "fs";
import * as path from "path";
import { updateElectronApp } from "update-electron-app";
import { BrowserWindow, Notification, app, shell } from "electron";
import { BrowserWindow, app, shell } from "electron";
import started from "electron-squirrel-startup";
import { aviaVersion } from "../package.json";
@ -11,6 +10,7 @@ import { autoLaunch } from "./native/autoLaunch";
import { setBadgeCount } from "./native/badges";
import { config } from "./native/config";
import { initDiscordRpc } from "./native/discordRpc";
import { checkForUpdates } from "./native/updater";
import { initTray } from "./native/tray";
import { BUILD_URL, createMainWindow, mainWindow } from "./native/window";
@ -42,16 +42,6 @@ if (!config.hardwareAcceleration) {
const acquiredLock = app.requestSingleInstanceLock();
const onNotifyUser = () => {
const notification = new Notification({
title: "Update Available",
body: "Restart the app to install the update.",
silent: true,
});
notification.show();
};
const loadInject = () => {
if (!mainWindow) return;
@ -87,8 +77,6 @@ const loadInject = () => {
};
if (acquiredLock) {
updateElectronApp({ onNotifyUser });
app.whenReady().then(() => {
applyAppName();
createMainWindow();
@ -110,6 +98,7 @@ if (acquiredLock) {
initTray();
initDiscordRpc();
checkForUpdates();
setBadgeCount(0);
if (process.platform === "win32") {

View file

@ -0,0 +1,83 @@
import { BrowserWindow, app, ipcMain } from "electron";
let win: BrowserWindow | null = null;
const HTML = `<!DOCTYPE html><html><head><meta charset="utf-8"><style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:system-ui,sans-serif;background:#1e1e2e;color:#cdd6f4;
display:flex;flex-direction:column;align-items:center;justify-content:center;
height:100vh;padding:28px 32px;gap:14px;user-select:none;-webkit-app-region:drag}
h2{font-size:15px;font-weight:600;letter-spacing:.3px}
#status{font-size:13px;color:#a6adc8}
.track{width:100%;height:6px;background:#313244;border-radius:3px}
#bar{height:6px;background:#89b4fa;border-radius:3px;width:0%;transition:width .4s ease}
#btn{display:none;margin-top:6px;padding:9px 24px;background:#89b4fa;color:#1e1e2e;
border:none;border-radius:6px;font-size:13px;font-weight:700;cursor:pointer;
-webkit-app-region:no-drag}
#btn:hover{background:#b4befe}
</style></head><body>
<h2 id="title">Updating Sanctum</h2>
<div id="status">Downloading</div>
<div class="track"><div id="bar"></div></div>
<button id="btn">Restart Now</button>
<script>
const {ipcRenderer}=require('electron');
ipcRenderer.on('upd-progress',(_,p)=>{
document.getElementById('bar').style.width=p+'%';
document.getElementById('status').textContent=p<100?'Downloading… '+p+'%':'Installing…';
});
ipcRenderer.on('upd-ready',(_,v)=>{
document.getElementById('title').textContent='Sanctum '+v+' installed';
document.getElementById('bar').style.width='100%';
var s=document.getElementById('status');
var n=2;
s.textContent='Restarting in '+n+'…';
var t=setInterval(function(){
n--;
if(n<=0){clearInterval(t);s.textContent='Restarting…';}
else s.textContent='Restarting in '+n+'…';
},1000);
});
ipcRenderer.on('upd-error',(_,msg)=>{
document.getElementById('title').textContent='Update Failed';
document.getElementById('status').textContent=msg;
document.getElementById('bar').style.background='#f38ba8';
});
</script></body></html>`;
export function showUpdateWindow() {
if (win) { win.focus(); return; }
win = new BrowserWindow({
width: 400,
height: 180,
resizable: false,
minimizable: false,
maximizable: false,
fullscreenable: false,
title: "Sanctum Update",
frame: false,
alwaysOnTop: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
},
});
win.loadURL("data:text/html;charset=utf-8," + encodeURIComponent(HTML));
win.on("closed", () => { win = null; });
}
export function setUpdateProgress(percent: number) {
win?.webContents.send("upd-progress", Math.round(percent));
}
export function setUpdateReady(version: string, relaunch: boolean) {
win?.webContents.send("upd-ready", version);
setTimeout(() => {
if (relaunch) app.relaunch();
app.exit(0);
}, 3000);
}
export function setUpdateError(msg: string) {
win?.webContents.send("upd-error", msg);
}

154
src/native/updater.ts Normal file
View file

@ -0,0 +1,154 @@
import { Notification, app, ipcMain } from "electron";
import { exec, spawn } from "child_process";
import { createWriteStream, mkdirSync, writeFileSync } from "fs";
import { dirname, join } from "path";
import { tmpdir } from "os";
import { showUpdateWindow, setUpdateProgress, setUpdateReady, setUpdateError } from "./update-window";
ipcMain.handle("checkForUpdates", () => checkForUpdates());
const RELEASES_URL =
"https://git.mithraic.cloud/api/v1/repos/ad3laid3/sanctum/releases/latest";
interface Asset {
name: string;
browser_download_url: string;
}
interface Release {
tag_name: string;
html_url: string;
assets: Asset[];
}
export async function checkForUpdates() {
try {
console.log("[updater] checking:", RELEASES_URL);
const res = await fetch(RELEASES_URL);
if (!res.ok) {
console.error("[updater] releases API returned", res.status, res.statusText);
notify("Update Check Failed", `API returned ${res.status}`);
return;
}
const release = (await res.json()) as Release;
const latest = release.tag_name.replace(/^v/, "");
const current = app.getVersion();
console.log(`[updater] current=${current} latest=${latest}`);
if (!isNewer(latest, current)) {
notify("Already Up to Date", `You're on Sanctum ${current}.`);
return;
}
const asset = findAsset(release.assets);
if (!asset) {
const names = release.assets.map(a => a.name).join(", ");
console.error("[updater] no matching asset for platform:", process.platform, names);
notify("Update Failed", `No ${process.platform} asset found.`);
return;
}
console.log(`[updater] update available: ${current}${latest}, downloading ${asset.name}`);
showUpdateWindow();
await downloadAndInstall(asset.browser_download_url, latest);
} catch (err) {
console.error("[updater] update check failed:", err);
setUpdateError(String(err));
}
}
function findAsset(assets: Asset[]): Asset | undefined {
if (process.platform === "linux")
return assets.find((a) => a.name.includes("linux") && a.name.endsWith(".zip"));
if (process.platform === "win32")
return assets.find((a) => a.name.includes("win32") && a.name.endsWith(".zip"));
}
async function downloadAndInstall(url: string, version: string) {
const tmpDir = join(tmpdir(), `sanctum-update-${version}`);
mkdirSync(tmpDir, { recursive: true });
const zipPath = join(tmpDir, "update.zip");
const extractDir = join(tmpDir, "extracted");
const installDir = dirname(process.execPath);
console.log(`[updater] downloading from ${url}`);
const res = await fetch(url);
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
const total = Number(res.headers.get("content-length")) || 0;
let downloaded = 0;
const writer = createWriteStream(zipPath);
const reader = res.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
writer.write(value);
downloaded += value.length;
if (total > 0) setUpdateProgress(Math.round((downloaded / total) * 90));
}
await new Promise<void>(resolve => writer.end(resolve));
setUpdateProgress(95);
console.log(`[updater] download complete, extracting to ${installDir}`);
if (process.platform === "win32") {
// Extract zip while the app is still running (no locked files yet)
await new Promise<void>((resolve, reject) => {
const cmd = `powershell -Command "Expand-Archive -Force -Path '${zipPath}' -DestinationPath '${extractDir}'"`;
exec(cmd, (err, _stdout, stderr) => {
if (err) { console.error("[updater] extract failed:", stderr); reject(err); }
else resolve();
});
});
// Write a batch script that runs after we exit: waits, copies, relaunches
const batchPath = join(tmpDir, "apply-update.bat");
const bat = [
"@echo off",
"timeout /t 3 /nobreak >nul",
`for /d %%D in ("${extractDir}\\*") do set SUB=%%D`,
`xcopy /E /Y /I "%SUB%\\*" "${installDir}\\"`,
`start "" "${join(installDir, "sanctum.exe")}"`,
`del "%~f0"`,
].join("\r\n");
writeFileSync(batchPath, bat);
// Spawn batch truly detached so it outlives this process
const child = spawn("cmd.exe", ["/C", batchPath], {
detached: true,
stdio: "ignore",
windowsHide: false,
});
child.unref();
setUpdateReady(version, false); // batch script handles relaunch
} else {
await new Promise<void>((resolve, reject) => {
const cmd = `unzip -o "${zipPath}" -d "${extractDir}" && SUBDIR=$(ls "${extractDir}" | head -1) && rm -f "${installDir}/sanctum" && cp -rT "${extractDir}/$SUBDIR" "${installDir}"`;
exec(cmd, { shell: "/bin/bash" }, (err, _stdout, stderr) => {
if (err) { console.error("[updater] extract failed:", stderr); reject(err); }
else resolve();
});
});
setUpdateReady(version, true);
}
}
function notify(title: string, body: string) {
const n = new Notification({ title, body, silent: true });
n.show();
return n;
}
function isNewer(latest: string, current: string): boolean {
const parse = (v: string) => v.split(".").map(p => Number(p) || 0);
const [lA, lB, lC] = parse(latest);
const [cA, cB, cC] = parse(current);
if (lA !== cA) return lA > cA;
if (lB !== cB) return lB > cB;
return lC > cC;
}