feat: add 2-stage update system (asar swap + full installer)

This commit is contained in:
2026-04-16 08:10:06 -07:00
parent 4aecb402d6
commit d2696a510f
6 changed files with 852 additions and 331 deletions
+72 -30
View File
@@ -1,6 +1,6 @@
import { app, BrowserWindow, Menu, clipboard, ipcMain, safeStorage, session, shell } from 'electron';
import { join } from 'path';
import { existsSync, mkdirSync, promises as fsp } from 'fs';
import { join, dirname } from 'path';
import { existsSync, mkdirSync, unlinkSync, promises as fsp } from 'fs';
import { get as httpsGet } from 'https';
import { execFile } from 'child_process';
import * as os from 'os';
@@ -10,7 +10,7 @@ import { initSwapperProtocol, registerSwapperFileProtocol, ResourceSwapper } fro
import { UserscriptManager } from './userscripts';
import { ALL_CLIENT_CSS } from './client-ui';
import { electronLog, getLogPath, closeLogStreams } from './logger';
import { checkForUpdate, downloadUpdate, installUpdate } from './updater';
import { checkForUpdate, downloadUpdate, installUpdate, applyMinorUpdate } from './updater';
import { showUpdateWindow } from './update-window';
import { DiscordRPC } from './discord-rpc';
import { listThemes, getThemeCSS, listLoadingThemes, getLoadingScreenCSS } from './css-themes';
@@ -164,51 +164,93 @@ function saveWindowState(win: BrowserWindow): void {
app.whenReady().then(async () => {
electronLog.log('[KCC] App ready');
electronLog.log('[KCC] Minor update test — asar swap successful');
// ── Auto-update check (mandatory, Windows NSIS install only) ──
// ── Auto-update check (2-stage: minor asar swap or major installer) ──
const isPortable = !!process.env.PORTABLE_EXECUTABLE_DIR;
const isAppImage = !!process.env.APPIMAGE;
const isDev = !app.isPackaged;
if (isDev || process.platform !== 'win32' || isPortable || isAppImage) {
electronLog.log('[KCC] Skipping auto-update (portable or non-Windows)');
const canMajorUpdate = process.platform === 'win32' && !isPortable;
const canMinorUpdate = !isPortable && !isAppImage;
if (isDev || (!canMajorUpdate && !canMinorUpdate)) {
electronLog.log('[KCC] Skipping auto-update');
} else {
// Clean up stale pending asar from a previous failed swap
const resourcesDir = join(dirname(app.getPath('exe')), 'resources');
const stalePending = join(resourcesDir, 'app-pending.asar');
if (existsSync(stalePending)) {
try { unlinkSync(stalePending); } catch { /* ignore */ }
}
try {
electronLog.log('[KCC] Checking for updates...');
const update = await checkForUpdate(appVersion);
if (update) {
electronLog.log(`[KCC] Update available: v${update.version}`);
electronLog.log(`[KCC] Update available: v${update.version} (${update.updateType})`);
const { window: updateWin, sendProgress } = showUpdateWindow();
sendProgress(`Update available (v${update.version})`, 0);
const tempDir = join(app.getPath('temp'), 'kcc-update');
if (!existsSync(tempDir)) mkdirSync(tempDir, { recursive: true });
const installerPath = join(tempDir, `KCC-${update.version}-Setup.exe`);
let cancelled = false;
updateWin.on('closed', () => { cancelled = true; });
let cancelled = false;
updateWin.on('closed', () => { cancelled = true; });
if (update.updateType === 'minor' && canMinorUpdate) {
// Minor update: download app.asar, swap via external script, restart
sendProgress(`Patch available (v${update.version})`, 0);
const pendingPath = join(resourcesDir, 'app-pending.asar');
try {
await downloadUpdate(update.downloadUrl, installerPath, (pct) => {
if (!cancelled && !updateWin.isDestroyed()) {
sendProgress(`Downloading update... ${pct}%`, pct);
try {
await downloadUpdate(update.downloadUrl, pendingPath, (pct) => {
if (!cancelled && !updateWin.isDestroyed()) {
sendProgress(`Downloading patch... ${pct}%`, pct);
}
}, update.sha256 || undefined);
if (!cancelled) {
sendProgress('Applying patch...', 100);
applyMinorUpdate(pendingPath);
return;
}
} catch (err) {
electronLog.error('[KCC] Patch download failed:', err);
// Clean up failed download
if (existsSync(pendingPath)) {
try { unlinkSync(pendingPath); } catch { /* ignore */ }
}
if (!updateWin.isDestroyed()) updateWin.close();
}
}, update.sha256);
} else if (update.updateType === 'major' && canMajorUpdate) {
// Major update: download Setup.exe, run installer
sendProgress(`Update available (v${update.version})`, 0);
const tempDir = join(app.getPath('temp'), 'kcc-update');
if (!existsSync(tempDir)) mkdirSync(tempDir, { recursive: true });
const installerPath = join(tempDir, `KCC-${update.version}-Setup.exe`);
if (!cancelled) {
sendProgress('Installing update...', 100);
installUpdate(installerPath);
return; // app.quit() called by installUpdate
try {
await downloadUpdate(update.downloadUrl, installerPath, (pct) => {
if (!cancelled && !updateWin.isDestroyed()) {
sendProgress(`Downloading update... ${pct}%`, pct);
}
}, update.sha256 || undefined);
if (!cancelled) {
sendProgress('Installing update...', 100);
installUpdate(installerPath);
return;
}
} catch (err) {
electronLog.error('[KCC] Update download failed:', err);
if (!updateWin.isDestroyed()) updateWin.close();
}
} else {
electronLog.log('[KCC] Update available but cannot auto-install on this platform');
if (!updateWin.isDestroyed()) updateWin.close();
}
} catch (err) {
electronLog.error('[KCC] Update download failed:', err);
if (!updateWin.isDestroyed()) updateWin.close();
} else {
electronLog.log('[KCC] No updates available');
}
} else {
electronLog.log('[KCC] No updates available');
} catch (err) {
electronLog.error('[KCC] Update check failed:', err);
}
} catch (err) {
electronLog.error('[KCC] Update check failed:', err);
}
}
await launchApp();