Settle process-signal races across the services layer
A Process's exited and streamFinished signals aren't guaranteed to fire in order, and several services decided an outcome on whichever fired first: KdeConnect could report a successful file transfer as failed if exited landed before the real stdout payload; Clipboard could present a failed history query as an empty-but-healthy one; Brightness could strand the last queued write of a drag; SoundFeedback and SystemLocale could drop or misapply a rapid second toggle/click because re-arming an already-running Process is a no-op. All five now wait for both signals and let the authoritative one decide, matching the pattern HomeAssistantConfig.qml already used correctly. Health's "copy report" never enabled stdin, so it copied nothing while claiming success. Capture announced every recording as saved regardless of the recorder's actual exit code. Connectivity never restarted Bluetooth discovery when the adapter was enabled from an already-open page. CalendarAgenda left the UI in "loading" forever if its helper died at startup, and the helper itself could crash unguarded instead of reporting unavailable. Geocoding silently dropped a query typed while the previous one was still in flight. Notifs leaked tracked-but-undisplayed notifications under Do Not Disturb, and dismissAll() skipped them. Claude-Session: https://claude.ai/code/session_01E6TJUAh41HaP25MVHWkhRZ
This commit is contained in:
@@ -4,7 +4,7 @@ pragma Singleton
|
||||
// The freedesktop notification server, plus the two lists the UI renders:
|
||||
//
|
||||
// popups — what Toasts.qml is currently showing (transient, timed)
|
||||
// history — GNOME's message tray, what NotificationCenter.qml shows
|
||||
// history — GNOME's message tray, what NotificationList.qml shows
|
||||
//
|
||||
// A notification lives exactly as long as `tracked` is true, so history holds
|
||||
// the *live* objects rather than copies: that keeps actions and inline replies
|
||||
@@ -80,6 +80,21 @@ Singleton {
|
||||
return at ? Qt.formatDateTime(at, Settings.use24Hour ? "HH:mm" : "h:mm AP") : "";
|
||||
}
|
||||
|
||||
// Freedesktop timeout resolution, shared by the toast countdown (Toast.qml)
|
||||
// and the no-display expiry a transient notification gets while Do Not
|
||||
// Disturb is on (below). Critical urgency and an explicit expireTimeout
|
||||
// override policy; -1 ("server decides") falls back to it. 0 means "never
|
||||
// auto-expire" per spec.
|
||||
function notificationTimeoutMs(notification: var): int {
|
||||
if (notification.urgency === NotificationUrgency.Critical)
|
||||
return Settings.notificationTimeoutCriticalMs;
|
||||
if (notification.expireTimeout === 0)
|
||||
return 0;
|
||||
if (notification.expireTimeout > 0)
|
||||
return Math.round(notification.expireTimeout * 1000);
|
||||
return Settings.notificationTimeoutMs;
|
||||
}
|
||||
|
||||
readonly property bool hasNotifications: root.history.length > 0
|
||||
|
||||
function notificationAppId(notification: var): string {
|
||||
@@ -156,7 +171,7 @@ Singleton {
|
||||
}
|
||||
|
||||
// history grouped by app, in most-recent-app-first order — the shape
|
||||
// NotificationCenter.qml renders directly.
|
||||
// NotificationList.qml renders directly.
|
||||
readonly property var groups: {
|
||||
const out = [];
|
||||
const byApp = {};
|
||||
@@ -219,8 +234,44 @@ Singleton {
|
||||
root.unreadCount += 1;
|
||||
}
|
||||
|
||||
if (!root.doNotDisturb)
|
||||
if (!root.doNotDisturb) {
|
||||
root.popups = [notification].concat(root.popups);
|
||||
} else if (notification.transient) {
|
||||
// Never shown, and (being transient) never filed in history
|
||||
// either — nothing will otherwise dismiss() it, so schedule
|
||||
// the same release its popup timeout would have given it.
|
||||
root.scheduleTransientExpiry(notification);
|
||||
}
|
||||
}
|
||||
|
||||
// Runs a DND-hidden transient notification through the same lifetime it
|
||||
// would have gotten as a visible popup (Toast.qml's countdown), just
|
||||
// without ever showing it, so it still gets released instead of staying
|
||||
// tracked forever.
|
||||
function scheduleTransientExpiry(notification: var): void {
|
||||
const ms = root.notificationTimeoutMs(notification);
|
||||
if (ms <= 0)
|
||||
return;
|
||||
|
||||
const timer = Qt.createQmlObject("import QtQuick; Timer { repeat: false }", root);
|
||||
timer.interval = ms;
|
||||
timer.triggered.connect(() => {
|
||||
timer.destroy();
|
||||
root.releaseTransient(notification);
|
||||
});
|
||||
|
||||
// Closed some other way first (app-side close, dismissAll()) — cancel
|
||||
// the pending timer instead of firing a stale dismiss() later.
|
||||
notification.closed.connect(() => timer.destroy());
|
||||
timer.running = true;
|
||||
}
|
||||
|
||||
// Transient notifications are never filed in history, so nothing else
|
||||
// holds a reference once their lifetime ends — release tracked state
|
||||
// directly. Shared by the DND-hidden expiry above and dismissAll() below.
|
||||
function releaseTransient(n: var): void {
|
||||
if (n.transient)
|
||||
n.dismiss();
|
||||
}
|
||||
|
||||
// ── Mutation ────────────────────────────────────────────────────────────
|
||||
@@ -243,10 +294,7 @@ Singleton {
|
||||
if (next.length === root.popups.length)
|
||||
return;
|
||||
root.popups = next;
|
||||
|
||||
// Transients were never in history, so nothing else holds them.
|
||||
if (n.transient)
|
||||
n.dismiss();
|
||||
root.releaseTransient(n);
|
||||
}
|
||||
|
||||
function dismiss(n: Notification): void {
|
||||
@@ -257,10 +305,19 @@ Singleton {
|
||||
// Copy first: dismiss() re-enters through forget() and rewrites both
|
||||
// lists while we iterate.
|
||||
const all = root.history.slice();
|
||||
|
||||
// Transient notifications are never filed in history — whether
|
||||
// currently shown as a popup or hidden by Do Not Disturb with a
|
||||
// pending scheduleTransientExpiry() timer — so releasing them needs
|
||||
// its own pass over the server's full tracked set.
|
||||
const transients = root.active.values.filter(n => n.transient);
|
||||
|
||||
root.history = [];
|
||||
root.popups = [];
|
||||
for (const n of all)
|
||||
n.dismiss();
|
||||
for (const n of transients)
|
||||
root.releaseTransient(n);
|
||||
root.unreadCount = 0;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user