Merge refactor/qbz-app-boot-watchdog: boot watchdog

cx
This commit is contained in:
vicrodh committed 2026-05-16 09:37:18 -06:00
commit 420fd4d286
3 files changed
+427 -211

No files matched your search

+413
View File
@@ -0,0 +1,413 @@
//! Framework-agnostic boot watchdog decision logic.
//!
//! The Tauri shell owns marker-file IO, settings persistence, and startup env
//! mutation. This module owns the pure crash-recovery state transition that can
//! be reused by any future native shell.
use serde::{Deserialize, Serialize};
/// Snapshot of the risky settings attempted in a boot. Compared against the
/// previous boot's snapshot to decide which setting "caused" a crash.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct BootAttempt {
pub hardware_acceleration: bool,
pub force_dmabuf: bool,
pub preferred_gpu: String,
pub force_x11: bool,
}
/// Sticky flags written when a setting was auto-reverted after a crash.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct CrashFlags {
/// Last boot crashed with hardware_acceleration ON; we reverted it.
pub hardware_acceleration_disabled: bool,
pub hardware_acceleration_consecutive_failures: u8,
/// Same for force_dmabuf.
pub force_dmabuf_disabled: bool,
pub force_dmabuf_consecutive_failures: u8,
/// Same for preferred_gpu (reset to "auto").
pub preferred_gpu_disabled: bool,
pub preferred_gpu_consecutive_failures: u8,
/// Lockout - set when consecutive_failures >= 2 for a given knob.
pub hardware_acceleration_locked: bool,
pub force_dmabuf_locked: bool,
pub preferred_gpu_locked: bool,
}
/// Resolved settings the host shell should actually use for this boot.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WatchdogResolution {
pub hardware_acceleration: bool,
pub force_dmabuf: bool,
pub preferred_gpu: String,
/// Reverted-in-this-boot messages so the UI knows which setting was
/// just rolled back. Persisted in CrashFlags as well.
pub recovery_messages: Vec<String>,
}
/// Pure output for a boot watchdog decision. The host adapter decides how to
/// persist marker files and settings writes from this value.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BootWatchdogDecision {
pub resolved_attempt: BootAttempt,
pub crash_flags: CrashFlags,
pub recovery_messages: Vec<String>,
pub should_persist_reverts: bool,
}
impl BootWatchdogDecision {
pub fn resolution(&self) -> WatchdogResolution {
WatchdogResolution {
hardware_acceleration: self.resolved_attempt.hardware_acceleration,
force_dmabuf: self.resolved_attempt.force_dmabuf,
preferred_gpu: self.resolved_attempt.preferred_gpu.clone(),
recovery_messages: self.recovery_messages.clone(),
}
}
}
pub fn resolve_boot_attempt(
intended: BootAttempt,
pending: Option<BootAttempt>,
mut flags: CrashFlags,
) -> BootWatchdogDecision {
let mut resolved = intended.clone();
let mut messages: Vec<String> = Vec::new();
let mut any_revert = false;
if let Some(prev) = pending {
if prev.hardware_acceleration && resolved.hardware_acceleration {
flags.hardware_acceleration_consecutive_failures = flags
.hardware_acceleration_consecutive_failures
.saturating_add(1);
resolved.hardware_acceleration = false;
flags.hardware_acceleration_disabled = true;
if flags.hardware_acceleration_consecutive_failures >= 2 {
flags.hardware_acceleration_locked = true;
}
messages.push(
"Hardware acceleration was disabled because the previous launch crashed during \
graphics init."
.to_string(),
);
any_revert = true;
}
if prev.force_dmabuf && resolved.force_dmabuf {
flags.force_dmabuf_consecutive_failures =
flags.force_dmabuf_consecutive_failures.saturating_add(1);
resolved.force_dmabuf = false;
flags.force_dmabuf_disabled = true;
if flags.force_dmabuf_consecutive_failures >= 2 {
flags.force_dmabuf_locked = true;
}
messages.push(
"DMA-BUF renderer was disabled because the previous launch crashed during \
graphics init."
.to_string(),
);
any_revert = true;
}
if prev.preferred_gpu == resolved.preferred_gpu && resolved.preferred_gpu != "auto" {
flags.preferred_gpu_consecutive_failures =
flags.preferred_gpu_consecutive_failures.saturating_add(1);
resolved.preferred_gpu = "auto".to_string();
flags.preferred_gpu_disabled = true;
if flags.preferred_gpu_consecutive_failures >= 2 {
flags.preferred_gpu_locked = true;
}
messages.push(format!(
"Rendering GPU preference was reset to Auto because pinning to {:?} crashed the \
previous launch.",
prev.preferred_gpu
));
any_revert = true;
}
if any_revert {
if resolved.hardware_acceleration {
resolved.hardware_acceleration = false;
flags.hardware_acceleration_disabled = true;
messages.push(
"Hardware acceleration was also disabled as a safety fallback to land in \
CPU mode."
.to_string(),
);
}
if resolved.force_dmabuf {
resolved.force_dmabuf = false;
flags.force_dmabuf_disabled = true;
}
if resolved.preferred_gpu != "auto" {
resolved.preferred_gpu = "auto".to_string();
flags.preferred_gpu_disabled = true;
}
}
} else {
if !flags.hardware_acceleration_locked {
flags.hardware_acceleration_disabled = false;
flags.hardware_acceleration_consecutive_failures = 0;
}
if !flags.force_dmabuf_locked {
flags.force_dmabuf_disabled = false;
flags.force_dmabuf_consecutive_failures = 0;
}
if !flags.preferred_gpu_locked {
flags.preferred_gpu_disabled = false;
flags.preferred_gpu_consecutive_failures = 0;
}
}
let should_persist_reverts = resolved != intended;
BootWatchdogDecision {
resolved_attempt: resolved,
crash_flags: flags,
recovery_messages: messages,
should_persist_reverts,
}
}
pub fn mark_boot_success_flags(attempt: &BootAttempt, mut flags: CrashFlags) -> CrashFlags {
if attempt.hardware_acceleration {
flags.hardware_acceleration_consecutive_failures = 0;
flags.hardware_acceleration_disabled = false;
flags.hardware_acceleration_locked = false;
}
if attempt.force_dmabuf {
flags.force_dmabuf_consecutive_failures = 0;
flags.force_dmabuf_disabled = false;
flags.force_dmabuf_locked = false;
}
if attempt.preferred_gpu != "auto" {
flags.preferred_gpu_consecutive_failures = 0;
flags.preferred_gpu_disabled = false;
flags.preferred_gpu_locked = false;
}
flags
}
pub fn clear_crash_flag_value(mut flags: CrashFlags, flag: &str) -> Option<CrashFlags> {
match flag {
"hardware_acceleration" => {
flags.hardware_acceleration_disabled = false;
flags.hardware_acceleration_consecutive_failures = 0;
flags.hardware_acceleration_locked = false;
}
"force_dmabuf" => {
flags.force_dmabuf_disabled = false;
flags.force_dmabuf_consecutive_failures = 0;
flags.force_dmabuf_locked = false;
}
"preferred_gpu" => {
flags.preferred_gpu_disabled = false;
flags.preferred_gpu_consecutive_failures = 0;
flags.preferred_gpu_locked = false;
}
_ => return None,
}
Some(flags)
}
#[cfg(test)]
mod tests {
use super::*;
fn attempt() -> BootAttempt {
BootAttempt {
hardware_acceleration: true,
force_dmabuf: true,
preferred_gpu: "auto".to_string(),
force_x11: false,
}
}
#[test]
fn no_pending_returns_intended_and_clears_non_locked_flags() {
let flags = CrashFlags {
hardware_acceleration_disabled: true,
hardware_acceleration_consecutive_failures: 1,
force_dmabuf_disabled: true,
force_dmabuf_consecutive_failures: 1,
preferred_gpu_disabled: true,
preferred_gpu_consecutive_failures: 1,
..CrashFlags::default()
};
let decision = resolve_boot_attempt(attempt(), None, flags);
assert_eq!(decision.resolved_attempt, attempt());
assert!(!decision.should_persist_reverts);
assert_eq!(decision.crash_flags, CrashFlags::default());
}
#[test]
fn no_pending_preserves_locked_flags() {
let flags = CrashFlags {
hardware_acceleration_disabled: true,
hardware_acceleration_consecutive_failures: 2,
hardware_acceleration_locked: true,
..CrashFlags::default()
};
let decision = resolve_boot_attempt(attempt(), None, flags);
assert!(decision.crash_flags.hardware_acceleration_disabled);
assert_eq!(
decision
.crash_flags
.hardware_acceleration_consecutive_failures,
2
);
assert!(decision.crash_flags.hardware_acceleration_locked);
}
#[test]
fn pending_hardware_acceleration_crash_disables_hardware_acceleration() {
let decision = resolve_boot_attempt(attempt(), Some(attempt()), CrashFlags::default());
assert!(!decision.resolved_attempt.hardware_acceleration);
assert!(!decision.resolved_attempt.force_dmabuf);
assert_eq!(decision.resolved_attempt.preferred_gpu, "auto");
assert!(decision.crash_flags.hardware_acceleration_disabled);
assert_eq!(
decision
.crash_flags
.hardware_acceleration_consecutive_failures,
1
);
assert!(decision.should_persist_reverts);
}
#[test]
fn pending_dmabuf_crash_disables_dmabuf() {
let mut intended = attempt();
intended.hardware_acceleration = false;
let pending = intended.clone();
let decision = resolve_boot_attempt(intended, Some(pending), CrashFlags::default());
assert!(!decision.resolved_attempt.force_dmabuf);
assert!(decision.crash_flags.force_dmabuf_disabled);
assert_eq!(decision.crash_flags.force_dmabuf_consecutive_failures, 1);
assert!(decision.should_persist_reverts);
}
#[test]
fn pending_same_preferred_gpu_resets_to_auto() {
let mut intended = attempt();
intended.hardware_acceleration = false;
intended.force_dmabuf = false;
intended.preferred_gpu = "0000:01:00.0".to_string();
let pending = intended.clone();
let decision = resolve_boot_attempt(intended, Some(pending), CrashFlags::default());
assert_eq!(decision.resolved_attempt.preferred_gpu, "auto");
assert!(decision.crash_flags.preferred_gpu_disabled);
assert_eq!(decision.crash_flags.preferred_gpu_consecutive_failures, 1);
assert!(decision.should_persist_reverts);
}
#[test]
fn pending_different_preferred_gpu_does_not_blame_gpu_choice() {
let mut intended = attempt();
intended.hardware_acceleration = false;
intended.force_dmabuf = false;
intended.preferred_gpu = "0000:02:00.0".to_string();
let mut pending = intended.clone();
pending.preferred_gpu = "0000:01:00.0".to_string();
let decision = resolve_boot_attempt(intended.clone(), Some(pending), CrashFlags::default());
assert_eq!(decision.resolved_attempt, intended);
assert!(!decision.crash_flags.preferred_gpu_disabled);
assert!(!decision.should_persist_reverts);
}
#[test]
fn second_failure_locks_setting() {
let flags = CrashFlags {
hardware_acceleration_consecutive_failures: 1,
..CrashFlags::default()
};
let decision = resolve_boot_attempt(attempt(), Some(attempt()), flags);
assert_eq!(
decision
.crash_flags
.hardware_acceleration_consecutive_failures,
2
);
assert!(decision.crash_flags.hardware_acceleration_locked);
}
#[test]
fn success_with_enabled_knobs_clears_matching_flags() {
let flags = CrashFlags {
hardware_acceleration_disabled: true,
hardware_acceleration_consecutive_failures: 2,
hardware_acceleration_locked: true,
force_dmabuf_disabled: true,
force_dmabuf_consecutive_failures: 2,
force_dmabuf_locked: true,
preferred_gpu_disabled: true,
preferred_gpu_consecutive_failures: 2,
preferred_gpu_locked: true,
};
let mut boot = attempt();
boot.preferred_gpu = "0000:01:00.0".to_string();
let updated = mark_boot_success_flags(&boot, flags);
assert_eq!(updated, CrashFlags::default());
}
#[test]
fn success_with_cpu_fallback_does_not_clear_disabled_flags() {
let flags = CrashFlags {
hardware_acceleration_disabled: true,
hardware_acceleration_consecutive_failures: 1,
force_dmabuf_disabled: true,
force_dmabuf_consecutive_failures: 1,
preferred_gpu_disabled: true,
preferred_gpu_consecutive_failures: 1,
..CrashFlags::default()
};
let boot = BootAttempt {
hardware_acceleration: false,
force_dmabuf: false,
preferred_gpu: "auto".to_string(),
force_x11: false,
};
let updated = mark_boot_success_flags(&boot, flags.clone());
assert_eq!(updated, flags);
}
#[test]
fn clear_known_flag_resets_disabled_counter_and_lock() {
let flags = CrashFlags {
force_dmabuf_disabled: true,
force_dmabuf_consecutive_failures: 2,
force_dmabuf_locked: true,
..CrashFlags::default()
};
let updated = clear_crash_flag_value(flags, "force_dmabuf").expect("known flag");
assert!(!updated.force_dmabuf_disabled);
assert_eq!(updated.force_dmabuf_consecutive_failures, 0);
assert!(!updated.force_dmabuf_locked);
}
#[test]
fn clear_unknown_flag_is_noop() {
assert_eq!(
clear_crash_flag_value(CrashFlags::default(), "force_x11"),
None
);
}
}
+1
View File
@@ -1,3 +1,4 @@
pub mod boot_watchdog;
pub mod graphics_autoconfig;
pub mod graphics_detection;
pub mod runtime;
+13 -211
View File
@@ -22,7 +22,10 @@
//! state: the setting stays disabled until the user explicitly clears
//! the flag from Settings. Prevents an infinite retry loop.
use serde::{Deserialize, Serialize};
use qbz_app::boot_watchdog::{
clear_crash_flag_value, mark_boot_success_flags, resolve_boot_attempt,
};
pub use qbz_app::boot_watchdog::{BootAttempt, CrashFlags, WatchdogResolution};
use std::fs;
use std::path::PathBuf;
@@ -31,52 +34,6 @@ const PENDING_FILE: &str = "pending.json";
const LAST_GOOD_FILE: &str = "last-good.json";
const CRASH_FLAGS_FILE: &str = "crash-flags.json";
/// Snapshot of the risky settings attempted in a boot. Compared against
/// the previous boot's snapshot to decide which setting "caused" a crash.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct BootAttempt {
pub hardware_acceleration: bool,
pub force_dmabuf: bool,
pub preferred_gpu: String,
pub force_x11: bool,
}
/// Sticky flags written when a setting was auto-reverted after a crash.
/// The UI reads these to render the recovery banner. Frontend can also
/// clear them via `v2_clear_crash_recovery_flag`.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CrashFlags {
/// Last boot crashed with hardware_acceleration ON; we reverted it.
pub hardware_acceleration_disabled: bool,
pub hardware_acceleration_consecutive_failures: u8,
/// Same for force_dmabuf.
pub force_dmabuf_disabled: bool,
pub force_dmabuf_consecutive_failures: u8,
/// Same for preferred_gpu (reset to "auto").
pub preferred_gpu_disabled: bool,
pub preferred_gpu_consecutive_failures: u8,
/// Lockout — set when consecutive_failures >= 2 for a given knob.
/// The runtime continues to revert; the UI shows a stronger
/// "disabled due to repeated crashes" message and gates re-enable
/// behind an explicit clear action.
pub hardware_acceleration_locked: bool,
pub force_dmabuf_locked: bool,
pub preferred_gpu_locked: bool,
}
/// Result of `before_webkit_init`. The caller (main.rs) replaces its
/// own variables for hardware_accel / force_dmabuf / preferred_gpu
/// with these values before proceeding to apply env vars.
#[derive(Debug, Clone)]
pub struct WatchdogResolution {
pub hardware_acceleration: bool,
pub force_dmabuf: bool,
pub preferred_gpu: String,
/// Reverted-in-this-boot flags so the UI knows which setting was
/// just rolled back. Persisted in CrashFlags as well.
pub recovery_messages: Vec<String>,
}
fn state_dir() -> Option<PathBuf> {
let base = dirs::data_dir()?.join("qbz").join(STATE_DIR);
fs::create_dir_all(&base).ok()?;
@@ -141,135 +98,14 @@ fn write_crash_flags(flags: &CrashFlags) {
/// Also writes the new pending marker for THIS boot so a crash before
/// the frontend signals success is detectable on the next launch.
pub fn before_webkit_init(intended: BootAttempt) -> WatchdogResolution {
let mut flags = read_crash_flags();
let mut resolved = intended.clone();
let mut messages: Vec<String> = Vec::new();
// If a previous boot left a pending marker, it crashed before
// first paint. Compare what it tried against what we'd try now,
// and revert the field(s) that look like the cause.
let mut any_revert = false;
if let Some(prev) = read_pending() {
// Hardware acceleration: if previous tried ON and we're also
// trying ON, blame this knob and revert.
if prev.hardware_acceleration && resolved.hardware_acceleration {
flags.hardware_acceleration_consecutive_failures =
flags.hardware_acceleration_consecutive_failures.saturating_add(1);
resolved.hardware_acceleration = false;
flags.hardware_acceleration_disabled = true;
if flags.hardware_acceleration_consecutive_failures >= 2 {
flags.hardware_acceleration_locked = true;
}
messages.push(
"Hardware acceleration was disabled because the previous launch crashed during \
graphics init."
.to_string(),
);
any_revert = true;
}
// Force DMA-BUF: same logic.
if prev.force_dmabuf && resolved.force_dmabuf {
flags.force_dmabuf_consecutive_failures =
flags.force_dmabuf_consecutive_failures.saturating_add(1);
resolved.force_dmabuf = false;
flags.force_dmabuf_disabled = true;
if flags.force_dmabuf_consecutive_failures >= 2 {
flags.force_dmabuf_locked = true;
}
messages.push(
"DMA-BUF renderer was disabled because the previous launch crashed during \
graphics init."
.to_string(),
);
any_revert = true;
}
// Preferred GPU: only revert when the SAME non-auto choice
// was attempted. Switching from one explicit GPU to another
// shouldn't inherit a crash flag from the prior choice.
if prev.preferred_gpu == resolved.preferred_gpu && resolved.preferred_gpu != "auto" {
flags.preferred_gpu_consecutive_failures =
flags.preferred_gpu_consecutive_failures.saturating_add(1);
resolved.preferred_gpu = "auto".to_string();
flags.preferred_gpu_disabled = true;
if flags.preferred_gpu_consecutive_failures >= 2 {
flags.preferred_gpu_locked = true;
}
messages.push(format!(
"Rendering GPU preference was reset to Auto because pinning to {:?} crashed the \
previous launch.",
prev.preferred_gpu
));
any_revert = true;
}
// Full CPU fallback after ANY revert. The single-knob revert
// above narrows down the *cause* for the UI banner, but the
// safest landing pad for a boot that just crashed in graphics
// init is full CPU mode (hardware_acceleration off, DMA-BUF
// off, preferred_gpu cleared to auto). A partial revert can
// still hit the next crash if the failure was a stack-wide
// issue (driver bug, missing extension) and only one symptom
// was the visible one. Going all-the-way-CPU guarantees the
// user always reaches a paintable window after a recovery —
// they can selectively re-enable from the Settings banner.
if any_revert {
if resolved.hardware_acceleration {
resolved.hardware_acceleration = false;
flags.hardware_acceleration_disabled = true;
messages.push(
"Hardware acceleration was also disabled as a safety fallback to land in \
CPU mode."
.to_string(),
);
}
if resolved.force_dmabuf {
resolved.force_dmabuf = false;
flags.force_dmabuf_disabled = true;
}
if resolved.preferred_gpu != "auto" {
resolved.preferred_gpu = "auto".to_string();
flags.preferred_gpu_disabled = true;
}
}
// Persist the revert decisions back to graphics_settings.db so
// the user sees the new state in the UI and the next boot
// doesn't try the same setting again.
persist_reverts_to_db(&resolved, &intended);
// Clear consecutive-failure counters for any knob that was
// attempted AND succeeded (we infer "succeeded" from "this
// boot's input matches last-good"). Handled in
// mark_boot_succeeded for paths that actually paint.
} else {
// No pending marker → previous boot either succeeded or never
// ran the watchdog. Reset disabled flags to non-sticky values
// so the UI doesn't keep showing a banner across launches
// where everything's fine. Locked flags STAY sticky — those
// require explicit user clear.
if !flags.hardware_acceleration_locked {
flags.hardware_acceleration_disabled = false;
flags.hardware_acceleration_consecutive_failures = 0;
}
if !flags.force_dmabuf_locked {
flags.force_dmabuf_disabled = false;
flags.force_dmabuf_consecutive_failures = 0;
}
if !flags.preferred_gpu_locked {
flags.preferred_gpu_disabled = false;
flags.preferred_gpu_consecutive_failures = 0;
}
let decision = resolve_boot_attempt(intended.clone(), read_pending(), read_crash_flags());
if decision.should_persist_reverts {
persist_reverts_to_db(&decision.resolved_attempt, &intended);
}
write_crash_flags(&flags);
write_pending(&resolved);
WatchdogResolution {
hardware_acceleration: resolved.hardware_acceleration,
force_dmabuf: resolved.force_dmabuf,
preferred_gpu: resolved.preferred_gpu,
recovery_messages: messages,
}
write_crash_flags(&decision.crash_flags);
write_pending(&decision.resolved_attempt);
decision.resolution()
}
/// Called from a Tauri command after the frontend signals first paint.
@@ -284,24 +120,7 @@ pub fn mark_boot_succeeded() {
clear_pending();
write_last_good(&attempt);
let mut flags = read_crash_flags();
// Successful paint with HW accel ON resets that knob's failure
// streak entirely (including lockout — the issue resolved).
if attempt.hardware_acceleration {
flags.hardware_acceleration_consecutive_failures = 0;
flags.hardware_acceleration_disabled = false;
flags.hardware_acceleration_locked = false;
}
if attempt.force_dmabuf {
flags.force_dmabuf_consecutive_failures = 0;
flags.force_dmabuf_disabled = false;
flags.force_dmabuf_locked = false;
}
if attempt.preferred_gpu != "auto" {
flags.preferred_gpu_consecutive_failures = 0;
flags.preferred_gpu_disabled = false;
flags.preferred_gpu_locked = false;
}
let flags = mark_boot_success_flags(&attempt, read_crash_flags());
write_crash_flags(&flags);
}
@@ -314,26 +133,9 @@ pub fn get_crash_flags() -> CrashFlags {
/// the user clicks "Try again" / "I want to re-enable this" on the
/// recovery banner. Unknown names are no-ops.
pub fn clear_crash_flag(flag: &str) {
let mut flags = read_crash_flags();
match flag {
"hardware_acceleration" => {
flags.hardware_acceleration_disabled = false;
flags.hardware_acceleration_consecutive_failures = 0;
flags.hardware_acceleration_locked = false;
}
"force_dmabuf" => {
flags.force_dmabuf_disabled = false;
flags.force_dmabuf_consecutive_failures = 0;
flags.force_dmabuf_locked = false;
}
"preferred_gpu" => {
flags.preferred_gpu_disabled = false;
flags.preferred_gpu_consecutive_failures = 0;
flags.preferred_gpu_locked = false;
}
_ => return,
if let Some(flags) = clear_crash_flag_value(read_crash_flags(), flag) {
write_crash_flags(&flags);
}
write_crash_flags(&flags);
}
/// Best-effort write of reverted values back to graphics_settings.db