Skip to content

Commit 9464dfc

Browse files
lizthegreyclaude
andcommitted
feat(lifecycle): notify and offer restart on in-place package upgrade
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 <[email protected]>
1 parent b367f8e commit 9464dfc

1 file changed

Lines changed: 90 additions & 0 deletions

File tree

scripts/frame-fix-wrapper.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,96 @@ X-GNOME-Autostart-enabled=true
654654
console.log('[Autostart] XDG Autostart shim installed');
655655
}
656656

657+
// Detect in-place package upgrade and prompt for restart.
658+
// dpkg/rpm replace app.asar via rename(); the running main
659+
// process keeps its in-memory JS, but any BrowserWindow opened
660+
// after the swap reads HTML/assets fresh from disk, where the
661+
// hashed asset filenames already point at v(N+1) bundles the
662+
// in-memory v(N) IPC and preload no longer match. Symptoms
663+
// observed: Quick Entry rendering as raw JS text, About
664+
// dialog showing minified source, Ctrl+Q intermittently
665+
// failing — anything where the post-swap window load crosses
666+
// the version boundary. macOS / Windows handle this via
667+
// Squirrel; Linux deb/RPM has no equivalent, so we watch the
668+
// file ourselves and surface a notification. AppImage is
669+
// unaffected (squashfs mount stays pinned to the running
670+
// file's contents); Nix store paths are immutable until GC,
671+
// so the running inode also stays valid until the user
672+
// explicitly relaunches.
673+
if (process.platform === 'linux') {
674+
try {
675+
const fs = require('fs');
676+
const asarPath = path.join(process.resourcesPath, 'app.asar');
677+
let baseline = null;
678+
try { baseline = fs.statSync(asarPath); } catch { /* not present */ }
679+
if (baseline) {
680+
let notified = false;
681+
let debounceTimer = null;
682+
const promptRestart = () => {
683+
if (notified) return;
684+
let cur;
685+
try { cur = fs.statSync(asarPath); } catch { return; }
686+
// Compare both inode (rename-replace path) and mtime
687+
// (in-place rewrite path) so we catch either upgrade
688+
// shape. Touching the file with no real content change
689+
// would still fire — tolerable; the restart is a no-op
690+
// for the user, and dpkg/rpm don't do that in practice.
691+
if (cur.ino === baseline.ino
692+
&& cur.mtimeMs === baseline.mtimeMs) {
693+
return;
694+
}
695+
notified = true;
696+
console.log(
697+
'[Frame Fix] app.asar replaced — prompting restart');
698+
const show = () => {
699+
try {
700+
const n = new result.Notification({
701+
title: 'Claude Desktop has been updated',
702+
body: 'Click to restart and apply the update.',
703+
});
704+
// Linux libnotify ignores Electron's `actions`
705+
// (macOS-only per the Notification docs), so the
706+
// whole-notification click is the only affordance.
707+
n.on('click', () => {
708+
result.app.relaunch();
709+
result.app.quit();
710+
});
711+
n.show();
712+
} catch (err) {
713+
console.warn(
714+
'[Frame Fix] Restart notification failed:',
715+
err.message);
716+
}
717+
};
718+
if (result.app.isReady()) show();
719+
else result.app.whenReady().then(show);
720+
};
721+
// Watch the parent dir, not the file: fs.watch on the
722+
// file loses the inode across a rename-replace, but the
723+
// dir watcher reports the new entry via inotify
724+
// IN_MOVED_TO / IN_CREATE. Filter by filename so we
725+
// ignore unrelated activity in the resources dir.
726+
const watcher = fs.watch(path.dirname(asarPath),
727+
(_evt, filename) => {
728+
if (filename !== 'app.asar') return;
729+
if (debounceTimer) clearTimeout(debounceTimer);
730+
// dpkg writes app.asar.dpkg-new then renames; rpm
731+
// and Nix have similar multi-stage swaps. 5s of
732+
// post-event quiet covers the slowest of these
733+
// without being noticeably late.
734+
debounceTimer = setTimeout(promptRestart, 5000);
735+
});
736+
// Don't keep the event loop alive on the watcher alone;
737+
// the app's other handles drive the lifetime.
738+
watcher.unref();
739+
console.log('[Frame Fix] Upgrade watcher armed:', asarPath);
740+
}
741+
} catch (err) {
742+
console.warn('[Frame Fix] Upgrade watcher failed to arm:',
743+
err.message);
744+
}
745+
}
746+
657747
console.log('[Frame Fix] Patches built successfully');
658748
}
659749

0 commit comments

Comments
 (0)