fix(displays): revert a pending change even if the app is killed #84
No reviewers
Labels
No labels
breadbug
bug
displays
enhancement
keybinds
network
phase-1
power
privacy
recovery
search
snapshots
sound
ui
ux
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Breadway/bos-settings!84
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/bset-67-revert-after-crash"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes BSET-67
What
The keep-or-revert deadline from #67 lives in a thread of the app. A SIGKILL, crash or OOM kill during the countdown left an unconfirmed layout live until the next login. (SIGTERM, SIGINT and SIGHUP are already handled.)
New
display_rescue.rs:{id, pid, deadline_ms, before}to$XDG_RUNTIME_DIR/bos-settings/display-pending.json(atomic tmp then rename). It then starts a detachedbos-settings --display-watchdog <id>in its own session (setsidinpre_exec), so killing the app or its process group doesn't kill the watchdog. If the record can't be written or the watchdog can't start, nothing is changed and the user gets a clear error.finish_revert), a failed apply, and the thread-spawn failure path.clear_recordonly removes a record whose id matches, so an older change can never delete a newer change's record.beforethrough the sameplan_statementsandhyprctl evalpath, then removes the record. If the owner is alive, it gets a 10s grace period to revert itself before the watchdog steps in.DisplayGuard::newwithout a rescue, so they never spawn processes.Tests
display_rescue: decide() covering not due, owner dead, a different change, and owner alive with grace then stuck. Also a record round trip with own-id clearing,check_oncerestoring exactly once and clearing, and pid liveness.display_guard:mem::forgetand expire the deadline.check_onceputs the mock compositor back to the original layout.--display-watchdog 5against an expired record, with a fakehyprctlon PATH and a tempXDG_RUNTIME_DIR. It issued exactly onehl.monitor(...)restoring the recorded layout and removed the record.Not verified live
I didn't SIGKILL a real session's app mid-countdown, because this would reverse a display change on a real compositor. It is worth one check in the BOS VM, the way the SIGTERM fix was checked.
Capability changes
None user-facing. Each display change now also starts one short-lived background
bos-settings --display-watchdogprocess, which exits when the change ends.Review of BSET-67 at exact head
44d5438ecb(base main7c7749d). Verdict: CHANGES REQUESTED (one real defect, plus non-blocking notes).Verified on this head
check / check (push): success on this exact SHA (run 178).--display-watchdogwith a fake hyprctl first on PATH and a temp XDG_RUNTIME_DIR (no real session, no real hyprctl):Required change
State::default()startsnext_idat 0 (display_guard.rs ~106-108, id taken at ~481-485), so every run's first change is id 1.Sequence: run A applies change id 1 and is SIGKILLed; its watchdog W1 (id 1) waits for the deadline. The user restarts the app inside the window.
recover_at_startcorrectly does nothing (deadline not passed), then the user applies a new change, which is again id 1.arm_rescue(display_guard.rs ~426-447) overwrites the stale record without looking at it, so:a) the record's
beforebecomes the still-unconfirmed layout from run A, and the original good layout is lost, so Revert, timeout or a second kill restores the unconfirmed layout;b) W1 now matches the new record by id and treats it as its own.
I reproduced b) in a scratch test (not committed): with the new change at id 1 and an expired deadline,
check_once(.., Some(1), ..)from the old watchdog acted on the new record and removed it.Fix: make ids unique across processes (for example start with
now_ms()or combine pid and counter) AND makeapplydeal with an existing record from a different owner before overwriting it (restore it if due, or refuse with a clear message if its owner is alive). Add a test for "stale record from a dead run, new change in the same window".Non-blocking notes
owner_alive(display_rescue.rs 68-77): after a package update replaces the binary, a live owner's exe reads.../bos-settings (deleted), so a freshly started watchdog treats it as dead and restores inside the grace period (I reproduced this with a replaced binary). Practical risk is small because spawning from a deleted exe fails and apply then changes nothing, but stripping the " (deleted)" suffix before comparing would close it.spawn_watchdogdrops theChildwithout waiting, so each finished watchdog stays a zombie until the app exits. Cheap to fix by moving the Child into a short reaper thread.DisplayGuard::livesilently skips rescue when XDG_RUNTIME_DIR is unset, so there is no protection and no error. Consider refusing the change, in line with "failure to set it up changes nothing".disarm_rescueruns after a failed restore too (failed apply,RevertFailed), so the watchdog cannot act as a last-resort retry. This matches the stated criterion that a failed apply is never undone later, so I am not asking for a change.CI: check green on
44d5438. Not merging because of item 1.Addressed review 4918 in
726ddb9:next_idis now seeded from the clock (ms x 1000), so a restarted app never reuses an id.applycallssettle_foreign_record()before reading the layout:beforelayout is restored first and the record cleared. Its watchdog then finds nothing and exits.applyrefuses ("Another BOS Settings window is waiting...") and touches nothing.restart_inside_a_dead_runs_window_keeps_both_safefails on44d5438and passes now.a_live_other_window_blocks_a_new_changeis also new.(deleted)now counts as the same program (same_program, with a test).XDG_RUNTIME_DIRis now logged.The live SIGKILL check in the BOS VM is still outstanding.
Re-review at exact head
726ddb9eac(base main7c7749d, unchanged). Verdict: PASS.Blocking defect from comment 4918: fixed.
beforerestored and cleared; a live owner makes apply refuse without touching the compositor.Non-blocking items: fixed.
New-risk review: settle_foreign_record runs under the op lock, but apply already runs eval plus confirm under that lock, so restoring a stale layout there adds at most one 5s hyprctl call. It runs before any state change or id allocation, so a refusal leaves no pending change, no record write and no counter change. If the foreign restore fails, apply stops and leaves that record in place, so its watchdog can still retry. Minor, not blocking: a stale record whose pid equals the new process's own pid is skipped as "ours" (pid reuse by the same binary), which is very unlikely.
Checks on this exact head:
check / check (push): success on726ddb9.Still outstanding after merge, for the coordinator: a live SIGKILL in the BOS VM.