196 lines
6.3 KiB
JavaScript
196 lines
6.3 KiB
JavaScript
const { autoUpdater } = require('electron-updater');
|
|
const log = require('electron-log');
|
|
|
|
autoUpdater.logger = log;
|
|
autoUpdater.autoDownload = false;
|
|
autoUpdater.autoInstallOnAppQuit = true;
|
|
|
|
// Remembered across the process so the main window can ask for it
|
|
// once the renderer is ready. Cleared on successful download so we
|
|
// don't mislead about a pending install.
|
|
let lastCheckResult = {
|
|
hasUpdate: false,
|
|
required: false,
|
|
latestVersion: null,
|
|
currentVersion: null,
|
|
releaseNotes: null,
|
|
downloading: false,
|
|
downloaded: false,
|
|
progress: 0,
|
|
error: null,
|
|
};
|
|
|
|
const statusListeners = new Set();
|
|
|
|
function emitStatus() {
|
|
for (const cb of statusListeners) {
|
|
try { cb({ ...lastCheckResult }); } catch (err) { log.warn('update status listener threw', err); }
|
|
}
|
|
}
|
|
|
|
function onStatus(cb) {
|
|
statusListeners.add(cb);
|
|
return () => statusListeners.delete(cb);
|
|
}
|
|
|
|
function getStatus() {
|
|
return { ...lastCheckResult };
|
|
}
|
|
|
|
// A release is "required" when its notes start with the `[REQUIRED]`
|
|
// marker. Keeping the signal in release notes means no new feed file
|
|
// or schema change — publishers just prefix the message.
|
|
function isRequired(info) {
|
|
const notes = typeof info?.releaseNotes === 'string' ? info.releaseNotes : '';
|
|
return /^\s*\[REQUIRED\]/i.test(notes);
|
|
}
|
|
|
|
// Splash-phase check. Resolves once we know whether to open the main
|
|
// window (optional or no update) or to force an install (required).
|
|
// - No update / error → resolve true (main window should open)
|
|
// - Optional update → resolve true (main window should open; header
|
|
// icon surfaces the update inside the app)
|
|
// - Required update → download + quitAndInstall; never resolves
|
|
function checkForUpdates(splashWindow) {
|
|
return new Promise((resolve) => {
|
|
function sendToSplash(js) {
|
|
if (splashWindow && !splashWindow.isDestroyed()) {
|
|
splashWindow.webContents.executeJavaScript(js).catch(() => {});
|
|
}
|
|
}
|
|
|
|
autoUpdater.on('checking-for-update', () => {
|
|
sendToSplash('setStatus("Checking for updates...")');
|
|
});
|
|
|
|
autoUpdater.on('update-available', (info) => {
|
|
const required = isRequired(info);
|
|
lastCheckResult = {
|
|
hasUpdate: true,
|
|
required,
|
|
latestVersion: info?.version ?? null,
|
|
currentVersion: autoUpdater.currentVersion?.version ?? null,
|
|
releaseNotes: typeof info?.releaseNotes === 'string' ? info.releaseNotes : null,
|
|
downloading: required,
|
|
downloaded: false,
|
|
progress: 0,
|
|
error: null,
|
|
};
|
|
emitStatus();
|
|
if (required) {
|
|
sendToSplash('setStatus("Downloading required update...")');
|
|
autoUpdater.downloadUpdate().catch((err) => {
|
|
log.error('downloadUpdate (required) failed:', err);
|
|
sendToSplash('setStatus("Update failed — opening anyway")');
|
|
lastCheckResult.error = err?.message || 'download failed';
|
|
emitStatus();
|
|
setTimeout(() => resolve(true), 1500);
|
|
});
|
|
} else {
|
|
// Optional — fall through to open main window. The header
|
|
// icon will surface the offer inside the app.
|
|
sendToSplash('setStatus("Update available — continuing")');
|
|
setTimeout(() => resolve(true), 400);
|
|
}
|
|
});
|
|
|
|
autoUpdater.on('download-progress', (progress) => {
|
|
const percent = Math.round(progress.percent || 0);
|
|
lastCheckResult.progress = percent;
|
|
lastCheckResult.downloading = true;
|
|
emitStatus();
|
|
if (lastCheckResult.required) {
|
|
sendToSplash(`setProgress(${percent})`);
|
|
sendToSplash(`setStatus("Downloading required update... ${percent}%")`);
|
|
}
|
|
});
|
|
|
|
autoUpdater.on('update-downloaded', () => {
|
|
lastCheckResult.downloading = false;
|
|
lastCheckResult.downloaded = true;
|
|
lastCheckResult.progress = 100;
|
|
emitStatus();
|
|
if (lastCheckResult.required) {
|
|
sendToSplash('setStatus("Installing update...")');
|
|
sendToSplash('setProgress(100)');
|
|
setTimeout(() => {
|
|
autoUpdater.quitAndInstall();
|
|
}, 1200);
|
|
}
|
|
});
|
|
|
|
autoUpdater.on('update-not-available', () => {
|
|
lastCheckResult = {
|
|
hasUpdate: false,
|
|
required: false,
|
|
latestVersion: null,
|
|
currentVersion: autoUpdater.currentVersion?.version ?? null,
|
|
releaseNotes: null,
|
|
downloading: false,
|
|
downloaded: false,
|
|
progress: 0,
|
|
error: null,
|
|
};
|
|
emitStatus();
|
|
sendToSplash('setStatus("Up to date!")');
|
|
sendToSplash('hideProgress()');
|
|
setTimeout(() => resolve(true), 500);
|
|
});
|
|
|
|
autoUpdater.on('error', (err) => {
|
|
log.error('Auto-updater error:', err);
|
|
lastCheckResult.error = err?.message || String(err);
|
|
lastCheckResult.downloading = false;
|
|
emitStatus();
|
|
sendToSplash('setStatus("Update check failed")');
|
|
sendToSplash('hideProgress()');
|
|
setTimeout(() => resolve(true), 1500);
|
|
});
|
|
|
|
autoUpdater.checkForUpdates().catch((err) => {
|
|
log.error('checkForUpdates failed:', err);
|
|
lastCheckResult.error = err?.message || String(err);
|
|
emitStatus();
|
|
sendToSplash('setStatus("Update check failed")');
|
|
sendToSplash('hideProgress()');
|
|
setTimeout(() => resolve(true), 1500);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Start downloading an optional update from the renderer. The
|
|
// splash flow handles required updates itself so the renderer only
|
|
// ever reaches here for optional ones.
|
|
async function downloadAndInstall() {
|
|
if (!lastCheckResult.hasUpdate) return { ok: false, error: 'no update' };
|
|
if (lastCheckResult.downloaded) {
|
|
autoUpdater.quitAndInstall();
|
|
return { ok: true };
|
|
}
|
|
if (lastCheckResult.downloading) return { ok: true };
|
|
try {
|
|
lastCheckResult.downloading = true;
|
|
lastCheckResult.progress = 0;
|
|
lastCheckResult.error = null;
|
|
emitStatus();
|
|
await autoUpdater.downloadUpdate();
|
|
// The 'update-downloaded' handler bumps status; install triggers
|
|
// the quit-and-install flow the next tick.
|
|
autoUpdater.quitAndInstall();
|
|
return { ok: true };
|
|
} catch (err) {
|
|
log.error('downloadAndInstall failed:', err);
|
|
lastCheckResult.downloading = false;
|
|
lastCheckResult.error = err?.message || String(err);
|
|
emitStatus();
|
|
return { ok: false, error: lastCheckResult.error };
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
checkForUpdates,
|
|
getStatus,
|
|
onStatus,
|
|
downloadAndInstall,
|
|
};
|