feat(lifecycle): notify and offer restart on in-place package upgrade#564
feat(lifecycle): notify and offer restart on in-place package upgrade#564lizthegrey wants to merge 2 commits intoaaddrick:mainfrom
Conversation
dpkg/rpm replace app.asar via rename() while the main process keeps
its in-memory JS. Any window opened after the swap loads HTML/asset
files fresh from disk, where the hashed asset filenames now point at
v(N+1) bundles that the in-memory v(N) IPC and preload layers don't
match. Symptoms observed across recent reports: Quick Entry rendering
as raw JS text, About dialog showing minified source, and Ctrl+Q
intermittently failing — anything where a post-swap window load
crosses the version boundary.
macOS / Windows clients get this from Squirrel; Linux deb/RPM has no
equivalent, so we watch the file ourselves and surface a click-to-
restart notification. AppImage is unaffected (squashfs mount stays
pinned to the running file's contents); Nix store paths are immutable
until GC, so the running inode also stays valid until explicit
relaunch. The watcher noop-quiet on those targets is deliberate.
Implementation: stat-baseline app.asar at first require('electron'),
watch the parent dir (file-level fs.watch loses the inode across
rename-replace; inotify on the dir reports the new entry via
IN_MOVED_TO), filename-filter, debounce 5s past the last event to
clear dpkg's .dpkg-new → rename dance, compare ino+mtime to confirm
a real change, then show a Notification deferred behind whenReady.
Click → app.relaunch(); app.quit().
Co-Authored-By: Claude <claude@anthropic.com>
Hoist the in-place-upgrade detection block in frame-fix-wrapper.js into an armUpgradeWatcher() helper so the missing-baseline path becomes an early return instead of an outer if (baseline) wrap. Collapse the isReady() ? show() : whenReady().then(show) ternary to plain whenReady().then(show) — Electron's whenReady() resolves immediately when the app is already ready, so the branch was dead. Trim narrative comments that duplicate the previous commit message; keep the why-comments that earn their keep (parent-dir watch, ino+mtime, 5s debounce, watcher.unref). Behaviour preserved: filename filter, 5s debounce, ino+mtime double-check, watcher.unref(), best-effort try/catch, idempotent notified guard. Net -22 lines. Co-Authored-By: Claude <claude@anthropic.com>
|
I accidentally ran into something related to this earlier today while working on a testing suite to validate functionality between distros. Issue #567 I'll come back with more details momentarily |
|
Hey @lizthegrey! I dug into this further after the earlier review and wanted to share the research before we lock in a direction. Trying to figure out the right shape here together rather than render a verdict. ContextI walked the upstream The implementation looks solid from static analysis. Parent-dir I also did a quick ecosystem search to see if there's prior art. Nobody in Electron-land has a clean solution. Official OptionsThere's a related issue (#567) covering a separate bug: Electron's That stub creates a hook point. There's a spectrum of how aggressively we use it: Level 1 — pure stub. What #567 proposes standalone. Defensive against future Electron bumps that implement Linux autoUpdater. Doesn't pretend to be anything more. Level 2 — stub plus synthetic emission. Same stub, but this PR's inotify watcher additionally emits a synthetic Level 3 — full hijack. Substitute our own feed URL. We already run a Cloudflare Worker at CaveatsLevel 3 has an awkward "click does what?" question for the Telemetry implication for Levels 2 and 3: the upstream Coverage noteInotify covers "upgraded while running." There's a sister gap for "upgraded while closed, opened cold" that nothing upstream handles either. I plan to file separately as a launcher-shim diff against a stored last-seen-version. The launcher already has clean hook points between the cleanup functions and The askWhich level feels right? A few possible sequences:
Open to whatever sequencing makes sense, and happy to write whichever piece you don't want to. Written by Claude opus-4.7 via Claude Code |
|
I'd say, land this PR to shore up the immediate issue, then check in with the team about a path forward. Would be interested in your perspective as well. |
Summary
Watch
app.asarfor in-place dpkg/rpm replacement; on detection, show a click-to-restart notification. Fixes the Quick Entry "raw JS" / About "minified source" / Ctrl+Q "intermittent" symptom cluster that traces to the running v(N) main process loading v(N+1) renderer assets after an upgrade. Linux only; AppImage and Nix immutable-store paths are inert as expected.The bug
dpkg/rpm replace
app.asarviarename()while the main process keeps its in-memory JS. AnyBrowserWindowopened after the swap reads HTML/asset files fresh from disk, where the hashed asset filenames now point at v(N+1) bundles that the in-memory v(N) IPC and preload no longer match. Symptoms observed across recent triage:<body>because the v(N)-shaped HTML→asset path landed in a v(N+1) renderer)Before this PR users had no signal that the running process was stale. The macOS / Windows clients get this from Squirrel; Linux deb/RPM has no equivalent.
The fix
Inserted in
scripts/frame-fix-wrapper.jsas a sibling to the autostart shim — same "main-process boot" surface, runs once on firstrequire('electron').fs.watchloses the inode across rename-replace; dir-level inotify reports the new entry viaIN_MOVED_TO/IN_CREATE. Filename-filtered toapp.asarso unrelated activity in the resources dir is ignored..dpkg-new→ rename dance and similar multi-stage swaps in rpm / Nix.inoandmtimeMsso the rename-replace path and the (rare) in-place-rewrite path both register as a real change.whenReadyif the app hasn't reached ready when the swap fires; click →app.relaunch(); app.quit(). Linux libnotify ignores Electron'sactionsarray per the API docs, so whole-notification click is the only affordance — that matches what the macOS client does anyway.Test plan
sudo apt install ./claude-desktop_v(N+1).debwhile it's runningframe-fix-wrapper.jshas no existing test harness in this repo (the autostart shim it sits next to is also untested). Open to wiring up a node mock-test if the maintainer wants one before merge.Generated with Claude Code
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
~100% AI / minimal Human
Claude: diagnosed root cause (running-process / on-disk asset version mismatch after upgrade), designed and implemented the watcher, wrote commit and PR body
Human: filed the bug report, ran the launch-context test that ruled out
CLAUDE_TITLEBAR_STYLEas the culprit, pointed at the macOS restart-prompt UX as the model to follow