Skip to content

fix(updates): install macOS updates through electron-updater (#955) - #3431

Open
jeanfbrito wants to merge 2 commits into
devfrom
fix/macos-update-install
Open

fix(updates): install macOS updates through electron-updater (#955)#3431
jeanfbrito wants to merge 2 commits into
devfrom
fix/macos-update-install

Conversation

@jeanfbrito

@jeanfbrito jeanfbrito commented Aug 3, 2026

Copy link
Copy Markdown
Member

Summary

Fixes the long-standing macOS update loop reported in #955: click install, the app quits, relaunch, and the same update is offered again — forever.

Root cause

The update-downloaded handler had a darwin-specific branch that destroyed every window and then drove Electron's native autoUpdater directly:

nativeUpdater.checkForUpdates();
nativeUpdater.on('update-downloaded', nativeUpdateDownloadedCallback);

But electron-updater's MacUpdater already owns that same native autoUpdater singleton:

// electron-updater/out/MacUpdater.js:16
this.nativeUpdater = require("electron").autoUpdater;

On macOS it installs by starting a local authenticated HTTP proxy server that serves the already-downloaded zip, pointing Squirrel at it with setFeedURL, and then going through its own quitAndInstall() — which is the only path that consults squirrelDownloadedUpdate and reaches handleUpdateDownloaded():

quitAndInstall() {
  if (this.squirrelDownloadedUpdate) { this.handleUpdateDownloaded(); }
  else { this.nativeUpdater.on("update-downloaded", () => this.handleUpdateDownloaded()); ... }
}

Calling checkForUpdates() on the singleton reached around the library and skipped that bookkeeping entirely. Squirrel never installed, and since all windows had just been destroyed there was no UI left and no way back — so the update reappeared on the next launch.

Two faults compounded it:

  • The update-downloaded listener was registered after the call meant to trigger it, so a fast resolution had nothing listening.
  • The try/catch wrapped only the setImmediate scheduling, not its body. A throw from the later tick escaped it, so UPDATES_ERROR_THROWN was never dispatched and the failure was silent — which is why this went undiagnosed for years.

All three came from 972f141 ("change updater to test", Apr 2023), an experiment that shipped.

The fix

  • Drop the darwin special case; call autoUpdater.quitAndInstall(true, true) on all platforms and let MacUpdater drive Squirrel through its own proxy. This restores the behaviour that predated 972f141.
  • Move the try/catch inside the setImmediate callback so install failures are actually reported.
  • Remove the now-unused BrowserWindow and nativeUpdater imports and the dead nativeUpdateDownloadedCallback.

autoRunAppAfterInstall and autoInstallOnAppQuit both default to true, and the only override in this file is autoDownload = false, so the defaults MacUpdater relies on are intact.

Testing

tsc --noEmit, eslint, and the full yarn test suite (155 suites, 1662 passing, 2 skipped) all pass.

Important

These checks prove nothing about the actual fix — they only show nothing else regressed. None of them execute the install path: setupUpdates has zero test coverage (src/updates/main.spec.ts only tests the pure mergeConfigurations), which is how this survived eight years.

This needs validation on a packaged, signed macOS build against a live update feed before merging. It is the mechanism by which users receive every other fix, and there is no in-app fallback if it misbehaves.

I deliberately did not add a unit test: meaningfully covering this means mocking the MacUpdater/Squirrel proxy interaction, and a test asserting "we called quitAndInstall" would just restate the diff while implying coverage of behaviour it cannot reach.

Note for #3427

#3427 extracts this same block verbatim into installDownloadedUpdate(), so it carries the bug forward. Landing this first means that PR inherits the fix on rebase instead of re-shipping it.

Closes #955

Summary by CodeRabbit

  • Bug Fixes
    • Improved the reliability of installing app updates across supported platforms.
    • Streamlined the update installation process to reduce interruptions during restart.
    • Update failures are now handled more consistently and provide clearer error details.
    • Improved recovery when an update installation is delayed or encounters an unexpected issue.

The darwin branch of the `update-downloaded` handler destroyed every window
and then drove Electron's native `autoUpdater` directly:

    nativeUpdater.checkForUpdates();
    nativeUpdater.on('update-downloaded', nativeUpdateDownloadedCallback);

electron-updater's `MacUpdater` already owns that same native `autoUpdater`
singleton. It serves the downloaded zip from a local authenticated proxy
server, calls `setFeedURL` against it, and installs via its own
`quitAndInstall()` — the only path that consults `squirrelDownloadedUpdate`
and reaches `handleUpdateDownloaded()`. Calling `checkForUpdates()` on the
singleton bypassed that bookkeeping, so Squirrel never installed: the app
quit with no windows left and offered the same update on the next launch.

The listener was also registered after the call meant to trigger it, and the
`try/catch` wrapped only the `setImmediate` scheduling rather than its body,
so a throw from the later tick escaped and never dispatched
`UPDATES_ERROR_THROWN` — leaving the failure silent.

Drop the darwin special case and call `autoUpdater.quitAndInstall(true, true)`
on every platform, restoring the behaviour that predated 972f141, and move
the `try/catch` inside the callback so install failures are reported.

Not verified against a live update feed: that needs a packaged, signed macOS
build. `tsc --noEmit`, `eslint`, and the full test suite pass.
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

macOS update installation

Layer / File(s) Summary
Direct update installation and error reporting
src/updates/main.ts
The updater removes obsolete Electron and native updater imports and macOS-specific fallback logic. Deferred installation calls autoUpdater.quitAndInstall(true, true), temporarily detaches window-all-closed listeners, restores them after failure, and dispatches normalized UPDATES_ERROR_THROWN errors.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested labels: type: bug

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes directly address issue #955 by replacing the macOS native updater flow to prevent the repeated update loop.
Out of Scope Changes check ✅ Passed The changes are limited to update installation and error handling required by issue #955.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: installing macOS updates through electron-updater to fix the update loop.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/updates/main.ts (2)

288-303: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add test coverage for the deferred install-failure path.

This code block is the direct fix for issue #955. Add a test in src/updates/main.spec.ts that simulates autoUpdater.quitAndInstall throwing inside the setImmediate callback and asserts that UPDATES_ERROR_THROWN is dispatched. This locks in the regression fix and guards against the try/catch placement mistake described in the inline comment reoccurring.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/updates/main.ts` around lines 288 - 303, Add a regression test in the
update tests for the deferred install path: mock autoUpdater.quitAndInstall to
throw inside the setImmediate callback, then assert dispatch receives an
UPDATES_ERROR_THROWN action with the captured error details. Ensure the test
flushes the deferred callback and validates the try/catch behavior in the
surrounding update flow.

285-304: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the repeated error-dispatch block into a shared helper.

The error instanceof Error && dispatch({type: UPDATES_ERROR_THROWN, payload: {message, stack, name}}) block appears four times in this file (here and at lines 322-330, 340-348, and 366-374). A small helper, for example reportUpdateError(error: unknown): void, removes the duplication and gives one place to fix the non-Error handling gap noted above.

const reportUpdateError = (error: unknown): void => {
  const normalizedError = error instanceof Error ? error : new Error(String(error));
  dispatch({
    type: UPDATES_ERROR_THROWN,
    payload: {
      message: normalizedError.message,
      stack: normalizedError.stack,
      name: normalizedError.name,
    },
  });
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/updates/main.ts` around lines 285 - 304, Extract the repeated
UPDATES_ERROR_THROWN dispatch logic into a shared reportUpdateError(error:
unknown) helper in src/updates/main.ts. Have it normalize non-Error values into
Error instances and dispatch message, stack, and name; then replace the four
duplicated error instanceof Error blocks, including the setImmediate catch, with
calls to this helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/updates/main.ts`:
- Around line 292-301: Update the catch block around quitAndInstall to normalize
every thrown value into an Error before dispatching UPDATES_ERROR_THROWN. Remove
the error instanceof Error guard, preserve existing Error instances, and derive
message, stack, and name from the normalized error so non-Error throws are
reported as well.
- Around line 288-303: Update the setImmediate update-install flow around
autoUpdater.quitAndInstall so a thrown installation error restores the
window-all-closed listener(s) removed by app.removeAllListeners. Perform the
restoration in the catch path before or alongside dispatching
UPDATES_ERROR_THROWN, preserving the existing error payload behavior.

---

Nitpick comments:
In `@src/updates/main.ts`:
- Around line 288-303: Add a regression test in the update tests for the
deferred install path: mock autoUpdater.quitAndInstall to throw inside the
setImmediate callback, then assert dispatch receives an UPDATES_ERROR_THROWN
action with the captured error details. Ensure the test flushes the deferred
callback and validates the try/catch behavior in the surrounding update flow.
- Around line 285-304: Extract the repeated UPDATES_ERROR_THROWN dispatch logic
into a shared reportUpdateError(error: unknown) helper in src/updates/main.ts.
Have it normalize non-Error values into Error instances and dispatch message,
stack, and name; then replace the four duplicated error instanceof Error blocks,
including the setImmediate catch, with calls to this helper.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 031bdcaa-2cbf-49c0-98f7-4ed1400ee893

📥 Commits

Reviewing files that changed from the base of the PR and between f935367 and 96a88fe.

📒 Files selected for processing (1)
  • src/updates/main.ts
📜 Review details
⏰ Context from checks skipped due to timeout. (3)
  • GitHub Check: check (ubuntu-latest)
  • GitHub Check: check (windows-latest)
  • GitHub Check: Analyze (javascript)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript for new code unless explicitly told otherwise.
Use Fuselage components from @rocket.chat/fuselage for UI work unless the design requires something Fuselage does not provide.
Check Theme.d.ts for valid color tokens before using Fuselage colors.
Verify library props, APIs, and tokens against official docs or local .d.ts files instead of assuming.
Use React functional components with hooks.
Redux actions follow FSA shape.
Use camelCase for file names and PascalCase for components.
Prefer clear names over unnecessary comments.
Prefer editing existing files over creating new abstractions unless the new abstraction removes real complexity or matches an existing pattern.

**/*.{ts,tsx}: Use TypeScript for all new code unless explicitly told otherwise.
Use Fuselage components for all UI work; create custom components only when Fuselage lacks the required functionality.
Import Fuselage components from @rocket.chat/fuselage.
Use only valid color tokens documented by Theme.d.ts.
Use optional chaining with fallbacks for platform-specific APIs, especially Linux-only process APIs such as process.getuid(), getgid(), geteuid(), and getegid().
Use TypeScript strict mode.
Redux actions must follow the Flux Standard Action pattern.
Use camelCase for file names and PascalCase for component names.
Avoid unnecessary comments; prefer self-documenting code through clear naming.
Do not commit or push without explicit user permission.
Verify library APIs, props, tokens, and types against official documentation and .d.ts files instead of assuming they are valid.

Files:

  • src/updates/main.ts
🔇 Additional comments (1)
src/updates/main.ts (1)

4-4: LGTM!

Comment thread src/updates/main.ts
Comment thread src/updates/main.ts Outdated
…tall

Addresses two review findings on the install path:

- `app.removeAllListeners('window-all-closed')` detaches the app's quit
  handler process-wide. The removal is needed so the pending quit isn't
  cancelled, but when the install throws we never quit, leaving those
  listeners detached for the rest of the session — closing every window would
  then no longer reach the handler in src/app/main/app.ts. Capture the
  listeners first and reattach them in the error path.

- `error instanceof Error && dispatch(...)` silently dropped non-Error thrown
  values, so a thrown string or object produced no error action at all. Since
  this block exists specifically to surface install failures that used to go
  unreported, normalize any thrown value into a serializable
  {message, stack, name} payload before dispatching.
@jeanfbrito

Copy link
Copy Markdown
Member Author

Holding this for the release after 4.16.0 rather than merging into the current cycle.

The reasoning is the validation gap called out above: nothing in CI executes this code path, and the change can only be meaningfully verified on a packaged, signed macOS build against a real update feed. Since it touches the update mechanism itself — and a regression there costs users the ability to update in-app at all, including to a fix — it should not ride along on a release that is already carrying the navigation rework.

Deferring alongside #3427, which reworks the same update surface (its installDownloadedUpdate() extracts this exact block). Landing both in the same cycle keeps the update UX and the install fix validated together instead of split across releases.

No changes needed on the PR itself: both CodeRabbit findings are addressed and confirmed, tsc/eslint/the full suite pass, and the branch is conflict-free against master. It is ready for review whenever the next cycle opens — the outstanding work is the packaged-build validation, not the code.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Updates on macOS do not install

1 participant