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:
Gabriel Brown
2026-08-18 21:23:07 -04:00
parent e6b4d3c1a1
commit 8b59b78d9f
12 changed files with 323 additions and 45 deletions
@@ -35,13 +35,32 @@ Singleton {
function setEventSounds(enabled: bool): void {
root.eventSounds = enabled;
eventWrite.command = ["gsettings", "set", "org.gnome.desktop.sound", "event-sounds", String(enabled)];
eventWrite.running = true;
root._writeEventSounds();
}
function setInputFeedback(enabled: bool): void {
root.inputFeedback = enabled;
inputWrite.command = ["gsettings", "set", "org.gnome.desktop.sound", "input-feedback-sounds", String(enabled)];
root._writeInputFeedback();
}
// Assigning `running = true` to a Process that is already running is a
// no-op, not a queue -- so a toggle that lands mid-write would otherwise
// be dropped silently. eventWrite.onExited re-checks root.eventSounds
// against what was actually written and calls this again if they still
// disagree.
function _writeEventSounds(): void {
if (eventWrite.running)
return;
eventWrite.writtenValue = root.eventSounds;
eventWrite.command = ["gsettings", "set", "org.gnome.desktop.sound", "event-sounds", String(root.eventSounds)];
eventWrite.running = true;
}
function _writeInputFeedback(): void {
if (inputWrite.running)
return;
inputWrite.writtenValue = root.inputFeedback;
inputWrite.command = ["gsettings", "set", "org.gnome.desktop.sound", "input-feedback-sounds", String(root.inputFeedback)];
inputWrite.running = true;
}
@@ -71,6 +90,7 @@ Singleton {
Process {
id: eventWrite
property bool writtenValue: true
onExited: (code, status) => {
if (code !== 0) {
root.lastError = "Event sound preferences could not be changed.";
@@ -78,11 +98,14 @@ Singleton {
} else {
root.lastError = "";
}
if (root.eventSounds !== eventWrite.writtenValue)
root._writeEventSounds();
}
}
Process {
id: inputWrite
property bool writtenValue: false
onExited: (code, status) => {
if (code !== 0) {
root.lastError = "Input feedback preferences could not be changed.";
@@ -90,6 +113,8 @@ Singleton {
} else {
root.lastError = "";
}
if (root.inputFeedback !== inputWrite.writtenValue)
root._writeInputFeedback();
}
}