fix(mac): prevent updater hang on manual install

Bump to 1.0.20. Disable autoDownload during manual check and fix
Squirrel.Mac quitAndInstall race on darwin.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-05-19 08:24:52 +08:00
parent 428fa09224
commit 5706355c5f
3 changed files with 59 additions and 8 deletions
+56 -5
View File
@@ -14,6 +14,43 @@ import type { LicenseService } from '../license/licenseService';
const STARTUP_CHECK_DELAY_MS = 12_000; const STARTUP_CHECK_DELAY_MS = 12_000;
/** Не дёргать сервер чаще (смена лицензии / повторные emit). */ /** Не дёргать сервер чаще (смена лицензии / повторные emit). */
const RE_CHECK_COOLDOWN_MS = 30_000; const RE_CHECK_COOLDOWN_MS = 30_000;
/** Ручная загрузка из модалки — не зависать бесконечно на «Загрузка…». */
const MANUAL_DOWNLOAD_TIMEOUT_MS = 30 * 60 * 1000;
function withTimeout<T>(promise: Promise<T>, ms: number, code: string): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error(code)), ms);
promise.then(
(v) => {
clearTimeout(timer);
resolve(v);
},
(e: unknown) => {
clearTimeout(timer);
reject(e instanceof Error ? e : new Error(String(e)));
},
);
});
}
/**
* На macOS Squirrel.Mac может уже получить update-downloaded к моменту quitAndInstall.
* При autoInstallOnAppQuit=true MacUpdater не вызывает checkForUpdates повторно и зависает.
*/
function quitAndInstallForPlatform(): void {
if (process.platform === 'darwin') {
autoUpdater.autoInstallOnAppQuit = false;
}
autoUpdater.quitAndInstall(false, true);
}
function formatUpdaterError(e: unknown): string {
const raw = e instanceof Error ? e.message : String(e);
if (raw === 'UPDATE_DOWNLOAD_TIMEOUT') {
return 'Превышено время ожидания загрузки обновления';
}
return raw;
}
let lastCheckAt = 0; let lastCheckAt = 0;
/** Ручная установка: не показывать второй диалог из `update-downloaded`. */ /** Ручная установка: не показывать второй диалог из `update-downloaded`. */
@@ -42,6 +79,8 @@ async function runManualUpdaterCheck(licenseService: LicenseService): Promise<Up
if (!isLicensedForUpdates(licenseService)) { if (!isLicensedForUpdates(licenseService)) {
return { outcome: 'no_license' }; return { outcome: 'no_license' };
} }
const prevAutoDownload = autoUpdater.autoDownload;
autoUpdater.autoDownload = false;
try { try {
const result = await autoUpdater.checkForUpdates(); const result = await autoUpdater.checkForUpdates();
if (result && result.isUpdateAvailable && result.updateInfo.version) { if (result && result.isUpdateAvailable && result.updateInfo.version) {
@@ -51,6 +90,8 @@ async function runManualUpdaterCheck(licenseService: LicenseService): Promise<Up
} catch (e) { } catch (e) {
const message = e instanceof Error ? e.message : String(e); const message = e instanceof Error ? e.message : String(e);
return { outcome: 'error', message }; return { outcome: 'error', message };
} finally {
autoUpdater.autoDownload = prevAutoDownload;
} }
} }
@@ -58,15 +99,25 @@ async function runManualDownloadAndRestart(): Promise<UpdaterDownloadResponse> {
if (!app.isPackaged) { if (!app.isPackaged) {
return { ok: false, message: 'NOT_PACKAGED' }; return { ok: false, message: 'NOT_PACKAGED' };
} }
const prevAutoInstallOnAppQuit = autoUpdater.autoInstallOnAppQuit;
try { try {
suppressAutoInstallDialog = true; suppressAutoInstallDialog = true;
await autoUpdater.downloadUpdate(); if (process.platform === 'darwin') {
autoUpdater.quitAndInstall(false, true); autoUpdater.autoInstallOnAppQuit = false;
}
await withTimeout(
autoUpdater.downloadUpdate(),
MANUAL_DOWNLOAD_TIMEOUT_MS,
'UPDATE_DOWNLOAD_TIMEOUT',
);
quitAndInstallForPlatform();
return { ok: true }; return { ok: true };
} catch (e) { } catch (e) {
suppressAutoInstallDialog = false; suppressAutoInstallDialog = false;
const message = e instanceof Error ? e.message : String(e); if (process.platform === 'darwin') {
return { ok: false, message }; autoUpdater.autoInstallOnAppQuit = prevAutoInstallOnAppQuit;
}
return { ok: false, message: formatUpdaterError(e) };
} }
} }
@@ -119,7 +170,7 @@ export function installAutoUpdater(licenseService: LicenseService, register: Reg
}) })
.then((r) => { .then((r) => {
if (r.response === 0) { if (r.response === 0) {
autoUpdater.quitAndInstall(false, true); quitAndInstallForPlatform();
} }
}); });
}); });
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "TTRPGPlayer", "name": "TTRPGPlayer",
"version": "1.0.18", "version": "1.0.20",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "TTRPGPlayer", "name": "TTRPGPlayer",
"version": "1.0.18", "version": "1.0.20",
"hasInstallScript": true, "hasInstallScript": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "TTRPGPlayer", "name": "TTRPGPlayer",
"version": "1.0.19", "version": "1.0.20",
"description": "TTRPG Player — редактор и проигрыватель НРИ", "description": "TTRPG Player — редактор и проигрыватель НРИ",
"main": "dist/main/index.cjs", "main": "dist/main/index.cjs",
"scripts": { "scripts": {