Linux desktop notifications no longer freeze the main process (salvage #109623) - #118197
Merged
kshitijk4poor merged 6 commits intoSep 21, 2026
Conversation
(cherry picked from commit 3dd43eb)
(cherry picked from commit 5b0a1e3)
…transport `finished` cannot be observed inside show(): close() aborts the signal first, and dbus-native settles an aborted invoke synchronously, so the awaiting code throws instead of resuming past the check. `connection !== state` is implied by the generation check (disconnect bumps the generation before clearing the connection), `connection !== target.state` in close() is implied by `delivered` (disconnect fails every live entry, which clears it), and receive() can never run after release() deleted its map entry. A malformed Notify id now gets its own error instead of being reported as an owner change.
…Linux Every failure in show() armed the global 10 s cooldown, including the ones caused by the notification daemon being replaced mid-call: the owner-changed fences, and the bus's NameHasNoOwner error for a call already addressed to the vanished unique name. Linux has no Electron fallback, so every notification in that window was silently dropped exactly when the new daemon was healthy. Classify by generation rather than by error: a failure against an owner that is still current is the daemon's fault and cools down; one against an owner that has since been replaced does not. The fixture now answers calls to a stale unique name with NameHasNoOwner like the real bus, and the race test asserts the next notification reaches the new owner immediately.
The transport was picked from process.platform, so the ipc test mocked the Linux module and the only vitest lane (ubuntu) never exercised the Electron Notification branch that Windows and macOS run, while the Linux tests skipped everywhere else. Make the platform an injectable host parameter: the ipc test drives the Electron branch as darwin, the Linux tests pass linux explicitly and lose their skipIf.
The session-bus socket was created lazily and never closed. main.ts already tears down its pooled keep-alive sockets in will-quit so nothing holds the event loop open or leaks an fd past app teardown; the new long-lived socket joins that policy. Disposal goes through the existing close path, so live notifications are failed the same way a daemon crash fails them.
kshitijk4poor
enabled auto-merge (rebase)
September 21, 2026 13:20
11 of 12 tasks
alt-glitch
self-requested a review
September 21, 2026 13:56
alt-glitch
approved these changes
Sep 21, 2026
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.
Linux desktop notifications no longer freeze the main process when the notification daemon stalls; they go over D-Bus asynchronously and fail with a bounded
falseinstead.Salvage of #109623 by @BearHuddleston (both commits cherry-picked with authorship preserved; clean apply on current
main) plus four follow-ups from review.Why
Electron's
Notificationon Linux calls libnotify synchronously. A daemon that is slow to activate, hung, or restarting blocks the main thread for the duration (observed while working on Bot Screen, #108914). Windows and macOS are unaffected and keep the Electron path.Changes
Contributor commits (@BearHuddleston):
apps/desktop/electron/notification-linux.ts(new): freedesktoporg.freedesktop.Notificationstransport over the session bus using the pure-JSdbus-native@0.15.2. OwnHello,GetNameOwner→StartServiceByNamefallback →GetCapabilities→Notifywith the same payload shape libnotify sends (default/View+ indexed actions,desktop-entry,suppress-sound, no expiry). 5 s per-call timeout with AbortController, 10 s cooldown after a failure,NameOwnerChangedfencing so a restarted daemon's reused IDs never fire the old notification's callbacks, sender fencing onActionInvoked/NotificationClosed.notification-ipc.ts: picks the Linux transport onlinux, otherwise unchanged ElectronNotification; peer renderers now share the in-flight delivery promise for the same dedupe key.notification-registry.ts:releaseOnCloseoption (Linux only) so a daemon-dismissed notification is released instead of held for the 10 min TTL.Follow-ups (ours):
refactor: drop fences that cannot execute (finishedafter an aborted invoke,connection !== statealongside the generation check,if (finished)inside a deleted map entry); a malformedNotifyid gets its own error.fix: a benign daemon swap mid-call no longer arms the 10 s global cooldown; Linux has no fallback, so that window dropped every notification exactly when the new daemon was healthy. Failures are classified by generation (owner still current → daemon's fault → cooldown; owner replaced since → none), because on a real bus a call to the vanished unique name comes back asNameHasNoOwner, not as the transport's own fence error. The fixture now answers stale unique names that way, and the race test asserts the next notification reaches the new owner immediately.test:registerNativeNotificationstakes an injectableplatform. The only vitest lane runs on ubuntu, and the ipc test had mocked the Linux module, so the Electron branch that Windows/macOS run had zero CI coverage; the Linux tests lose theirskipIf. 8 tests now run on every host (was 5 + 3 skipped on macOS).fix:dispose()closes the bus socket from the existingwill-quitteardown inmain.ts, matching the file's keep-alive socket policy.Dependency:
dbus-native@0.15.2(one runtime dep,xml2js; no native code, no dynamic requires). Lockfile delta is that closure plussaxflipping dev→prod. Bundled intodist/electron-main.mjsby the existing esbuild script; nothing needs to be external.Verification
tsc -p tsconfig.electron.json, eslint on touched filesnotification-{linux,ipc,registry}.test.tson macOS hostreleaseOnCloseoff / delivery timeout removed / cooldown exemption removed /disposeno-op /Notification.isSupportedfalsedbus-daemon --session+ stuborg.freedesktop.Notifications, esbuild-bundled transport)Notify/GetCapabilities→falseat 5.0 s with the event loop alive (53 heartbeats during the stall) and the next call cooled down; absent service and dead bus →falsein <10 ms; daemon killed and replaced whileGetCapabilitiesis in flight →falseat 0.9 s, next notification delivered 12 ms later (no cooldown)main-boot-smoke.sh(main.ts touched)requiredNon-Linux behaviour traced unchanged:
linuxis undefined, so theisSupportedgate, dedupe key,new Notification(options), registry retain andshow()order are as before; the renderer discards thenotify()result (void window.hermesDesktop?.notify(...)).Fork CI never ran on #109623 (
action_required), so the above is the first execution evidence for this change.Closes #109623