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;
/** Не дёргать сервер чаще (смена лицензии / повторные emit). */
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;
/** Ручная установка: не показывать второй диалог из `update-downloaded`. */
@@ -42,6 +79,8 @@ async function runManualUpdaterCheck(licenseService: LicenseService): Promise<Up
if (!isLicensedForUpdates(licenseService)) {
return { outcome: 'no_license' };
}
const prevAutoDownload = autoUpdater.autoDownload;
autoUpdater.autoDownload = false;
try {
const result = await autoUpdater.checkForUpdates();
if (result && result.isUpdateAvailable && result.updateInfo.version) {
@@ -51,6 +90,8 @@ async function runManualUpdaterCheck(licenseService: LicenseService): Promise<Up
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { outcome: 'error', message };
} finally {
autoUpdater.autoDownload = prevAutoDownload;
}
}
@@ -58,15 +99,25 @@ async function runManualDownloadAndRestart(): Promise<UpdaterDownloadResponse> {
if (!app.isPackaged) {
return { ok: false, message: 'NOT_PACKAGED' };
}
const prevAutoInstallOnAppQuit = autoUpdater.autoInstallOnAppQuit;
try {
suppressAutoInstallDialog = true;
await autoUpdater.downloadUpdate();
autoUpdater.quitAndInstall(false, true);
if (process.platform === 'darwin') {
autoUpdater.autoInstallOnAppQuit = false;
}
await withTimeout(
autoUpdater.downloadUpdate(),
MANUAL_DOWNLOAD_TIMEOUT_MS,
'UPDATE_DOWNLOAD_TIMEOUT',
);
quitAndInstallForPlatform();
return { ok: true };
} catch (e) {
suppressAutoInstallDialog = false;
const message = e instanceof Error ? e.message : String(e);
return { ok: false, message };
if (process.platform === 'darwin') {
autoUpdater.autoInstallOnAppQuit = prevAutoInstallOnAppQuit;
}
return { ok: false, message: formatUpdaterError(e) };
}
}
@@ -119,7 +170,7 @@ export function installAutoUpdater(licenseService: LicenseService, register: Reg
})
.then((r) => {
if (r.response === 0) {
autoUpdater.quitAndInstall(false, true);
quitAndInstallForPlatform();
}
});
});
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "TTRPGPlayer",
"version": "1.0.18",
"version": "1.0.20",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "TTRPGPlayer",
"version": "1.0.18",
"version": "1.0.20",
"hasInstallScript": true,
"license": "ISC",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "TTRPGPlayer",
"version": "1.0.19",
"version": "1.0.20",
"description": "TTRPG Player — редактор и проигрыватель НРИ",
"main": "dist/main/index.cjs",
"scripts": {