Summary
When a launcher update finishes downloading, the "Update downloaded and ready to install" prompt has exactly one button and its result is discarded — autoUpdater.quitAndInstall() runs unconditionally on every way out of the dialog. There is no way to decline or defer, and no check for whether Invoke is running or an install is in progress, so a background update can terminate a live session with no warning.
Present on main today. Found while reviewing #142, but unrelated to it.
The code
src/main/updater.ts (end of checkForUpdates):
await dialog.showMessageBox(mainWindow, {
type: 'info',
title: 'Update Downloaded',
message: 'Update downloaded and ready to install.',
buttons: ['Restart and Install'],
});
autoUpdater.quitAndInstall();
The returned { response } is not destructured and not checked. With a single button, cancelId defaults to 0, so dismissing with Esc or the window close button also resolves to 0 and falls straight through to quitAndInstall(). There is no "Later" button.
Reproduction
- Have a launcher update available. On startup,
checkForUpdates prompts; click Download.
await autoUpdater.downloadUpdate() runs unbounded — a 100MB+ installer can take minutes on a slow link.
- During the download, start Invoke and begin working (or start a multi-GB install).
- The download completes. The "Update downloaded" dialog appears with one button.
- Click it, press Esc, or close the dialog — all three reach
autoUpdater.quitAndInstall().
quitAndInstall() → app.quit() → before-quit → cleanup() → cleanupInvoke() kills Invoke / cleanupInstall() cancels the install.
Impact
- The user cannot decline or defer the restart — the only escape is killing the launcher process.
- An in-flight generation is lost, or a multi-GB install is cancelled, with no warning that either was running.
app.on('before-quit', cleanup) is async and Electron does not await it (the existing TODO(psyche) at src/main/index.ts), so quitAndInstall() has already spawned the platform installer while exitInvoke() is still inside commandRunner.kill(10_000). The installer and Invoke's SIGTERM shutdown race.
Suggested fix
Two parts, both small:
- Give the dialog a real choice and honour it:
const { response } = await dialog.showMessageBox(mainWindow, {
type: 'info',
title: 'Update Downloaded',
message: 'Update downloaded and ready to install.',
buttons: ['Restart and Install', 'Later'],
defaultId: 0,
cancelId: 1,
});
if (response !== 0) {
return; // electron-updater installs on next quit anyway
}
- Warn when work is in flight. The invoke manager already exposes
isProcessRunning(), and the install manager has a status — if either is active, say so in the detail so the user knows what restarting will stop, rather than discovering it afterwards.
Worth noting that #142 adds a close-confirmation for exactly this class of "you're about to lose a running Invoke" mistake, but it cannot help here: quitAndInstall() goes through app.quit(), which sets isQuitting before any window close, so the confirmation is intentionally skipped. This path needs its own guard.
Summary
When a launcher update finishes downloading, the "Update downloaded and ready to install" prompt has exactly one button and its result is discarded —
autoUpdater.quitAndInstall()runs unconditionally on every way out of the dialog. There is no way to decline or defer, and no check for whether Invoke is running or an install is in progress, so a background update can terminate a live session with no warning.Present on
maintoday. Found while reviewing #142, but unrelated to it.The code
src/main/updater.ts(end ofcheckForUpdates):The returned
{ response }is not destructured and not checked. With a single button,cancelIddefaults to0, so dismissing with Esc or the window close button also resolves to0and falls straight through toquitAndInstall(). There is no "Later" button.Reproduction
checkForUpdatesprompts; click Download.await autoUpdater.downloadUpdate()runs unbounded — a 100MB+ installer can take minutes on a slow link.autoUpdater.quitAndInstall().quitAndInstall()→app.quit()→before-quit→cleanup()→cleanupInvoke()kills Invoke /cleanupInstall()cancels the install.Impact
app.on('before-quit', cleanup)isasyncand Electron does not await it (the existingTODO(psyche)atsrc/main/index.ts), soquitAndInstall()has already spawned the platform installer whileexitInvoke()is still insidecommandRunner.kill(10_000). The installer and Invoke's SIGTERM shutdown race.Suggested fix
Two parts, both small:
isProcessRunning(), and the install manager has a status — if either is active, say so in thedetailso the user knows what restarting will stop, rather than discovering it afterwards.Worth noting that #142 adds a close-confirmation for exactly this class of "you're about to lose a running Invoke" mistake, but it cannot help here:
quitAndInstall()goes throughapp.quit(), which setsisQuittingbefore any window close, so the confirmation is intentionally skipped. This path needs its own guard.