From c2cd79b547a5737cb98587225e62ce01094c5ef6 Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Tue, 18 Aug 2026 02:53:01 -0400 Subject: [PATCH] Serialize display recovery checks --- config/dot/quickshell/services/Displays.qml | 25 ++++++++++++++------- tests/quickshell/displays-contract.sh | 18 ++++++++++++++- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/config/dot/quickshell/services/Displays.qml b/config/dot/quickshell/services/Displays.qml index 20fd834..19c40b8 100644 --- a/config/dot/quickshell/services/Displays.qml +++ b/config/dot/quickshell/services/Displays.qml @@ -85,7 +85,8 @@ Singleton { if (!root.awaitingConfirmation) return; if (root.revertQueued) { - root.performRevert(); + if (!query.running) + root.performRevert(); return; } if (exitCode !== 0) { @@ -93,6 +94,7 @@ Singleton { return; } verifyTimer.attempts = 0; + verifyTimer.ticks = 0; verifyTimer.restart(); } } @@ -104,17 +106,20 @@ Singleton { // success without applying a value, so exact readback decides. root.revertVerificationActive = true; revertVerifyTimer.attempts = 0; + revertVerifyTimer.ticks = 0; revertVerifyTimer.restart(); } } Component.onCompleted: root.refresh() - function refresh(): void { + function refresh(): bool { if (!query.running) { query.generation = root.operationGeneration; query.running = true; + return true; } + return false; } function parse(text: string, generation: int): void { @@ -411,26 +416,29 @@ Singleton { Timer { id: verifyTimer property int attempts: 0 + property int ticks: 0 interval: 120 repeat: true onTriggered: { - attempts++; - if (attempts > 25) { + ticks++; + if (ticks > 50) { root.revertWithMessage("The display did not apply that setting, so Panama restored the previous one."); return; } - root.refresh(); + if (root.refresh()) + attempts++; } } Timer { id: revertVerifyTimer property int attempts: 0 + property int ticks: 0 interval: 120 repeat: true onTriggered: { - attempts++; - if (attempts > 25) { + ticks++; + if (ticks > 50) { stop(); root.revertVerificationActive = false; root.revertGeneration = -1; @@ -439,7 +447,8 @@ Singleton { root.lastError = "The previous display setting could not be verified. Open Displays and restore it manually."; return; } - root.refresh(); + if (root.refresh()) + attempts++; } } diff --git a/tests/quickshell/displays-contract.sh b/tests/quickshell/displays-contract.sh index ec0acd0..a7fda87 100755 --- a/tests/quickshell/displays-contract.sh +++ b/tests/quickshell/displays-contract.sh @@ -225,8 +225,24 @@ KINDS now="$(status)" [[ "$(jq -r .scale <<<"$now")" == "$original_scale" ]] || fail 'a refused change still altered the scale' -# ── An unconfirmed change reverts on its own and stores nothing ────────────── +# An immediate Revert may race both the apply process and its first readback. +# It must queue until both are clear, then verify the original generation. target_scale=$(awk -v s="$original_scale" 'BEGIN { print (s == 1.25) ? 1.5 : 1.25 }') +[[ "$(run ipc call displays-test applyScale "$target_scale")" == "true" ]] \ + || fail 'the immediate-revert fixture could not apply' +run ipc call displays-test revertChange >/dev/null +immediate_reverted=false +for _ in $(seq 1 60); do + if display_is_restored && [[ "$(status | jq -r .awaiting)" == "false" ]]; then + immediate_reverted=true + break + fi + sleep 0.2 +done +[[ "$immediate_reverted" == true ]] \ + || fail 'an immediate Revert raced the apply/readback and did not restore the display' + +# ── An unconfirmed change reverts on its own and stores nothing ────────────── [[ "$(run ipc call displays-test applyScale "$target_scale")" == "true" ]] \ || fail 'a valid scale change was refused'