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:
@@ -55,6 +55,18 @@ Singleton {
|
||||
// installed. Distinct from an empty history.
|
||||
property bool available: true
|
||||
|
||||
// query's `exited` and its stdout `streamFinished` are not guaranteed to
|
||||
// fire in a particular order (the same signal-ordering hazard
|
||||
// HomeAssistantConfig.qml's settle-both pattern guards against). These
|
||||
// track which of the two have arrived for the query currently in flight
|
||||
// so the result is only finalized once both are known -- otherwise a
|
||||
// failed query's empty stdout could be parsed as "empty history" before
|
||||
// the non-zero exit code is seen, or vice versa.
|
||||
property bool _queryExited: false
|
||||
property int _queryExitCode: 0
|
||||
property bool _queryStdoutDone: false
|
||||
property string _queryStdoutText: ""
|
||||
|
||||
// Wall-clock seconds at the moment `entries` was filled. Relative times are
|
||||
// rendered against this rather than against a live clock, so a row's label
|
||||
// cannot change while the user is reading it and nothing has to tick.
|
||||
@@ -69,6 +81,8 @@ Singleton {
|
||||
if (query.running)
|
||||
return;
|
||||
root.loading = true;
|
||||
root._queryExited = false;
|
||||
root._queryStdoutDone = false;
|
||||
query.running = true;
|
||||
}
|
||||
|
||||
@@ -146,16 +160,18 @@ Singleton {
|
||||
command: ["sqlite3", "-readonly", "-json", root.dbPath, root.sql]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: root._parse(this.text)
|
||||
onStreamFinished: {
|
||||
root._queryStdoutDone = true;
|
||||
root._queryStdoutText = this.text;
|
||||
root._settleQuery();
|
||||
}
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
root.loading = false;
|
||||
if (exitCode !== 0) {
|
||||
root.available = false;
|
||||
root.entries = [];
|
||||
root.refreshed();
|
||||
}
|
||||
root._queryExited = true;
|
||||
root._queryExitCode = exitCode;
|
||||
root._settleQuery();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +185,29 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Called from both query.onExited and its stdout streamFinished. Only
|
||||
// finalizes once both signals have arrived, since their firing order is
|
||||
// not guaranteed -- see the _queryExited / _queryStdoutDone comment
|
||||
// above. A non-zero exit always means the history is unavailable,
|
||||
// regardless of what (if anything) stdout produced; only a clean exit
|
||||
// reaches _parse, where empty stdout is legitimately "no history yet".
|
||||
function _settleQuery(): void {
|
||||
if (!root._queryExited || !root._queryStdoutDone)
|
||||
return;
|
||||
const exitCode = root._queryExitCode;
|
||||
const text = root._queryStdoutText;
|
||||
root._queryExited = false;
|
||||
root._queryStdoutDone = false;
|
||||
|
||||
if (exitCode !== 0) {
|
||||
root.available = false;
|
||||
root.entries = [];
|
||||
root.refreshed();
|
||||
return;
|
||||
}
|
||||
root._parse(text);
|
||||
}
|
||||
|
||||
function _parse(text: string): void {
|
||||
root.available = true;
|
||||
root.queriedAt = Date.now() / 1000;
|
||||
|
||||
Reference in New Issue
Block a user