Skip to content

[bug]: Update prompt cannot be declined and can kill a running Invoke session or in-progress install #153

Description

@lstein

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

  1. Have a launcher update available. On startup, checkForUpdates prompts; click Download.
  2. await autoUpdater.downloadUpdate() runs unbounded — a 100MB+ installer can take minutes on a slow link.
  3. During the download, start Invoke and begin working (or start a multi-GB install).
  4. The download completes. The "Update downloaded" dialog appears with one button.
  5. Click it, press Esc, or close the dialog — all three reach autoUpdater.quitAndInstall().
  6. quitAndInstall()app.quit()before-quitcleanup()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:

  1. 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
    }
  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions