fix(desktop): surface failed update installs
"Restart to Update" answered the renderer with null before the install was attempted, so a rejected install only reached main.log and the button looked dead. The apply-update path now keeps the IPC call open until the app quits or autoUpdater reports the failure, rolls the quit/install flags back when the app stays up, and the update dialog shows the real reason with a translated hint for a rejected code signature. Also settle the download promise on downloadUpdate() itself: an already cached payload emits no 'update-downloaded', which left that promise pending with its listeners attached on every retry.
This commit is contained in:
+71
-11
@@ -3149,6 +3149,59 @@ const setupAutoUpdater = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// quitAndInstall() reports failures (rejected code signature, a Squirrel
|
||||
// session already disabled by an earlier failure) asynchronously on the
|
||||
// 'error' event, long after the call returns. Give the install that long to
|
||||
// either take the app down or report why it did not.
|
||||
const UPDATE_INSTALL_GRACE_MS = 15_000;
|
||||
|
||||
/**
|
||||
* Hand the downloaded update to the platform installer and keep the IPC call
|
||||
* open until the app quits or the updater reports a failure, so a rejected
|
||||
* install reaches the renderer instead of dying in the log. Restores the
|
||||
* quit/install flags when the install never happens.
|
||||
*/
|
||||
const installDownloadedUpdate = () => new Promise((resolve, reject) => {
|
||||
let settled = false;
|
||||
|
||||
const rollbackQuitState = () => {
|
||||
state.quitRequested = false;
|
||||
state.installingUpdate = false;
|
||||
};
|
||||
|
||||
const fail = (error) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(graceTimer);
|
||||
autoUpdater.off('error', fail);
|
||||
rollbackQuitState();
|
||||
log.error('[electron] update install failed', error);
|
||||
reject(error instanceof Error ? error : new Error(String(error)));
|
||||
};
|
||||
|
||||
// Still running after the grace period: the install is underway and the app
|
||||
// is shutting down, so release the pending IPC reply.
|
||||
const graceTimer = setTimeout(() => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
autoUpdater.off('error', fail);
|
||||
resolve(null);
|
||||
}, UPDATE_INSTALL_GRACE_MS);
|
||||
|
||||
autoUpdater.on('error', fail);
|
||||
|
||||
// Defer so the renderer's invoke channel is idle before the app starts
|
||||
// shutting down.
|
||||
setImmediate(() => {
|
||||
try {
|
||||
killSidecar();
|
||||
autoUpdater.quitAndInstall();
|
||||
} catch (error) {
|
||||
fail(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const parseRelevantChangelogNotes = async (fromVersion, toVersion) => {
|
||||
try {
|
||||
const response = await fetch(CHANGELOG_URL, { signal: AbortSignal.timeout(10_000) });
|
||||
@@ -4486,9 +4539,20 @@ const handleInvoke = async (browserWindow, command, args = {}) => {
|
||||
const onError = (error) => finish(reject, error);
|
||||
autoUpdater.on('update-downloaded', onDownloaded);
|
||||
autoUpdater.on('error', onError);
|
||||
Promise.resolve(autoUpdater.downloadUpdate()).catch((error) => finish(reject, error));
|
||||
// downloadUpdate() resolves once the payload is on disk. It stays
|
||||
// the authoritative signal: when the file was already cached the
|
||||
// updater emits no 'update-downloaded', and waiting only for the
|
||||
// event left this promise pending and its listeners attached on
|
||||
// every retry.
|
||||
Promise.resolve(autoUpdater.downloadUpdate())
|
||||
.then(() => finish(resolve, null))
|
||||
.catch((error) => finish(reject, error));
|
||||
});
|
||||
}
|
||||
// The 'update-downloaded' event does not fire for an already cached
|
||||
// payload, so record the payload as ready here too; otherwise restart
|
||||
// would relaunch without installing anything.
|
||||
state.pendingUpdate.downloaded = true;
|
||||
emitToAllWindows('openchamber:update-progress', mapUpdaterProgressEvent({
|
||||
event: 'Finished',
|
||||
data: {},
|
||||
@@ -4525,20 +4589,16 @@ const handleInvoke = async (browserWindow, command, args = {}) => {
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
return await installDownloadedUpdate();
|
||||
}
|
||||
// Defer so the IPC reply flushes before the app starts shutting down.
|
||||
// Without this, quitAndInstall() can race with the renderer's pending
|
||||
// invoke and the restart appears to do nothing from the UI side.
|
||||
// Without this, relaunch can race with the renderer's pending invoke and
|
||||
// the restart appears to do nothing from the UI side.
|
||||
setImmediate(() => {
|
||||
try {
|
||||
if (applyUpdate) {
|
||||
killSidecar();
|
||||
autoUpdater.quitAndInstall();
|
||||
} else {
|
||||
prepareForQuit();
|
||||
app.relaunch();
|
||||
app.exit(0);
|
||||
}
|
||||
prepareForQuit();
|
||||
app.relaunch();
|
||||
app.exit(0);
|
||||
} catch (err) {
|
||||
log.error('[electron] desktop_restart failed', err);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user