fix(electron): install Linux .deb updates without freezing the app - #416
Open
audichuang wants to merge 1 commit into
Open
fix(electron): install Linux .deb updates without freezing the app#416audichuang wants to merge 1 commit into
audichuang wants to merge 1 commit into
Conversation
electron-updater's DebUpdater shells out through BaseUpdater.spawnSyncLog, and spawnSync blocks the whole main process, event loop included, for as long as polkit keeps its password prompt open. When that prompt is not answered (a dialog behind other windows, an SSH session, no graphical agent) the app hangs with no window updates and no way to cancel, and nothing on the JS side can interrupt a spawnSync that has already started. Run the privileged install here instead: spawn pkexec asynchronously, quit only after a zero exit, and report the exit code. Only a downloaded .deb takes this path. AppImage rewrites its own file as the current user, and every other platform keeps electron-updater. Passing argv directly also drops the shell quoting the sync path needed, skips the apt-get retry that raised a second password prompt for the same install, and treats a signalled installer as a failure rather than the success spawnSync reports for `status: null`. A failed install or a failed periodic check no longer discards a downloaded package. recordError keeps the phase at `downloaded` while the package is on disk, because dropping to `error` hid both the sidebar banner and the About install button, which are the only ways to retry: a transient dpkg lock left the user with no button and an `update_not_downloaded` reply for a package sitting in the cache. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Model: claude-opus-5[1m]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related issue
Closes #415
Problem / pressure
DebUpdater.doInstallruns its package manager throughBaseUpdater.spawnSyncLog, which usesspawnSync. That blocks the Electronmain process, event loop included, until the command exits, and on Linux the
command is
pkexec, which does not exit until the polkit password prompt isanswered. Nothing on the JS side can interrupt a
spawnSyncalready inprogress, so there is no timeout and no cancel.
On the reporter's machine one click froze the app for 51 minutes, waiting on a
prompt that was on a desktop session nobody was looking at. A window that stops
repainting that long is indistinguishable from a crash, and the app can neither
report nor cancel anything while it is in that state.
Separately, a failed install discarded a package that was still installable.
phase: 'error'makesreadUpdateBannerStatereturnnull(sidebar bannergone), makes
about-setting.tsxswap the install button for "Check forupdates", and makes
quitAndInstallanswerupdate_not_downloadedfor a filesitting in the pending cache. A transient dpkg lock therefore cost the reporter
the retry until the next 30-minute check; they recovered only by finding the
manual re-check button.
Summary
app-updater-linux-install.tsresolves the privileged command for adownloaded
.deband runs it asynchronously, resolving on the installer'sexit rather than blocking the loop.
AppUpdaterService.quitAndInstallis nowasync. When the plan resolves itspawns
pkexec --disable-internal-agent dpkg -i <file>and callsapp.relaunch()/app.quit()only once the installer has exited 0;otherwise it reports the exit code. Every other target keeps electron-updater
unchanged.
the
apt-get install -f -yretry that raised a second password prompt for thesame install is gone, and a signalled installer is a failure rather than the
success
spawnSyncreports forstatus: null.installInFlightstops a second click from raising a second prompt now thatthe window stays interactive during the first.
recordErrorkeepsphase: 'downloaded'while a package is on disk, so afailed install or a failed periodic check no longer hides the retry. It
replaces the two places that wrote
phase: 'error'directly, andquitAndInstalldetects an electron-updater install failure from an errorcount instead of from the phase.
carries an error, so the surface that keeps its button also says why the last
attempt failed.
Before / after
spawnSyncfor the whole polkit prompt; app frozen, no cancel, no timeoutspawn+awaiton exit; the window keeps repainting and responding while the prompt is openapp.relaunch()ran wheneverdoInstallreturned true, including for a signal-killed installer (status: null)dpkg -ifailure retried throughapt-get install -f -y, raising a second password promptphase: 'error', hiding the sidebar banner and the install button for up to 30 minutesdownloadedwith its error attached; both surfaces keep the retryTest plan
apps/electron/src/main/services/app-updater-linux-install.test.mjs(new, 12cases, added to the package
testscript): plan resolution for deb vsAppImage vs non-Linux, a package path with spaces passed as one unescaped
argument, and through an injected spawn — that the call is still unsettled
after the spawn returns and settles only on
exit, exit 0, 126, 127, anon-zero dpkg code with its stderr, a signalled installer, an
errorevent,a spawn that throws, and
errorplusexitsettling once.pnpm --filter @lody/electron run test— 90/90 pass excludingloro-data-plane-relay.test.mjs, which fails in this checkout with "Electronfailed to install correctly":
node_modules/electron/dist/electronis absenthere, unrelated to this change.
pnpm --filter @lody/electron run typecheck,pnpm --filter @lody/components run typecheck,pnpm lint(0 errors),pnpm lint:i18n,pnpm check:public-boundary,pnpm check:platform-boundaries,pnpm check:code-collab-imports— all pass.pnpm typecheckcannot complete in this checkout:prepare:acp-adaptersfails building the
acp-extension-dshsubmodule on aReadableStreamtypeassertion. Verified identical on a clean tree with these changes stashed, so
it is pre-existing and unrelated.
.debupgrade. It needs a signed releasepair on the update feed, which this checkout cannot produce.
Context handoff
Instructions for reviewing agents
app-updater-service.tsquitAndInstall/installLinuxDebfor the quit-only-on-success ordering, and
recordErrorplus theupdate-available/update-downloadedlisteners fordownloadedFilelifetime.
apt-get install -f -yfallback;reporting instead of falling back when
pkexecis unavailable; keepingphase: 'downloaded'for every error while a package is on disk.run; the promise settles on the installer's
exit, so a helper that neverexits (observed once on the reporter's machine after a successful
dpkg,undiagnosed) leaves the app responsive but not relaunched with a stuck
spinner;
update-availableclearsdownloadedFileandupdate-downloadedre-sets it from cache each cycle, leaving a sub-second window where the plan
does not resolve.
Authoring context
.debin-app update at the rootafter a previous PR addressed only failure reporting, then file the issue and
this PR.
behavior change for Windows, macOS, AppImage, or Sparkle; the upstream
spawnSyncin electron-updater is bypassed, not patched..debinstalls, so a wrong exit-code reading would either skip a restart or restart
without an install; both are reported to the renderer rather than silent.
dpkg -ion the downloaded packageis unchanged from the previous path. Dropping the
apt-get -fretry means anunmet-dependency install now stops and reports rather than attempting repair;
recovery is
sudo apt-get install -fand the error text says so.electron-updater; the mobile About surface is untouched because the bug is
Linux-desktop only; no packaged end-to-end upgrade was run.
behavior, both covered by unit tests and read against electron-updater 6.7.3
sources. Lower for exotic Linux sessions without a polkit agent, which now get
a reported failure instead of a freeze but still cannot install in place.