import { app, dialog } from 'electron'; import { autoUpdater } from 'electron-updater'; import { ipcChannels, type UpdaterCheckResponse, type UpdaterDownloadResponse, } from '../../shared/ipc/contracts'; import type { IpcRegisterHandler } from '../ipc/router'; import { addLicenseChangeListener } from '../license/licenseService'; import type { LicenseService } from '../license/licenseService'; const STARTUP_CHECK_DELAY_MS = 12_000; /** Не дёргать сервер чаще (смена лицензии / повторные emit). */ const RE_CHECK_COOLDOWN_MS = 30_000; let lastCheckAt = 0; /** Ручная установка: не показывать второй диалог из `update-downloaded`. */ let suppressAutoInstallDialog = false; type RegisterFn = IpcRegisterHandler; function isLicensedForUpdates(licenseService: LicenseService): boolean { const snap = licenseService.getStatusSync(); return snap.active; } function maybeCheckForUpdates(licenseService: LicenseService, ignoreCooldown: boolean): void { if (!app.isPackaged) return; if (!isLicensedForUpdates(licenseService)) return; const now = Date.now(); if (!ignoreCooldown && now - lastCheckAt < RE_CHECK_COOLDOWN_MS) return; lastCheckAt = now; void autoUpdater.checkForUpdates().catch(() => undefined); } async function runManualUpdaterCheck(licenseService: LicenseService): Promise { if (!app.isPackaged) { return { outcome: 'not_packaged' }; } if (!isLicensedForUpdates(licenseService)) { return { outcome: 'no_license' }; } try { const result = await autoUpdater.checkForUpdates(); if (result && result.isUpdateAvailable && result.updateInfo.version) { return { outcome: 'available', version: result.updateInfo.version }; } return { outcome: 'current', currentVersion: app.getVersion() }; } catch (e) { const message = e instanceof Error ? e.message : String(e); return { outcome: 'error', message }; } } async function runManualDownloadAndRestart(): Promise { if (!app.isPackaged) { return { ok: false, message: 'NOT_PACKAGED' }; } try { suppressAutoInstallDialog = true; await autoUpdater.downloadUpdate(); autoUpdater.quitAndInstall(false, true); return { ok: true }; } catch (e) { suppressAutoInstallDialog = false; const message = e instanceof Error ? e.message : String(e); return { ok: false, message }; } } function registerUpdaterHandlers(register: RegisterFn, licenseService: LicenseService): void { register(ipcChannels.updater.check, () => runManualUpdaterCheck(licenseService)); register(ipcChannels.updater.downloadAndRestart, () => runManualDownloadAndRestart()); } /** * Проверка обновлений: только упакованное приложение, только при активной лицензии. * Канал и URL задаются при сборке (`publish` → `app-update.yml` внутри установки). */ export function installAutoUpdater(licenseService: LicenseService, register: RegisterFn): void { registerUpdaterHandlers(register, licenseService); if (!app.isPackaged) return; const feedOverride = process.env.DND_UPDATE_FEED_URL?.trim(); if (feedOverride) { const url = feedOverride.endsWith('/') ? feedOverride : `${feedOverride}/`; autoUpdater.setFeedURL({ provider: 'generic', url }); } autoUpdater.autoDownload = true; autoUpdater.autoInstallOnAppQuit = true; autoUpdater.on('update-downloaded', (info) => { if (suppressAutoInstallDialog) { suppressAutoInstallDialog = false; return; } void dialog .showMessageBox({ type: 'info', title: 'DNDGamePlayer', message: `Доступна новая версия ${info.version}. Установить и перезапустить?`, buttons: ['Перезапустить сейчас', 'Позже'], defaultId: 0, cancelId: 1, }) .then((r) => { if (r.response === 0) { autoUpdater.quitAndInstall(false, true); } }); }); autoUpdater.on('error', () => { /* без console: в production main минифицируется с drop console */ }); addLicenseChangeListener(() => { maybeCheckForUpdates(licenseService, false); }); setTimeout(() => { maybeCheckForUpdates(licenseService, true); }, STARTUP_CHECK_DELAY_MS); }