509552725f
Splits `reap_pid` into the `reap_pid_inner(crashed: bool)` private helper plus two public entry points: - `reap_pid(pid)` — crash path. Calls `crash_tracker().record_failure` after the maps are cleared, which advances the auto-blacklist counter. - `reap_pid_clean(pid)` — clean exit path. Calls `crash_tracker().record_success` so the device's backoff is reset; a future crash starts the count from 1, not from where the previous clean-exit backoff ended. Both paths also call `error_channel::global().remove(&key)` to close the sidecar UnixStream fd (Bug4: each crash previously leaked one fd; ~1024 crashes exhausted the manager's fd table). This is the kernel of N18-Q2+Bug4; the call-site change in `main.rs` that interprets the waitpid status was committed in the prior Q1+Q2+S3 commit.
2051 lines
73 KiB
Rust
2051 lines
73 KiB
Rust
use std::collections::HashMap;
|
||
use std::fs;
|
||
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
|
||
use std::path::Path;
|
||
use std::process::Command;
|
||
use std::string::String;
|
||
use std::sync::Mutex;
|
||
use std::thread;
|
||
use std::time::{Duration, Instant};
|
||
use std::vec::Vec;
|
||
|
||
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
|
||
|
||
use pcid_interface::PciFunctionHandle;
|
||
use redox_driver_core::device::DeviceInfo;
|
||
use redox_driver_core::driver::{Driver, DriverError, ErrorSeverity, ProbeResult, RecoveryAction};
|
||
use redox_driver_core::r#match::DriverMatch;
|
||
use redox_driver_core::params::{DriverParams, ParamValue};
|
||
|
||
use serde::Deserialize;
|
||
|
||
const SIGTERM: i32 = 15;
|
||
const SIGKILL: i32 = 9;
|
||
const GRACE_PERIOD_MS: u64 = 3000;
|
||
const POLL_INTERVAL_MS: u64 = 50;
|
||
|
||
/// Default crash-threshold (consecutive spawn failures before a driver
|
||
/// is auto-blacklisted for this BDF). Overridable via
|
||
/// `REDBEAR_DRIVER_CRASH_THRESHOLD`; see `crash_tracker` below.
|
||
const DEFAULT_CRASH_THRESHOLD: u32 = 5;
|
||
const DEFAULT_BACKOFF_BASE_MS: u64 = 100;
|
||
const DEFAULT_BACKOFF_CAP_MS: u64 = 30_000;
|
||
|
||
#[derive(Debug)]
|
||
struct SpawnedDriver {
|
||
pid: u32,
|
||
channel_fd: OwnedFd,
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
pub struct DriverConfig {
|
||
pub name: String,
|
||
pub description: String,
|
||
pub priority: i32,
|
||
pub command: Vec<String>,
|
||
pub matches: Vec<DriverMatch>,
|
||
pub depends_on: Vec<String>,
|
||
pub exclusive_with: Vec<String>,
|
||
/// Per-driver parameter overrides from the policy layer's
|
||
/// `[driver.params]` table. Each `(name, value)` pair is emitted
|
||
/// as `REDBEAR_DRIVER_PARAM_<NAME>=<value>` in the spawn env.
|
||
/// This is the wiring for `redbear-driver-policy`'s `50-amdgpu.toml`
|
||
/// style per-driver knobs (radeon_overlap, amdgpu_force,
|
||
/// amdgpu_disable_accel, etc.).
|
||
pub params: Vec<(String, String)>,
|
||
/// Initial PCI power state for the spawned driver daemon. D0 is the
|
||
/// default and matches the post-`enable_device` state of every bound
|
||
/// device. D3hot / D3cold can be set for low-power drivers (e.g.
|
||
/// always-on housekeeping devices) that wake the bus on demand.
|
||
/// When non-D0, driver-manager emits
|
||
/// `REDBEAR_DRIVER_INITIAL_POWER_STATE=<state>` in the child's env so
|
||
/// the daemon can call `set_power_state` on its granted channel.
|
||
pub initial_power_state: PciPowerState,
|
||
spawned: Mutex<HashMap<String, SpawnedDriver>>,
|
||
pid_to_device: Mutex<HashMap<u32, String>>,
|
||
}
|
||
|
||
/// PCI power state for driver spawn. Mirrors the subset of
|
||
/// `pci_power_state` (include/linux/pci.h) that Red Bear OS uses at
|
||
/// boot: D0 (fully on, default) and D3hot (software-visible but
|
||
/// not consuming bus power).
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub enum PciPowerState {
|
||
D0,
|
||
D3hot,
|
||
}
|
||
|
||
impl PciPowerState {
|
||
pub fn from_toml(s: &str) -> Result<Self, String> {
|
||
match s {
|
||
"D0" => Ok(Self::D0),
|
||
"D3hot" => Ok(Self::D3hot),
|
||
other => Err(format!(
|
||
"invalid initial_power_state {:?}: expected D0 or D3hot",
|
||
other
|
||
)),
|
||
}
|
||
}
|
||
|
||
pub fn as_str(self) -> &'static str {
|
||
match self {
|
||
Self::D0 => "D0",
|
||
Self::D3hot => "D3hot",
|
||
}
|
||
}
|
||
|
||
pub fn is_default(self) -> bool {
|
||
matches!(self, Self::D0)
|
||
}
|
||
}
|
||
|
||
impl Clone for DriverConfig {
|
||
fn clone(&self) -> Self {
|
||
DriverConfig {
|
||
name: self.name.clone(),
|
||
description: self.description.clone(),
|
||
priority: self.priority,
|
||
command: self.command.clone(),
|
||
matches: self.matches.clone(),
|
||
depends_on: self.depends_on.clone(),
|
||
exclusive_with: self.exclusive_with.clone(),
|
||
params: self.params.clone(),
|
||
initial_power_state: self.initial_power_state,
|
||
spawned: Mutex::new(HashMap::new()),
|
||
pid_to_device: Mutex::new(HashMap::new()),
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Deserialize)]
|
||
struct RawDriverMatch {
|
||
vendor: Option<u16>,
|
||
device: Option<u16>,
|
||
class: Option<u8>,
|
||
subclass: Option<u8>,
|
||
prog_if: Option<u8>,
|
||
subsystem_vendor: Option<u16>,
|
||
subsystem_device: Option<u16>,
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use std::collections::HashMap;
|
||
|
||
use super::{
|
||
is_process_alive, signal_all_spawned, signal_then_collect, spawned_pids_snapshot,
|
||
DriverConfig, PciPowerState,
|
||
};
|
||
use std::collections::BTreeMap;
|
||
use std::sync::Mutex;
|
||
use std::time::Instant;
|
||
|
||
static ENV_LOCK: Mutex<()> = Mutex::new(());
|
||
|
||
fn serialized<R>(body: impl FnOnce() -> R) -> R {
|
||
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||
body()
|
||
}
|
||
|
||
#[test]
|
||
fn load_all_accepts_matchless_driver_entries() {
|
||
serialized(|| {
|
||
let dir = std::env::temp_dir().join(format!(
|
||
"rb-dm-matchless-{}-{}",
|
||
std::process::id(),
|
||
std::time::SystemTime::now()
|
||
.duration_since(std::time::UNIX_EPOCH)
|
||
.unwrap()
|
||
.as_nanos()
|
||
));
|
||
std::fs::create_dir_all(&dir).unwrap();
|
||
std::fs::write(
|
||
dir.join("70-usb-class.toml"),
|
||
r#"
|
||
[[driver]]
|
||
name = "redbear-acmd"
|
||
description = "USB CDC ACM serial driver"
|
||
priority = 70
|
||
command = ["/usr/bin/redbear-acmd"]
|
||
"#,
|
||
)
|
||
.unwrap();
|
||
let configs = DriverConfig::load_all(dir.to_str().unwrap())
|
||
.expect("matchless driver entries must load");
|
||
assert_eq!(configs.len(), 1);
|
||
assert_eq!(configs[0].name, "redbear-acmd");
|
||
assert!(configs[0].matches.is_empty());
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn is_process_alive_returns_false_for_unused_pid() {
|
||
let dead = u32::MAX;
|
||
assert!(
|
||
!is_process_alive(dead),
|
||
"PID {} should not be alive; got true",
|
||
dead
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn signal_then_collect_returns_ok_for_unused_pid() {
|
||
let r = signal_then_collect(u32::MAX, std::time::Duration::from_millis(100));
|
||
assert!(
|
||
r.is_ok(),
|
||
"signal_then_collect against an unused PID should converge immediately; got {:?}",
|
||
r
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn spawned_pids_snapshot_returns_empty_for_unbound_driver() {
|
||
let cfg = DriverConfig {
|
||
name: "unbound".to_string(),
|
||
description: String::new(),
|
||
priority: 0,
|
||
command: Vec::new(),
|
||
matches: Vec::new(),
|
||
depends_on: Vec::new(),
|
||
exclusive_with: Vec::new(),
|
||
params: Vec::new(),
|
||
initial_power_state: PciPowerState::D0,
|
||
spawned: Mutex::new(HashMap::new()),
|
||
pid_to_device: Mutex::new(HashMap::new()),
|
||
};
|
||
let pids = spawned_pids_snapshot(&cfg);
|
||
assert!(pids.is_empty(), "no spawned children => empty snapshot");
|
||
}
|
||
|
||
#[test]
|
||
fn signal_all_spawned_returns_zero_for_unbound_driver() {
|
||
let cfg = DriverConfig {
|
||
name: "unbound".to_string(),
|
||
description: String::new(),
|
||
priority: 0,
|
||
command: Vec::new(),
|
||
matches: Vec::new(),
|
||
depends_on: Vec::new(),
|
||
exclusive_with: Vec::new(),
|
||
params: Vec::new(),
|
||
initial_power_state: PciPowerState::D0,
|
||
spawned: Mutex::new(HashMap::new()),
|
||
pid_to_device: Mutex::new(HashMap::new()),
|
||
};
|
||
let n = signal_all_spawned(&cfg, libc::SIGTERM);
|
||
assert_eq!(n, 0, "no spawned children => zero signals sent");
|
||
}
|
||
|
||
#[test]
|
||
fn legacy_vendor_plus_device_converts_to_one_match() {
|
||
let toml_str = r#"
|
||
[[drivers]]
|
||
name = "rtl"
|
||
vendor = 0x10ec
|
||
device = 0x8168
|
||
class = 0x02
|
||
command = ["rtl8168d"]
|
||
"#;
|
||
let parsed: super::RawLegacyToml = toml::from_str(toml_str).expect("parse legacy");
|
||
let cfg = super::convert_legacy(parsed.drivers.into_iter().next().unwrap());
|
||
assert_eq!(cfg.name, "rtl");
|
||
assert_eq!(cfg.command, vec!["rtl8168d"]);
|
||
assert_eq!(cfg.matches.len(), 1);
|
||
assert_eq!(cfg.matches[0].vendor, Some(0x10ec));
|
||
assert_eq!(cfg.matches[0].device, Some(0x8168));
|
||
assert_eq!(cfg.matches[0].class, Some(0x02));
|
||
}
|
||
|
||
#[test]
|
||
fn legacy_ids_map_expands_to_multiple_matches() {
|
||
let mut ids = BTreeMap::new();
|
||
ids.insert("0x8086".to_string(), vec![0x100E, 0x10D3]);
|
||
let entry = super::RawLegacyEntry {
|
||
name: Some("igb".to_string()),
|
||
class: Some(0x02),
|
||
subclass: Some(0x00),
|
||
interface: None,
|
||
ids: Some(ids),
|
||
vendor: None,
|
||
device: None,
|
||
device_id_range: None,
|
||
exclusive_with: Vec::new(),
|
||
command: vec!["e1000d".to_string()],
|
||
};
|
||
let cfg = super::convert_legacy(entry);
|
||
assert_eq!(cfg.matches.len(), 2);
|
||
assert_eq!(cfg.matches[0].vendor, Some(0x8086));
|
||
assert_eq!(cfg.matches[0].device, Some(0x100E));
|
||
assert_eq!(cfg.matches[1].device, Some(0x10D3));
|
||
}
|
||
|
||
#[test]
|
||
fn legacy_device_id_range_expands_to_multiple_matches() {
|
||
let entry = super::RawLegacyEntry {
|
||
name: Some("range_dr".to_string()),
|
||
class: None,
|
||
subclass: None,
|
||
interface: None,
|
||
ids: None,
|
||
vendor: None,
|
||
device: None,
|
||
device_id_range: Some(super::RawLegacyRange { start: 0x1000, end: 0x1002 }),
|
||
exclusive_with: Vec::new(),
|
||
command: vec!["test_dr".to_string()],
|
||
};
|
||
let cfg = super::convert_legacy(entry);
|
||
assert_eq!(cfg.matches.len(), 3);
|
||
assert_eq!(cfg.matches[0].device, Some(0x1000));
|
||
assert_eq!(cfg.matches[2].device, Some(0x1002));
|
||
}
|
||
|
||
#[test]
|
||
fn empty_legacy_does_not_panic_falls_back_to_class() {
|
||
let entry = super::RawLegacyEntry {
|
||
name: Some("emptily".to_string()),
|
||
class: Some(0x06),
|
||
subclass: None,
|
||
interface: None,
|
||
ids: None,
|
||
vendor: None,
|
||
device: None,
|
||
device_id_range: None,
|
||
exclusive_with: Vec::new(),
|
||
command: vec!["x".to_string()],
|
||
};
|
||
let cfg = super::convert_legacy(entry);
|
||
assert_eq!(cfg.matches.len(), 1);
|
||
assert_eq!(cfg.matches[0].class, Some(0x06));
|
||
}
|
||
|
||
#[test]
|
||
fn pci_power_state_from_toml_round_trips() {
|
||
assert_eq!(
|
||
super::PciPowerState::from_toml("D0").unwrap(),
|
||
super::PciPowerState::D0
|
||
);
|
||
assert_eq!(
|
||
super::PciPowerState::from_toml("D3hot").unwrap(),
|
||
super::PciPowerState::D3hot
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn pci_power_state_from_toml_rejects_unknown_values() {
|
||
assert!(super::PciPowerState::from_toml("D1").is_err());
|
||
assert!(super::PciPowerState::from_toml("D2").is_err());
|
||
assert!(super::PciPowerState::from_toml("D3cold").is_err());
|
||
assert!(super::PciPowerState::from_toml("d0").is_err(), "case-sensitive");
|
||
assert!(super::PciPowerState::from_toml("").is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn pci_power_state_is_default_for_d0_only() {
|
||
assert!(super::PciPowerState::D0.is_default());
|
||
assert!(!super::PciPowerState::D3hot.is_default());
|
||
}
|
||
|
||
#[test]
|
||
fn pci_power_state_as_str_matches_linux_pci_power_state_names() {
|
||
assert_eq!(super::PciPowerState::D0.as_str(), "D0");
|
||
assert_eq!(super::PciPowerState::D3hot.as_str(), "D3hot");
|
||
}
|
||
|
||
#[test]
|
||
fn load_all_parses_initial_power_state() {
|
||
serialized(|| {
|
||
let dir = std::env::temp_dir().join(format!(
|
||
"rb-dm-power-state-{}-{}",
|
||
std::process::id(),
|
||
std::time::SystemTime::now()
|
||
.duration_since(std::time::UNIX_EPOCH)
|
||
.unwrap()
|
||
.as_nanos()
|
||
));
|
||
std::fs::create_dir_all(&dir).unwrap();
|
||
std::fs::write(
|
||
dir.join("20-pm.toml"),
|
||
r#"
|
||
[[driver]]
|
||
name = "watchdog"
|
||
priority = 50
|
||
command = ["/usr/bin/watchdogd"]
|
||
initial_power_state = "D3hot"
|
||
"#,
|
||
)
|
||
.unwrap();
|
||
let configs = DriverConfig::load_all(dir.to_str().unwrap())
|
||
.expect("initial_power_state must parse");
|
||
assert_eq!(configs.len(), 1);
|
||
assert_eq!(
|
||
configs[0].initial_power_state,
|
||
super::PciPowerState::D3hot
|
||
);
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn load_all_defaults_to_d0_when_initial_power_state_absent() {
|
||
serialized(|| {
|
||
let dir = std::env::temp_dir().join(format!(
|
||
"rb-dm-power-default-{}-{}",
|
||
std::process::id(),
|
||
std::time::SystemTime::now()
|
||
.duration_since(std::time::UNIX_EPOCH)
|
||
.unwrap()
|
||
.as_nanos()
|
||
));
|
||
std::fs::create_dir_all(&dir).unwrap();
|
||
std::fs::write(
|
||
dir.join("10-net.toml"),
|
||
r#"
|
||
[[driver]]
|
||
name = "e1000d"
|
||
priority = 80
|
||
command = ["e1000d"]
|
||
"#,
|
||
)
|
||
.unwrap();
|
||
let configs = DriverConfig::load_all(dir.to_str().unwrap())
|
||
.expect("missing initial_power_state must default");
|
||
assert_eq!(configs.len(), 1);
|
||
assert_eq!(
|
||
configs[0].initial_power_state,
|
||
super::PciPowerState::D0
|
||
);
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn load_all_parses_driver_params() {
|
||
// N8: per-driver [driver.params] table from redbear-driver-policy
|
||
// (e.g. 50-amdgpu.toml) — each entry becomes a param override
|
||
// that the spawn path emits as REDBEAR_DRIVER_PARAM_<NAME>=<value>.
|
||
serialized(|| {
|
||
let dir = std::env::temp_dir().join(format!(
|
||
"rb-dm-driver-params-{}-{}",
|
||
std::process::id(),
|
||
std::time::SystemTime::now()
|
||
.duration_since(std::time::UNIX_EPOCH)
|
||
.unwrap()
|
||
.as_nanos()
|
||
));
|
||
std::fs::create_dir_all(&dir).unwrap();
|
||
std::fs::write(
|
||
dir.join("50-amdgpu.toml"),
|
||
r#"
|
||
[[driver]]
|
||
name = "redox-drm"
|
||
priority = 95
|
||
command = ["/usr/bin/redox-drm"]
|
||
|
||
[driver.params]
|
||
radeon_overlap = "exclusive"
|
||
amdgpu_force = "true"
|
||
amdgpu_disable_accel = "false"
|
||
"#,
|
||
)
|
||
.unwrap();
|
||
let configs = DriverConfig::load_all(dir.to_str().unwrap())
|
||
.expect("[driver.params] must parse");
|
||
assert_eq!(configs.len(), 1);
|
||
let params = &configs[0].params;
|
||
assert_eq!(params.len(), 3);
|
||
// BTreeMap iteration is sorted by key.
|
||
assert_eq!(params[0].0, "amdgpu_disable_accel");
|
||
assert_eq!(params[0].1, "false");
|
||
assert_eq!(params[1].0, "amdgpu_force");
|
||
assert_eq!(params[1].1, "true");
|
||
assert_eq!(params[2].0, "radeon_overlap");
|
||
assert_eq!(params[2].1, "exclusive");
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn load_all_driver_params_default_empty() {
|
||
// A driver entry without [driver.params] should still parse
|
||
// and have an empty params list — drivers that don't need
|
||
// knobs aren't required to declare any.
|
||
serialized(|| {
|
||
let dir = std::env::temp_dir().join(format!(
|
||
"rb-dm-driver-params-empty-{}-{}",
|
||
std::process::id(),
|
||
std::time::SystemTime::now()
|
||
.duration_since(std::time::UNIX_EPOCH)
|
||
.unwrap()
|
||
.as_nanos()
|
||
));
|
||
std::fs::create_dir_all(&dir).unwrap();
|
||
std::fs::write(
|
||
dir.join("10-foo.toml"),
|
||
r#"
|
||
[[driver]]
|
||
name = "foo"
|
||
priority = 80
|
||
command = ["/usr/bin/foo"]
|
||
"#,
|
||
)
|
||
.unwrap();
|
||
let configs = DriverConfig::load_all(dir.to_str().unwrap()).unwrap();
|
||
assert_eq!(configs.len(), 1);
|
||
assert!(configs[0].params.is_empty());
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn load_all_warns_and_defaults_on_invalid_initial_power_state() {
|
||
serialized(|| {
|
||
let dir = std::env::temp_dir().join(format!(
|
||
"rb-dm-power-invalid-{}-{}",
|
||
std::process::id(),
|
||
std::time::SystemTime::now()
|
||
.duration_since(std::time::UNIX_EPOCH)
|
||
.unwrap()
|
||
.as_nanos()
|
||
));
|
||
std::fs::create_dir_all(&dir).unwrap();
|
||
std::fs::write(
|
||
dir.join("30-bad.toml"),
|
||
r#"
|
||
[[driver]]
|
||
name = "broken"
|
||
priority = 50
|
||
command = ["/usr/bin/broken"]
|
||
initial_power_state = "D2"
|
||
"#,
|
||
)
|
||
.unwrap();
|
||
let configs = DriverConfig::load_all(dir.to_str().unwrap())
|
||
.expect("invalid initial_power_state must not abort config loading");
|
||
assert_eq!(configs.len(), 1);
|
||
assert_eq!(
|
||
configs[0].initial_power_state,
|
||
super::PciPowerState::D0,
|
||
"invalid value should default to D0 with a WARN log"
|
||
);
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
});
|
||
}
|
||
|
||
// ── F1 crash-tracker tests ──────────────────────────────────────
|
||
|
||
#[test]
|
||
fn crash_tracker_record_failure_increments_and_signals_threshold() {
|
||
let tracker = super::CrashTracker::new();
|
||
let bdf = "0000:00:1f.2";
|
||
assert_eq!(tracker.record_failure(bdf), None, "1st failure < 5 threshold");
|
||
assert_eq!(tracker.record_failure(bdf), None, "2nd failure");
|
||
assert_eq!(tracker.record_failure(bdf), None, "3rd failure");
|
||
assert_eq!(tracker.record_failure(bdf), None, "4th failure");
|
||
let count = tracker.record_failure(bdf).expect("5th failure >= threshold");
|
||
assert_eq!(count, 5);
|
||
}
|
||
|
||
#[test]
|
||
fn crash_tracker_record_success_clears_state() {
|
||
let tracker = super::CrashTracker::new();
|
||
let bdf = "0000:00:1f.2";
|
||
tracker.record_failure(bdf);
|
||
tracker.record_failure(bdf);
|
||
assert!(tracker.is_in_backoff(bdf, Instant::now()));
|
||
tracker.record_success(bdf);
|
||
assert!(!tracker.is_in_backoff(bdf, Instant::now()));
|
||
}
|
||
|
||
#[test]
|
||
fn crash_tracker_is_in_backoff_returns_true_within_window() {
|
||
let tracker = super::CrashTracker::new();
|
||
let bdf = "0000:00:1f.2";
|
||
tracker.record_failure(bdf);
|
||
// Just after the failure, backoff is active.
|
||
assert!(
|
||
tracker.is_in_backoff(bdf, Instant::now()),
|
||
"a just-recorded failure must trigger backoff"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn crash_tracker_snapshot_returns_per_bdf_counts() {
|
||
let tracker = super::CrashTracker::new();
|
||
tracker.record_failure("0000:00:1f.2");
|
||
tracker.record_failure("0000:00:1f.2");
|
||
tracker.record_failure("0000:01:00.0");
|
||
let snap = tracker.snapshot();
|
||
assert_eq!(snap.len(), 2);
|
||
let mut counts: std::collections::HashMap<String, u32> = snap.into_iter().collect();
|
||
assert_eq!(counts.remove("0000:00:1f.2").unwrap(), 2);
|
||
assert_eq!(counts.remove("0000:01:00.0").unwrap(), 1);
|
||
}
|
||
|
||
#[test]
|
||
fn crash_tracker_apply_env_overrides() {
|
||
serialized(|| {
|
||
let tracker = super::CrashTracker::new();
|
||
unsafe { std::env::set_var("REDBEAR_DRIVER_CRASH_THRESHOLD", "3") };
|
||
unsafe { std::env::set_var("REDBEAR_DRIVER_BACKOFF_BASE_MS", "50") };
|
||
unsafe { std::env::set_var("REDBEAR_DRIVER_BACKOFF_CAP_MS", "500") };
|
||
tracker.apply_env();
|
||
assert_eq!(tracker.threshold(), 3);
|
||
assert_eq!(tracker.backoff_base_ms(), 50);
|
||
assert_eq!(tracker.backoff_cap_ms(), 500);
|
||
unsafe { std::env::remove_var("REDBEAR_DRIVER_CRASH_THRESHOLD") };
|
||
unsafe { std::env::remove_var("REDBEAR_DRIVER_BACKOFF_BASE_MS") };
|
||
unsafe { std::env::remove_var("REDBEAR_DRIVER_BACKOFF_CAP_MS") };
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn crash_tracker_apply_env_ignores_invalid_values() {
|
||
serialized(|| {
|
||
let tracker = super::CrashTracker::new();
|
||
let orig_threshold = tracker.threshold();
|
||
let orig_base = tracker.backoff_base_ms();
|
||
let orig_cap = tracker.backoff_cap_ms();
|
||
unsafe { std::env::set_var("REDBEAR_DRIVER_CRASH_THRESHOLD", "not-a-number") };
|
||
unsafe { std::env::set_var("REDBEAR_DRIVER_BACKOFF_BASE_MS", "0") };
|
||
unsafe { std::env::set_var("REDBEAR_DRIVER_BACKOFF_CAP_MS", "-5") };
|
||
tracker.apply_env();
|
||
assert_eq!(
|
||
tracker.threshold(),
|
||
orig_threshold,
|
||
"invalid threshold must be ignored"
|
||
);
|
||
assert_eq!(
|
||
tracker.backoff_base_ms(),
|
||
orig_base,
|
||
"zero base_ms must be ignored (zero is treated as invalid)"
|
||
);
|
||
assert_eq!(
|
||
tracker.backoff_cap_ms(),
|
||
orig_cap,
|
||
"negative cap_ms must be ignored (parse fails, default kept)"
|
||
);
|
||
unsafe { std::env::remove_var("REDBEAR_DRIVER_CRASH_THRESHOLD") };
|
||
unsafe { std::env::remove_var("REDBEAR_DRIVER_BACKOFF_BASE_MS") };
|
||
unsafe { std::env::remove_var("REDBEAR_DRIVER_BACKOFF_CAP_MS") };
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn crash_tracker_global_stub_returns_when_unset() {
|
||
// CrashTracker global pointer must not be set by other tests.
|
||
let snap = super::crash_tracker().snapshot();
|
||
assert!(
|
||
snap.is_empty(),
|
||
"fresh crash tracker snapshot should be empty"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn spawn_decision_defaults_to_spawn_without_env_or_flags() {
|
||
serialized(|| {
|
||
unsafe {
|
||
std::env::remove_var("REDBEAR_DRIVER_MANAGER_ACTIVE");
|
||
}
|
||
let prev: Vec<String> = std::env::args().collect();
|
||
if !prev.iter().any(|a| a == "--no-spawn") {
|
||
assert_eq!(super::spawn_decision_gate(), super::SpawnDecision::Spawn);
|
||
}
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn spawn_decision_rejects_when_env_zero() {
|
||
serialized(|| {
|
||
unsafe {
|
||
std::env::set_var("REDBEAR_DRIVER_MANAGER_ACTIVE", "0");
|
||
}
|
||
let decision = super::spawn_decision_gate();
|
||
unsafe {
|
||
std::env::remove_var("REDBEAR_DRIVER_MANAGER_ACTIVE");
|
||
}
|
||
match decision {
|
||
super::SpawnDecision::ObserveOnly(reason) => {
|
||
assert!(reason.contains("REDBEAR_DRIVER_MANAGER_ACTIVE=0"));
|
||
}
|
||
super::SpawnDecision::Spawn => panic!(
|
||
"env REDBEAR_DRIVER_MANAGER_ACTIVE=0 should be observed-only"
|
||
),
|
||
}
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn spawn_decision_accepts_env_one() {
|
||
serialized(|| {
|
||
unsafe {
|
||
std::env::set_var("REDBEAR_DRIVER_MANAGER_ACTIVE", "1");
|
||
}
|
||
let decision = super::spawn_decision_gate();
|
||
unsafe {
|
||
std::env::remove_var("REDBEAR_DRIVER_MANAGER_ACTIVE");
|
||
}
|
||
assert_eq!(decision, super::SpawnDecision::Spawn);
|
||
});
|
||
}
|
||
}
|
||
|
||
/// Send a Unix signal to a spawned child process by PID.
|
||
///
|
||
/// We invoke the external `kill(1)` utility rather than `libc::kill(2)` so
|
||
/// that this code compiles unchanged on Linux host (where we run unit tests
|
||
/// and the no-stubs audit) and Redox target (where the user-space kill
|
||
/// binary is provided by `redox-userspace` / busybox). Standard exit code:
|
||
/// `0` on success, non-zero (treated as Err) otherwise.
|
||
fn send_signal(pid: u32, signal: i32) -> Result<(), String> {
|
||
let status = Command::new("kill")
|
||
.arg(format!("-{}", signal))
|
||
.arg(pid.to_string())
|
||
.status();
|
||
match status {
|
||
Ok(s) if s.success() => Ok(()),
|
||
Ok(s) => Err(format!(
|
||
"kill -{} {} exited with status {}",
|
||
signal,
|
||
pid,
|
||
s.code().map(|c| c.to_string()).unwrap_or_else(|| "no-code".to_string())
|
||
)),
|
||
Err(e) => Err(format!("failed to spawn kill(1): {}", e)),
|
||
}
|
||
}
|
||
|
||
/// Probe whether a PID is still alive using `kill(pid, 0)` semantics. Returns
|
||
/// `true` if the process is alive, `false` if it has exited (or the kill probe
|
||
/// reports ESRCH). Other errors (e.g. EPERM) are conservatively treated as
|
||
/// `true` so we do not falsely report an exit.
|
||
fn is_process_alive(pid: u32) -> bool {
|
||
match Command::new("kill").arg("-0").arg(pid.to_string()).status() {
|
||
Ok(s) => s.success(),
|
||
Err(_) => true,
|
||
}
|
||
}
|
||
|
||
/// Send a SIGTERM and wait up to `grace` for the child to exit; if still
|
||
/// alive, follow up with SIGKILL. Returns Ok once `kill -0` reports the
|
||
/// process is gone.
|
||
fn signal_then_collect(pid: u32, grace: Duration) -> Result<(), String> {
|
||
if let Err(why) = send_signal(pid, SIGTERM) {
|
||
log::warn!("signal_then_collect: SIGTERM to pid {} failed: {}", pid, why);
|
||
}
|
||
let deadline = Instant::now() + grace;
|
||
while Instant::now() < deadline {
|
||
if !is_process_alive(pid) {
|
||
return Ok(());
|
||
}
|
||
thread::sleep(Duration::from_millis(POLL_INTERVAL_MS));
|
||
}
|
||
log::warn!(
|
||
"signal_then_collect: pid {} did not exit within {:?}; escalating to SIGKILL",
|
||
pid,
|
||
grace
|
||
);
|
||
if let Err(why) = send_signal(pid, SIGKILL) {
|
||
return Err(format!(
|
||
"pid {} did not exit on SIGTERM and SIGKILL also failed: {}",
|
||
pid, why
|
||
));
|
||
}
|
||
let kill_deadline = Instant::now() + Duration::from_millis(GRACE_PERIOD_MS);
|
||
while Instant::now() < kill_deadline {
|
||
if !is_process_alive(pid) {
|
||
return Ok(());
|
||
}
|
||
thread::sleep(Duration::from_millis(POLL_INTERVAL_MS));
|
||
}
|
||
Err(format!(
|
||
"pid {} survived SIGKILL after {:?} — zombie child left for init",
|
||
pid,
|
||
grace + Duration::from_millis(GRACE_PERIOD_MS)
|
||
))
|
||
}
|
||
|
||
fn send_signal_to_spawned(
|
||
spawned: &Mutex<HashMap<String, SpawnedDriver>>,
|
||
signal: i32,
|
||
device_key: &str,
|
||
) -> Result<(), DriverError> {
|
||
let pids: Vec<u32> = {
|
||
let map = spawned.lock().unwrap_or_else(|e| e.into_inner());
|
||
map.values().map(|sd| sd.pid).collect()
|
||
};
|
||
for pid in pids {
|
||
if let Err(why) = send_signal(pid, signal) {
|
||
log::error!(
|
||
"send_signal_to_spawned: device {} pid {} signal {} failed: {}",
|
||
device_key,
|
||
pid,
|
||
signal,
|
||
why
|
||
);
|
||
return Err(DriverError::IoError);
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
// ── Crash counter, exponential backoff, auto-blacklist ───────────
|
||
//
|
||
// Closes the F1 finding: a driver that crashes on every spawn used
|
||
// to be respawned every hotplug poll (250ms) indefinitely. The
|
||
// `CrashTracker` records consecutive spawn failures per BDF, applies
|
||
// exponential backoff (100 ms × 2^N, cap 30 s) before the next probe,
|
||
// and auto-blacklists after `threshold` consecutive failures.
|
||
//
|
||
// Configuration is read once at startup from env vars:
|
||
// REDBEAR_DRIVER_CRASH_THRESHOLD (default 5)
|
||
// REDBEAR_DRIVER_BACKOFF_BASE_MS (default 100)
|
||
// REDBEAR_DRIVER_BACKOFF_CAP_MS (default 30000)
|
||
//
|
||
// A successful probe (Bind) clears the failure state for that BDF so
|
||
// a transient crash followed by a fix doesn't permanently blacklist
|
||
// the device.
|
||
|
||
/// Per-BDF crash state.
|
||
#[derive(Debug, Clone, Copy)]
|
||
pub struct CrashState {
|
||
pub failures: u32,
|
||
pub next_retry_at: Instant,
|
||
}
|
||
|
||
/// Process-wide tracker. Process-global because the failure of one
|
||
/// driver's spawn is observable across all drivers (every probe cycle
|
||
/// sees the same BDF and must respect the backoff window).
|
||
pub struct CrashTracker {
|
||
failures: std::sync::OnceLock<Mutex<HashMap<String, CrashState>>>,
|
||
threshold: AtomicU32,
|
||
backoff_base_ms: AtomicU64,
|
||
backoff_cap_ms: AtomicU64,
|
||
}
|
||
|
||
impl CrashTracker {
|
||
pub const fn new() -> Self {
|
||
Self {
|
||
failures: std::sync::OnceLock::new(),
|
||
threshold: AtomicU32::new(DEFAULT_CRASH_THRESHOLD),
|
||
backoff_base_ms: AtomicU64::new(DEFAULT_BACKOFF_BASE_MS),
|
||
backoff_cap_ms: AtomicU64::new(DEFAULT_BACKOFF_CAP_MS),
|
||
}
|
||
}
|
||
|
||
fn failures_map(&self) -> &Mutex<HashMap<String, CrashState>> {
|
||
self.failures.get_or_init(|| Mutex::new(HashMap::new()))
|
||
}
|
||
|
||
pub fn threshold(&self) -> u32 {
|
||
self.threshold.load(Ordering::SeqCst)
|
||
}
|
||
|
||
pub fn backoff_base_ms(&self) -> u64 {
|
||
self.backoff_base_ms.load(Ordering::SeqCst)
|
||
}
|
||
|
||
pub fn backoff_cap_ms(&self) -> u64 {
|
||
self.backoff_cap_ms.load(Ordering::SeqCst)
|
||
}
|
||
|
||
/// Read the env-var overrides and apply them. Called once at
|
||
/// startup from `apply_crash_tracker_env()` (see main.rs).
|
||
pub fn apply_env(&self) {
|
||
if let Ok(s) = std::env::var("REDBEAR_DRIVER_CRASH_THRESHOLD") {
|
||
if let Ok(n) = s.parse::<u32>() {
|
||
if n > 0 {
|
||
self.threshold.store(n, Ordering::SeqCst);
|
||
}
|
||
}
|
||
}
|
||
if let Ok(s) = std::env::var("REDBEAR_DRIVER_BACKOFF_BASE_MS") {
|
||
if let Ok(n) = s.parse::<u64>() {
|
||
if n > 0 {
|
||
self.backoff_base_ms.store(n, Ordering::SeqCst);
|
||
}
|
||
}
|
||
}
|
||
if let Ok(s) = std::env::var("REDBEAR_DRIVER_BACKOFF_CAP_MS") {
|
||
if let Ok(n) = s.parse::<u64>() {
|
||
if n > 0 {
|
||
self.backoff_cap_ms.store(n, Ordering::SeqCst);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Return true if `bdf` has a recorded failure whose
|
||
/// `next_retry_at` is still in the future. Used by `probe` to
|
||
/// short-circuit before the spawn attempt.
|
||
pub fn is_in_backoff(&self, bdf: &str, now: Instant) -> bool {
|
||
if let Ok(map) = self.failures_map().lock() {
|
||
if let Some(state) = map.get(bdf) {
|
||
return state.next_retry_at > now;
|
||
}
|
||
}
|
||
false
|
||
}
|
||
|
||
/// Snapshot the current failures table for the
|
||
/// `/crash_count/<bdf>` and `/crash_count` (list) scheme endpoints.
|
||
#[allow(dead_code)] // public API used by tests
|
||
pub fn snapshot(&self) -> Vec<(String, u32)> {
|
||
self.failures_map()
|
||
.lock()
|
||
.map(|map| {
|
||
map.iter()
|
||
.map(|(k, v)| (k.clone(), v.failures))
|
||
.collect()
|
||
})
|
||
.unwrap_or_default()
|
||
}
|
||
|
||
/// Record a spawn failure for `bdf`. Updates the failure counter
|
||
/// and computes the next-retry instant via exponential backoff.
|
||
/// Returns `Some(threshold_exceeded)` if the BDF should be
|
||
/// auto-blacklisted (i.e. `failures >= threshold`), `None` otherwise.
|
||
pub fn record_failure(&self, bdf: &str) -> Option<u32> {
|
||
let threshold = self.threshold();
|
||
let base_ms = self.backoff_base_ms();
|
||
let cap_ms = self.backoff_cap_ms();
|
||
let map_mutex = self.failures_map();
|
||
let mut map = match map_mutex.lock() {
|
||
Ok(m) => m,
|
||
Err(_) => return None,
|
||
};
|
||
let entry = map.entry(bdf.to_string()).or_insert(CrashState {
|
||
failures: 0,
|
||
next_retry_at: Instant::now(),
|
||
});
|
||
entry.failures = entry.failures.saturating_add(1);
|
||
let n = entry.failures;
|
||
let backoff_ms = base_ms
|
||
.checked_shl(n.min(20))
|
||
.unwrap_or(cap_ms)
|
||
.min(cap_ms);
|
||
entry.next_retry_at = Instant::now() + Duration::from_millis(backoff_ms);
|
||
if n >= threshold {
|
||
Some(n)
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
|
||
/// Clear any failure state for `bdf`. Called from a successful
|
||
/// probe (Bind result).
|
||
pub fn record_success(&self, bdf: &str) {
|
||
if let Ok(mut map) = self.failures_map().lock() {
|
||
map.remove(bdf);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Global crash tracker. The pointer is set once at startup
|
||
/// (`set_crash_tracker`); reads in the probe hot path are non-blocking
|
||
/// only because the pointer is `static`, not because of any
|
||
/// `RwLock`. If unset, `crash_tracker()` returns a static stub with
|
||
/// the default threshold and no recorded failures — i.e. behavior
|
||
/// identical to pre-F1 (every probe runs, no backoff, no blacklist).
|
||
static mut CRASH_TRACKER: *const CrashTracker = std::ptr::null();
|
||
|
||
/// Returns a reference to the global crash tracker. If the manager
|
||
/// has not yet called `set_crash_tracker`, returns a static stub that
|
||
/// reports threshold=0 (i.e. never auto-blacklists) and accepts every
|
||
/// probe — this preserves the pre-F1 behaviour for early-init code
|
||
/// paths (config loading, env parsing) that run before the manager
|
||
/// wires the tracker.
|
||
pub fn crash_tracker() -> &'static CrashTracker {
|
||
unsafe {
|
||
if CRASH_TRACKER.is_null() {
|
||
&CRASH_TRACKER_STUB
|
||
} else {
|
||
&*CRASH_TRACKER
|
||
}
|
||
}
|
||
}
|
||
|
||
static CRASH_TRACKER_STUB: CrashTracker = CrashTracker::new();
|
||
|
||
/// Wire the global crash tracker. Called once at startup from
|
||
/// `apply_crash_tracker_env()` after env vars are read. The pointer
|
||
/// is process-lifetime and never reset.
|
||
pub fn set_crash_tracker(tracker: &'static CrashTracker) {
|
||
unsafe {
|
||
CRASH_TRACKER = tracker;
|
||
}
|
||
}
|
||
|
||
/// Snapshot the PIDs of every spawned child for this driver config.
|
||
/// Used by the system-wide `/suspend` scheme endpoint to walk drivers
|
||
/// in priority order and signal each in turn. Returns PIDs in
|
||
/// registration order; the manager iterates drivers in priority order
|
||
/// (highest first for resume, lowest first for suspend).
|
||
#[allow(dead_code)] // public API used by tests
|
||
pub fn spawned_pids_snapshot(cfg: &DriverConfig) -> Vec<u32> {
|
||
let map = cfg
|
||
.spawned
|
||
.lock()
|
||
.unwrap_or_else(|e| e.into_inner());
|
||
map.values().map(|sd| sd.pid).collect()
|
||
}
|
||
|
||
/// Send `signal` to every spawned child of this driver config. Returns
|
||
/// the count of PIDs successfully signalled (signal failures are
|
||
/// logged but do not abort the walk — system PM must attempt every
|
||
/// driver even if one fails).
|
||
#[allow(dead_code)] // public API used by tests
|
||
pub fn signal_all_spawned(cfg: &DriverConfig, signal: i32) -> usize {
|
||
let pids = spawned_pids_snapshot(cfg);
|
||
let mut ok = 0;
|
||
for pid in pids {
|
||
if send_signal(pid, signal).is_ok() {
|
||
ok += 1;
|
||
}
|
||
}
|
||
ok
|
||
}
|
||
|
||
/// Decision returned by [`spawn_decision_gate`].
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
pub enum SpawnDecision {
|
||
/// Probe may spawn the driver daemon.
|
||
Spawn,
|
||
/// Probe must NOT spawn; the manager is in observe-only mode.
|
||
ObserveOnly(String),
|
||
}
|
||
|
||
/// Process-wide loaded blacklist. Initialised once via
|
||
/// [`set_global_shared_blacklist`] from `main()` and consulted by
|
||
/// [`spawn_decision_gate`]. A `None` value (e.g. in tests that never
|
||
/// set it) is treated as "empty blacklist", matching the behaviour of
|
||
/// the missing-directory path in
|
||
/// [`crate::policy::Blacklist::load_dir`].
|
||
static GLOBAL_BLACKLIST: std::sync::OnceLock<crate::policy::SharedBlacklist> =
|
||
std::sync::OnceLock::new();
|
||
|
||
/// Registered driver configs. Populated by `load_all()` at startup and
|
||
/// used by the `/modalias` scheme endpoint for lookup. A `None` value
|
||
/// (e.g. in tests that never call `load_all`) is treated as an empty set,
|
||
/// matching the behaviour of the missing-config path in `load_all`.
|
||
static REGISTERED_DRIVERS: std::sync::OnceLock<Vec<DriverConfig>> =
|
||
std::sync::OnceLock::new();
|
||
|
||
pub fn set_registered_drivers(drivers: Vec<DriverConfig>) {
|
||
let _ = REGISTERED_DRIVERS.set(drivers);
|
||
}
|
||
|
||
pub fn drivers_registered() -> &'static [DriverConfig] {
|
||
REGISTERED_DRIVERS
|
||
.get()
|
||
.map(|v| v.as_slice())
|
||
.unwrap_or(&[])
|
||
}
|
||
|
||
pub fn set_global_shared_blacklist(blacklist: crate::policy::SharedBlacklist) {
|
||
let _ = GLOBAL_BLACKLIST.set(blacklist);
|
||
}
|
||
|
||
pub fn blacklist_match(driver_name: &str) -> bool {
|
||
GLOBAL_BLACKLIST
|
||
.get()
|
||
.map(|b| b.is_blacklisted(driver_name))
|
||
.unwrap_or(false)
|
||
}
|
||
|
||
// ── Process-wide loaded driver options (N1 wiring) ──────────────────
|
||
//
|
||
// Initialised once via `set_global_shared_driver_options` from `main()`.
|
||
// Consulted by the spawn path to emit `REDBEAR_DRIVER_PARAM_<NAME>=<value>`
|
||
// env vars for each override declared for the spawned driver. A `None`
|
||
// value (e.g. in tests that never set it) is treated as "no overrides" —
|
||
// the spawn path simply emits no extra env vars.
|
||
static GLOBAL_DRIVER_OPTIONS: std::sync::OnceLock<crate::policy::SharedDriverOptions> =
|
||
std::sync::OnceLock::new();
|
||
|
||
pub fn set_global_shared_driver_options(options: crate::policy::SharedDriverOptions) {
|
||
let _ = GLOBAL_DRIVER_OPTIONS.set(options);
|
||
}
|
||
|
||
pub fn driver_options_for(driver_name: &str) -> Vec<(String, String)> {
|
||
GLOBAL_DRIVER_OPTIONS
|
||
.get()
|
||
.map(|o| o.for_driver(driver_name))
|
||
.unwrap_or_default()
|
||
}
|
||
|
||
// ── Process-wide loaded autoload list (N2 wiring) ───────────────────
|
||
|
||
static GLOBAL_AUTOLOAD: std::sync::OnceLock<crate::policy::SharedAutoloadList> =
|
||
std::sync::OnceLock::new();
|
||
|
||
pub fn set_global_shared_autoload_list(list: crate::policy::SharedAutoloadList) {
|
||
let _ = GLOBAL_AUTOLOAD.set(list);
|
||
}
|
||
|
||
#[allow(dead_code)] // public API used by future initfs manager (operator introspection)
|
||
pub fn autoload_modules() -> Vec<String> {
|
||
GLOBAL_AUTOLOAD
|
||
.get()
|
||
.map(|a| a.modules())
|
||
.unwrap_or_default()
|
||
}
|
||
|
||
// ── Process-wide loaded initfs manifest (N3 wiring) ─────────────────
|
||
|
||
static GLOBAL_INITFS_MANIFEST: std::sync::OnceLock<crate::policy::SharedInitfsManifest> =
|
||
std::sync::OnceLock::new();
|
||
|
||
pub fn set_global_shared_initfs_manifest(manifest: crate::policy::SharedInitfsManifest) {
|
||
let _ = GLOBAL_INITFS_MANIFEST.set(manifest);
|
||
}
|
||
|
||
#[allow(dead_code)] // public API used by initfs manager (operator introspection)
|
||
pub fn initfs_manifest_stages() -> Vec<(crate::policy::InitfsStageKind, Vec<String>)> {
|
||
GLOBAL_INITFS_MANIFEST
|
||
.get()
|
||
.map(|m| m.stages())
|
||
.unwrap_or_default()
|
||
}
|
||
|
||
/// The SpawnDecision committee: returns whether the manager should spawn the
|
||
/// driver daemon at this probe, given the current environment and CLI flags.
|
||
///
|
||
/// Per migration plan § 5.1 D1, the committee gates on five signals:
|
||
///
|
||
/// 1. The driver's `command` must be non-empty (handled in `probe` itself).
|
||
/// 2. `claim_pci_device` must have returned a bind handle (handled in `probe`).
|
||
/// 3. All `depends_on` schemes must be present (handled in `probe`).
|
||
/// 4. CLI flag `--no-spawn` (observation-only mode).
|
||
/// 5. Environment variable `REDBEAR_DRIVER_MANAGER_ACTIVE=0` (observation-only).
|
||
///
|
||
/// When any gate rejects, the committee returns [`SpawnDecision::ObserveOnly`]
|
||
/// with a short reason; the manager emits `ProbeResult::NotSupported`.
|
||
pub fn spawn_decision_gate() -> SpawnDecision {
|
||
let args: Vec<String> = std::env::args().collect();
|
||
if args.iter().any(|a| a == "--no-spawn" || a == "--observe") {
|
||
return SpawnDecision::ObserveOnly("cli flag --no-spawn/--observe".to_string());
|
||
}
|
||
if let Ok(val) = std::env::var("REDBEAR_DRIVER_MANAGER_ACTIVE") {
|
||
if val == "0" || val.eq_ignore_ascii_case("false") || val == "no" {
|
||
return SpawnDecision::ObserveOnly(format!(
|
||
"REDBEAR_DRIVER_MANAGER_ACTIVE={}",
|
||
val
|
||
));
|
||
}
|
||
}
|
||
SpawnDecision::Spawn
|
||
}
|
||
|
||
impl From<RawDriverMatch> for DriverMatch {
|
||
fn from(r: RawDriverMatch) -> Self {
|
||
DriverMatch {
|
||
vendor: r.vendor,
|
||
device: r.device,
|
||
class: r.class,
|
||
subclass: r.subclass,
|
||
prog_if: r.prog_if,
|
||
subsystem_vendor: r.subsystem_vendor,
|
||
subsystem_device: r.subsystem_device,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl DriverConfig {
|
||
/// Load every `.toml` config file in `dir` and return the merged,
|
||
/// priority-sorted list of driver configs.
|
||
///
|
||
/// Format detection: each file is tried as a `[[driver]]` (new) first,
|
||
/// then as `[[drivers]]` (legacy pcid-spawner) if that fails. The
|
||
/// legacy form is normalised to one `DriverConfig` per legacy entry,
|
||
/// expanding `ids` and `device_id_range` into individual match entries.
|
||
/// Format errors in either form are reported with `parse <path> failed`.
|
||
pub fn load_all(dir: &str) -> Result<Vec<DriverConfig>, String> {
|
||
let entries = fs::read_dir(dir).map_err(|e| format!("read_dir failed: {}", e))?;
|
||
|
||
let mut configs = Vec::new();
|
||
|
||
for entry in entries {
|
||
let entry = entry.map_err(|e| format!("entry error: {}", e))?;
|
||
let path = entry.path();
|
||
|
||
if !path.is_file() {
|
||
continue;
|
||
}
|
||
|
||
let data = fs::read_to_string(&path)
|
||
.map_err(|e| format!("read {} failed: {}", path.display(), e))?;
|
||
|
||
let parsed_new: Result<RawDriverToml, _> = toml::from_str(&data);
|
||
match parsed_new {
|
||
Ok(parsed) => {
|
||
for driver in parsed.driver {
|
||
let matches: Vec<DriverMatch> =
|
||
driver.r#match.into_iter().map(DriverMatch::from).collect();
|
||
let initial_power_state = match &driver.initial_power_state {
|
||
Some(s) => match PciPowerState::from_toml(s) {
|
||
Ok(state) => state,
|
||
Err(e) => {
|
||
log::warn!(
|
||
"{}: driver `{}`: {} (defaulting to D0)",
|
||
path.display(),
|
||
driver.name,
|
||
e
|
||
);
|
||
PciPowerState::D0
|
||
}
|
||
},
|
||
None => PciPowerState::D0,
|
||
};
|
||
// Collect per-driver [driver.params] into an
|
||
// ordered Vec for env-var emission. BTreeMap
|
||
// iteration is sorted by key so the env vars
|
||
// are emitted in deterministic order.
|
||
let params: Vec<(String, String)> = driver
|
||
.params
|
||
.iter()
|
||
.map(|(k, v)| (k.clone(), v.clone()))
|
||
.collect();
|
||
if !params.is_empty() {
|
||
log::info!(
|
||
"policy: driver `{}` has {} [driver.params] override{}",
|
||
driver.name,
|
||
params.len(),
|
||
if params.len() == 1 { "" } else { "s" }
|
||
);
|
||
}
|
||
configs.push(DriverConfig {
|
||
name: driver.name,
|
||
description: driver.description,
|
||
priority: driver.priority,
|
||
command: driver.command,
|
||
matches,
|
||
depends_on: driver.depends_on,
|
||
exclusive_with: driver.exclusive_with,
|
||
params,
|
||
initial_power_state,
|
||
spawned: Mutex::new(HashMap::new()),
|
||
pid_to_device: Mutex::new(HashMap::new()),
|
||
});
|
||
}
|
||
}
|
||
Err(new_err) => {
|
||
let parsed_legacy: Result<RawLegacyToml, _> = toml::from_str(&data);
|
||
match parsed_legacy {
|
||
Ok(parsed) => {
|
||
log::warn!(
|
||
"{}: legacy `[[drivers]]` format detected; \
|
||
convert to `[[driver]]` to silence this warning",
|
||
path.display()
|
||
);
|
||
for legacy in parsed.drivers {
|
||
configs.push(convert_legacy(legacy));
|
||
}
|
||
}
|
||
Err(legacy_err) => {
|
||
return Err(format!(
|
||
"{}: neither `[[driver]]` ({}) nor `[[drivers]]` ({}) \
|
||
parse succeeded",
|
||
path.display(),
|
||
new_err,
|
||
legacy_err
|
||
));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
configs.sort_by(|a, b| b.priority.cmp(&a.priority));
|
||
Ok(configs)
|
||
}
|
||
|
||
/// Reap a child known to have exited via crash (non-zero exit code,
|
||
/// or a signal other than SIGTERM/SIGINT). Removes the device from
|
||
/// the spawned/pid_to_device maps, closes the error_channel fd, and
|
||
/// advances the CrashTracker. After `threshold` consecutive crashes
|
||
/// the device is auto-blacklisted.
|
||
pub fn reap_pid(&self, pid: u32) {
|
||
self.reap_pid_inner(pid, /*crashed=*/ true);
|
||
}
|
||
|
||
/// Reap a child known to have exited cleanly (exit code 0, or
|
||
/// SIGTERM/SIGINT from our own `remove()`). Removes the device from
|
||
/// the maps and closes the error_channel fd, but does NOT advance
|
||
/// the crash tracker — this prevents the 5-consecutive-exits
|
||
/// autoblacklist from triggering on intentional driver reloads.
|
||
pub fn reap_pid_clean(&self, pid: u32) {
|
||
self.reap_pid_inner(pid, /*crashed=*/ false);
|
||
}
|
||
|
||
fn reap_pid_inner(&self, pid: u32, crashed: bool) {
|
||
let device_key = if let Ok(mut p2d) = self.pid_to_device.lock() {
|
||
if let Some(key) = p2d.remove(&pid) {
|
||
if let Ok(mut spawned) = self.spawned.lock() {
|
||
spawned.remove(&key);
|
||
}
|
||
Some(key)
|
||
} else {
|
||
None
|
||
}
|
||
} else {
|
||
None
|
||
};
|
||
if let Some(key) = device_key {
|
||
// N18 Bug4: close the error_channel fd on every reap path so
|
||
// a crashed or cleanly-exited daemon doesn't leak a UnixStream
|
||
// until process exit.
|
||
crate::error_channel::global().remove(&key);
|
||
if crashed {
|
||
if let Some(count) = crash_tracker().record_failure(&key) {
|
||
log::warn!(
|
||
"crash-threshold: driver={} device={} consecutive_failures={} \
|
||
auto-blacklisted (override via /etc/driver-manager.d/disable-<bdf>.toml \
|
||
or REDBEAR_DRIVER_CRASH_THRESHOLD override)",
|
||
self.name,
|
||
key,
|
||
count
|
||
);
|
||
}
|
||
} else {
|
||
// Clean exit: clear any backoff the device was in so a
|
||
// later crash starts the count from 1, not from where the
|
||
// previous clean-exit backoff ended.
|
||
crash_tracker().record_success(&key);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn pci_device_path(info: &DeviceInfo) -> String {
|
||
if info.raw_path.starts_with("/scheme/pci/") {
|
||
info.raw_path.clone()
|
||
} else {
|
||
format!("/scheme/pci/{}", info.id.path)
|
||
}
|
||
}
|
||
|
||
fn check_scheme_available(name: &str) -> bool {
|
||
if std::path::Path::new(&format!("/scheme/{}", name)).exists() {
|
||
return true;
|
||
}
|
||
false
|
||
}
|
||
|
||
impl Driver for DriverConfig {
|
||
fn name(&self) -> &str {
|
||
&self.name
|
||
}
|
||
|
||
fn description(&self) -> &str {
|
||
&self.description
|
||
}
|
||
|
||
fn priority(&self) -> i32 {
|
||
self.priority
|
||
}
|
||
|
||
fn match_table(&self) -> &[DriverMatch] {
|
||
&self.matches
|
||
}
|
||
|
||
fn probe(&self, info: &DeviceInfo) -> ProbeResult {
|
||
let device_key = info.id.path.clone();
|
||
|
||
{
|
||
let spawned = self.spawned.lock().unwrap_or_else(|e| e.into_inner());
|
||
if spawned.contains_key(&device_key) {
|
||
log::debug!("driver {} already bound to {}", self.name, device_key);
|
||
return ProbeResult::Bound;
|
||
}
|
||
}
|
||
|
||
// F1 crash tracker: if this BDF recently failed enough times
|
||
// to exceed the threshold, the policy blacklist has already
|
||
// absorbed the BDF (see `record_failure` integration below).
|
||
// For in-backoff state below the threshold, defer with a
|
||
// backoff reason so the next probe cycle respects it.
|
||
if crash_tracker().is_in_backoff(&device_key, Instant::now()) {
|
||
log::debug!(
|
||
"crash-backoff: driver={} device={} skipping until next retry",
|
||
self.name,
|
||
device_key
|
||
);
|
||
return ProbeResult::Deferred {
|
||
reason: format!("crash-backoff for {}", device_key),
|
||
};
|
||
}
|
||
|
||
let early_quirks = crate::quirks::decide_for_phase(
|
||
info,
|
||
redox_driver_sys::quirks::QuirkPhase::Early,
|
||
);
|
||
if early_quirks.has_flags() {
|
||
log::info!(
|
||
"early-quirks: driver={} device={} flags={}",
|
||
self.name,
|
||
device_key,
|
||
early_quirks.summary_short()
|
||
);
|
||
}
|
||
if early_quirks.flags.contains(redox_driver_sys::quirks::PciQuirkFlags::WRONG_CLASS)
|
||
|| early_quirks
|
||
.flags
|
||
.contains(redox_driver_sys::quirks::PciQuirkFlags::BROKEN_BRIDGE)
|
||
{
|
||
return ProbeResult::NotSupported;
|
||
}
|
||
|
||
// Claim the device by opening its pcid channel. The channel is
|
||
// exclusive: pcid returns ENOLCK to a second opener, so a
|
||
// concurrent probe of the same device loses atomically. This is
|
||
// the same model pcid-spawner uses — the channel fd doubles as
|
||
// the claim and is later handed to the spawned child.
|
||
let claim_start = Instant::now();
|
||
|
||
let device_path = pci_device_path(info);
|
||
let mut handle = match PciFunctionHandle::connect_by_path(Path::new(&device_path)) {
|
||
Ok(handle) => handle,
|
||
Err(err) => {
|
||
if err.raw_os_error() == Some(syscall::ENOLCK as i32) {
|
||
log::debug!("device {} already claimed (ENOLCK)", device_key);
|
||
return ProbeResult::NotSupported;
|
||
}
|
||
return ProbeResult::Deferred {
|
||
reason: format!("channel open for {} failed: {}", device_path, err),
|
||
};
|
||
}
|
||
};
|
||
|
||
// Mutual exclusion: if this driver is exclusive_with another driver
|
||
// and that other driver is already bound to this device, skip the
|
||
// spawn. Priority determines who wins — the higher-priority driver
|
||
// claims the device; the lower-priority one is deferred.
|
||
for excluded in &self.exclusive_with {
|
||
if let Some(other) = crate::config::drivers_registered()
|
||
.iter()
|
||
.find(|d| d.name == *excluded)
|
||
{
|
||
let other_bound = other
|
||
.spawned
|
||
.lock()
|
||
.unwrap_or_else(|e| e.into_inner())
|
||
.contains_key(&device_key);
|
||
if other_bound {
|
||
log::info!(
|
||
"exclusive_with: driver {} skips spawn of {} (already bound by {})",
|
||
self.name,
|
||
device_key,
|
||
other.name
|
||
);
|
||
return ProbeResult::Deferred {
|
||
reason: format!("exclusive_with {}: device {} already claimed by {}", self.name, device_key, other.name),
|
||
};
|
||
}
|
||
}
|
||
}
|
||
|
||
let quirk_decision = crate::quirks::decide(info);
|
||
if quirk_decision.has_flags() {
|
||
log::info!(
|
||
"quirks: driver={} device={} flags={}",
|
||
self.name,
|
||
device_key,
|
||
quirk_decision.summary_short()
|
||
);
|
||
}
|
||
if quirk_decision.needs_firmware {
|
||
if check_scheme_available("firmware") {
|
||
log::info!(
|
||
"firmware-ready: driver={} device={} firmware scheme available",
|
||
self.name,
|
||
device_key
|
||
);
|
||
crate::timing::record_firmware_ready_if_deferred(&device_key);
|
||
} else {
|
||
log::info!(
|
||
"quirk-defer: driver={} device={} waiting-for-firmware",
|
||
self.name,
|
||
device_key
|
||
);
|
||
crate::timing::record_firmware_defer_start(&device_key);
|
||
return ProbeResult::Deferred {
|
||
reason: format!(
|
||
"PciQuirkFlags::NEED_FIRMWARE set for {}",
|
||
device_key
|
||
),
|
||
};
|
||
}
|
||
}
|
||
|
||
if blacklist_match(&self.name) {
|
||
log::info!(
|
||
"policy: driver {} is on the blacklist; not spawning for {}",
|
||
self.name,
|
||
device_key
|
||
);
|
||
return ProbeResult::NotSupported;
|
||
}
|
||
|
||
match spawn_decision_gate() {
|
||
SpawnDecision::Spawn => {}
|
||
SpawnDecision::ObserveOnly(reason) => {
|
||
log::debug!(
|
||
"observe-only: skipping spawn of driver {} for {} ({})",
|
||
self.name,
|
||
device_key,
|
||
reason
|
||
);
|
||
return ProbeResult::NotSupported;
|
||
}
|
||
}
|
||
|
||
if self.command.is_empty() {
|
||
return ProbeResult::Fatal {
|
||
reason: String::from("empty command"),
|
||
};
|
||
}
|
||
|
||
let actual_path = if self.command[0].starts_with('/') {
|
||
self.command[0].clone()
|
||
} else {
|
||
format!("/usr/lib/drivers/{}", self.command[0])
|
||
};
|
||
|
||
if !std::path::Path::new(&actual_path).exists() {
|
||
return ProbeResult::Deferred {
|
||
reason: format!("driver binary not found: {}", actual_path),
|
||
};
|
||
}
|
||
|
||
let deps: Vec<String> = if !self.depends_on.is_empty() {
|
||
self.depends_on.clone()
|
||
} else {
|
||
guess_dependencies(&self.name)
|
||
};
|
||
for dep in &deps {
|
||
if !check_scheme_available(dep) {
|
||
return ProbeResult::Deferred {
|
||
reason: format!("dependency scheme not ready: {}", dep),
|
||
};
|
||
}
|
||
}
|
||
|
||
log::info!("probing {} with driver {}", device_key, self.name);
|
||
|
||
handle.enable_device();
|
||
let channel_fd = handle.into_inner_fd();
|
||
let channel_fd = unsafe { OwnedFd::from_raw_fd(channel_fd) };
|
||
|
||
let mut cmd = Command::new(&actual_path);
|
||
for arg in &self.command[1..] {
|
||
cmd.arg(arg);
|
||
}
|
||
|
||
// Sidecar error-report channel: a unix socketpair lets the
|
||
// spawned daemon receive AER notifications and return a
|
||
// RecoveryAction. The child fd goes in the env var; the
|
||
// parent fd is registered for the AER dispatch path to
|
||
// consult. Drivers that do not opt in ignore the fd.
|
||
//
|
||
// Initfs mode skips the socketpair entirely: the initfs
|
||
// kernel namespace does not support AF_UNIX socketpair (returns
|
||
// ENODEV) and the initfs driver-manager is transient — it
|
||
// exits after enumeration, so no AER dispatch ever runs. The
|
||
// initfs spawn log line is silent on this branch by design.
|
||
let error_channel = if crate::is_initfs_mode() {
|
||
None
|
||
} else {
|
||
match std::os::unix::net::UnixStream::pair() {
|
||
Ok((parent, child)) => {
|
||
cmd.env("REDBEAR_DRIVER_ERROR_FD", child.as_raw_fd().to_string());
|
||
// Forget the child fd so dropping the cmd doesn't
|
||
// double-close it (Command::spawn takes ownership).
|
||
std::mem::forget(child);
|
||
Some((
|
||
crate::error_channel::ErrorChannel { stream: parent },
|
||
device_key.clone(),
|
||
))
|
||
}
|
||
Err(err) => {
|
||
log::warn!(
|
||
"driver {} for device {} could not get sidecar error channel: {}",
|
||
self.name,
|
||
device_key,
|
||
err
|
||
);
|
||
None
|
||
}
|
||
}
|
||
};
|
||
|
||
cmd.env("PCID_CLIENT_CHANNEL", channel_fd.as_raw_fd().to_string());
|
||
cmd.env("PCID_DEVICE_PATH", &device_path);
|
||
|
||
// N1: apply per-driver parameter overrides from the policy
|
||
// directory's `[[options]]` entries. Each `(name, value)` pair
|
||
// becomes an env var `REDBEAR_DRIVER_PARAM_<NAME>=<value>` so
|
||
// the daemon can consume them without a separate IPC channel.
|
||
let overrides = driver_options_for(&self.name);
|
||
for (param_name, param_value) in &overrides {
|
||
cmd.env(
|
||
format!("REDBEAR_DRIVER_PARAM_{}", param_name.to_uppercase()),
|
||
param_value,
|
||
);
|
||
}
|
||
if !overrides.is_empty() {
|
||
log::info!(
|
||
"policy-options: driver={} applied {} override{}",
|
||
self.name,
|
||
overrides.len(),
|
||
if overrides.len() == 1 { "" } else { "s" }
|
||
);
|
||
}
|
||
|
||
// N8: apply per-driver [driver.params] overrides from the
|
||
// policy layer (e.g. 50-amdgpu.toml's radeon_overlap /
|
||
// amdgpu_force / amdgpu_disable_accel). Same env-var
|
||
// convention as `[[options]]` so drivers don't need to
|
||
// distinguish the two sources.
|
||
for (param_name, param_value) in &self.params {
|
||
cmd.env(
|
||
format!("REDBEAR_DRIVER_PARAM_{}", param_name.to_uppercase()),
|
||
param_value,
|
||
);
|
||
}
|
||
|
||
if !self.initial_power_state.is_default() {
|
||
cmd.env(
|
||
"REDBEAR_DRIVER_INITIAL_POWER_STATE",
|
||
self.initial_power_state.as_str(),
|
||
);
|
||
log::info!(
|
||
"power-state: driver={} device={} initial={} (driver daemon will \
|
||
set this on its granted channel)",
|
||
self.name,
|
||
device_key,
|
||
self.initial_power_state.as_str()
|
||
);
|
||
}
|
||
|
||
let quirk_decision_at_spawn = crate::quirks::decide(info);
|
||
if quirk_decision_at_spawn.has_flags() {
|
||
log::info!(
|
||
"spawn-hints: driver={} device={} flags={}",
|
||
self.name,
|
||
device_key,
|
||
quirk_decision_at_spawn.summary_short()
|
||
);
|
||
}
|
||
if std::env::var("pci").map(|v| v == "nomsi" || v == "no_msi").unwrap_or(false) {
|
||
cmd.env("REDBEAR_DRIVER_PCI_IRQ_MODE", "intx_only");
|
||
} else if quirk_decision_at_spawn.request_intx_or_msi_only {
|
||
cmd.env("REDBEAR_DRIVER_PCI_IRQ_MODE", "intx_or_msi");
|
||
}
|
||
if quirk_decision_at_spawn.disable_accel {
|
||
cmd.env("REDBEAR_DRIVER_DISABLE_ACCEL", "1");
|
||
}
|
||
|
||
cmd.env(
|
||
"REDBEAR_DRIVER_IOMMU_GROUP",
|
||
redox_driver_core::modern_technology::iommu_group_env_value(&info.id.path),
|
||
);
|
||
cmd.env(
|
||
"REDBEAR_DRIVER_NUMA_NODE",
|
||
redox_driver_core::modern_technology::numa_node_env_value(&info.id.path),
|
||
);
|
||
let msix = redox_driver_core::modern_technology::propose_msix_vectors(&info.id.path, 1, 16);
|
||
cmd.env("REDBEAR_DRIVER_MSIX_VECTORS", msix.recommended.to_string());
|
||
|
||
match cmd.spawn() {
|
||
Ok(child) => {
|
||
let pid = child.id();
|
||
let claim_spawn_us = claim_start.elapsed().as_micros() as u64;
|
||
crate::timing::record("claim-spawn", claim_spawn_us);
|
||
log::info!(
|
||
"driver {} spawned (pid {}) for device {} (claim-spawn {}us)",
|
||
self.name,
|
||
pid,
|
||
device_key,
|
||
claim_spawn_us
|
||
);
|
||
crate::timing::record_firmware_ready_if_deferred(&device_key);
|
||
if let Some((channel, key)) = error_channel {
|
||
crate::error_channel::global()
|
||
.insert(key, std::sync::Arc::new(channel));
|
||
}
|
||
let mut spawned = self.spawned.lock().unwrap_or_else(|e| e.into_inner());
|
||
spawned.insert(device_key.clone(), SpawnedDriver { pid, channel_fd });
|
||
if let Ok(mut p2d) = self.pid_to_device.lock() {
|
||
p2d.insert(pid, device_key.clone());
|
||
}
|
||
// F1: a successful spawn clears any prior failure
|
||
// state for this BDF, so a transient crash followed by
|
||
// a working spawn does not leave the device
|
||
// permanently back-pressured.
|
||
crash_tracker().record_success(&device_key);
|
||
ProbeResult::Bound
|
||
}
|
||
Err(e) => {
|
||
// F1: an immediate spawn failure (e.g. ENOENT for a
|
||
// missing binary) counts toward the threshold just
|
||
// like a SIGCHLD reap would.
|
||
if let Some(count) = crash_tracker().record_failure(&device_key) {
|
||
log::warn!(
|
||
"crash-threshold: driver={} device={} consecutive_failures={} \
|
||
(spawn returned: {})",
|
||
self.name,
|
||
device_key,
|
||
count,
|
||
e
|
||
);
|
||
}
|
||
ProbeResult::Fatal {
|
||
reason: format!("spawn failed: {}", e),
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn remove(&self, info: &DeviceInfo) -> Result<(), DriverError> {
|
||
let device_key = info.id.path.clone();
|
||
let binding = {
|
||
let mut spawned = self
|
||
.spawned
|
||
.lock()
|
||
.unwrap_or_else(|e| e.into_inner());
|
||
spawned.remove(&device_key)
|
||
};
|
||
crate::error_channel::global().remove(&device_key);
|
||
if let Some(ref b) = binding {
|
||
if let Ok(mut p2d) = self.pid_to_device.lock() {
|
||
p2d.remove(&b.pid);
|
||
}
|
||
}
|
||
if let Some(ref b) = binding {
|
||
if let Ok(mut p2d) = self.pid_to_device.lock() {
|
||
p2d.remove(&b.pid);
|
||
}
|
||
}
|
||
|
||
match binding {
|
||
Some(binding) => {
|
||
let channel_fd = binding.channel_fd.as_raw_fd();
|
||
log::info!(
|
||
"unbind-request: device {} from driver {} (pid {}, channel fd {})",
|
||
device_key,
|
||
self.name,
|
||
binding.pid,
|
||
channel_fd
|
||
);
|
||
match signal_then_collect(binding.pid, Duration::from_millis(GRACE_PERIOD_MS)) {
|
||
Ok(()) => {
|
||
log::info!(
|
||
"unbound: device {} from driver {} (pid {} exited cleanly)",
|
||
device_key,
|
||
self.name,
|
||
binding.pid
|
||
);
|
||
Ok(())
|
||
}
|
||
Err(why) => {
|
||
log::error!(
|
||
"unbind: failed to stop pid {} for driver {} device {}: {}",
|
||
binding.pid,
|
||
self.name,
|
||
device_key,
|
||
why
|
||
);
|
||
Err(DriverError::IoError)
|
||
}
|
||
}
|
||
}
|
||
None => {
|
||
log::warn!("driver {} not bound to device {}", self.name, device_key);
|
||
Err(DriverError::Other("not bound"))
|
||
}
|
||
}
|
||
}
|
||
|
||
fn suspend(&self, info: &DeviceInfo) -> Result<(), DriverError> {
|
||
let device_key = info.id.path.clone();
|
||
let spawned_pids = {
|
||
let spawned = self
|
||
.spawned
|
||
.lock()
|
||
.unwrap_or_else(|e| e.into_inner());
|
||
spawned.keys().cloned().collect::<Vec<_>>()
|
||
};
|
||
log::info!(
|
||
"suspend-request: driver {} device {} ({} spawned child(ren))",
|
||
self.name,
|
||
device_key,
|
||
spawned_pids.len()
|
||
);
|
||
send_signal_to_spawned(&self.spawned, SIGTERM, &device_key)?;
|
||
Ok(())
|
||
}
|
||
|
||
fn resume(&self, info: &DeviceInfo) -> Result<(), DriverError> {
|
||
let device_key = info.id.path.clone();
|
||
let spawned_pids = {
|
||
let spawned = self
|
||
.spawned
|
||
.lock()
|
||
.unwrap_or_else(|e| e.into_inner());
|
||
spawned.keys().cloned().collect::<Vec<_>>()
|
||
};
|
||
log::info!(
|
||
"resume-request: driver {} device {} ({} spawned child(ren))",
|
||
self.name,
|
||
device_key,
|
||
spawned_pids.len()
|
||
);
|
||
// SIGCONT (signal 18 on Linux/Redox). The driver daemon is
|
||
// expected to honour SIGCONT by resuming normal operation;
|
||
// if the daemon has exited, the SIGCHLD reaper will reap it
|
||
// and a subsequent probe cycle will rebind the device.
|
||
send_signal_to_spawned(&self.spawned, libc::SIGCONT as i32, &device_key)?;
|
||
Ok(())
|
||
}
|
||
|
||
fn on_error(
|
||
&self,
|
||
info: &DeviceInfo,
|
||
severity: ErrorSeverity,
|
||
) -> Result<RecoveryAction, DriverError> {
|
||
let _ = info;
|
||
Ok(match severity {
|
||
ErrorSeverity::Correctable => RecoveryAction::Handled,
|
||
ErrorSeverity::NonFatal => RecoveryAction::ResetDevice,
|
||
ErrorSeverity::Fatal => RecoveryAction::RescanBus,
|
||
})
|
||
}
|
||
|
||
fn params(&self) -> DriverParams {
|
||
let mut p = DriverParams::new();
|
||
p.define(
|
||
"enabled",
|
||
"Whether this driver is active",
|
||
ParamValue::Bool(true),
|
||
true,
|
||
);
|
||
p.define(
|
||
"priority",
|
||
"Probe priority (higher = earlier)",
|
||
ParamValue::Int(self.priority as i64),
|
||
false,
|
||
);
|
||
p
|
||
}
|
||
}
|
||
|
||
/// Driver-specified dependencies. Parsed from [driver.depends] TOML field.
|
||
/// Example: depends_on = ["pci", "acpi"]
|
||
/// When specified, takes precedence over guess_dependencies().
|
||
fn guess_dependencies(driver_name: &str) -> Vec<String> {
|
||
match driver_name {
|
||
"xhcid" | "usbhubd" | "usbctl" | "usbhidd" | "usbscsid" => {
|
||
vec![String::from("pci")]
|
||
}
|
||
"nvmed" | "ahcid" | "ided" | "virtio-blkd" => {
|
||
vec![String::from("pci")]
|
||
}
|
||
"e1000d" | "rtl8168d" | "rtl8139d" | "ixgbed" | "virtio-netd" => {
|
||
vec![String::from("pci")]
|
||
}
|
||
"vesad" | "virtio-gpud" | "redox-drm" => {
|
||
vec![String::from("pci")]
|
||
}
|
||
"ihdad" | "ac97d" | "sb16d" => {
|
||
vec![String::from("pci")]
|
||
}
|
||
"ps2d" => vec![String::from("serio")],
|
||
"i2c-hidd" => vec![String::from("i2c")],
|
||
"dw-acpi-i2cd" | "amd-mp2-i2cd" | "intel-lpss-i2cd" => {
|
||
vec![String::from("acpi"), String::from("i2c")]
|
||
}
|
||
_ => vec![String::from("pci")],
|
||
}
|
||
}
|
||
|
||
#[derive(Deserialize)]
|
||
struct RawDriverToml {
|
||
driver: Vec<RawDriverEntry>,
|
||
}
|
||
|
||
#[derive(Deserialize)]
|
||
struct RawDriverEntry {
|
||
name: String,
|
||
#[serde(default)]
|
||
description: String,
|
||
#[serde(default)]
|
||
priority: i32,
|
||
#[serde(default)]
|
||
command: Vec<String>,
|
||
#[serde(rename = "match")]
|
||
#[serde(default)]
|
||
r#match: Vec<RawDriverMatch>,
|
||
#[serde(default)]
|
||
depends_on: Vec<String>,
|
||
#[serde(default)]
|
||
exclusive_with: Vec<String>,
|
||
#[serde(default)]
|
||
initial_power_state: Option<String>,
|
||
/// Per-driver parameter overrides from the policy layer's
|
||
/// `[driver.params]` table. Each entry becomes a
|
||
/// `REDBEAR_DRIVER_PARAM_<NAME>=<value>` env var on the spawned
|
||
/// child's environment. The schema is a flat string-to-string map;
|
||
/// values are stored verbatim (the driver daemon interprets them
|
||
/// per its own schema). Used by `redbear-driver-policy`'s
|
||
/// `50-amdgpu.toml` and similar per-driver policy files.
|
||
#[serde(default)]
|
||
params: std::collections::BTreeMap<String, String>,
|
||
}
|
||
|
||
#[derive(Deserialize)]
|
||
struct RawLegacyToml {
|
||
drivers: Vec<RawLegacyEntry>,
|
||
}
|
||
|
||
#[derive(Deserialize)]
|
||
struct RawLegacyEntry {
|
||
#[serde(default)]
|
||
name: Option<String>,
|
||
#[serde(default)]
|
||
class: Option<u8>,
|
||
#[serde(default)]
|
||
subclass: Option<u8>,
|
||
#[serde(default)]
|
||
interface: Option<u8>,
|
||
#[serde(default)]
|
||
ids: Option<std::collections::BTreeMap<String, Vec<u16>>>,
|
||
#[serde(default)]
|
||
vendor: Option<u16>,
|
||
#[serde(default)]
|
||
exclusive_with: Vec<String>,
|
||
#[serde(default)]
|
||
device: Option<u16>,
|
||
#[serde(default)]
|
||
device_id_range: Option<RawLegacyRange>,
|
||
#[serde(default)]
|
||
command: Vec<String>,
|
||
}
|
||
|
||
#[derive(Deserialize)]
|
||
struct RawLegacyRange {
|
||
start: u16,
|
||
end: u16,
|
||
}
|
||
|
||
fn convert_legacy(legacy: RawLegacyEntry) -> DriverConfig {
|
||
let name = legacy
|
||
.name
|
||
.unwrap_or_else(|| "(unnamed from legacy pcid.d)".to_string());
|
||
let mut matches: Vec<DriverMatch> = Vec::new();
|
||
|
||
if let (Some(v), Some(d)) = (legacy.vendor, legacy.device) {
|
||
matches.push(DriverMatch {
|
||
vendor: Some(v),
|
||
device: Some(d),
|
||
class: legacy.class,
|
||
subclass: legacy.subclass,
|
||
prog_if: legacy.interface,
|
||
subsystem_vendor: None,
|
||
subsystem_device: None,
|
||
});
|
||
} else if let Some(v) = legacy.vendor {
|
||
matches.push(DriverMatch {
|
||
vendor: Some(v),
|
||
device: None,
|
||
class: legacy.class,
|
||
subclass: legacy.subclass,
|
||
prog_if: legacy.interface,
|
||
subsystem_vendor: None,
|
||
subsystem_device: None,
|
||
});
|
||
}
|
||
|
||
if let Some(ids) = legacy.ids {
|
||
for (vendor_token, dev_list) in ids {
|
||
let vendor_u16 = match u16::from_str_radix(vendor_token.trim_start_matches("0x"), 16) {
|
||
Ok(v) => v,
|
||
Err(e) => {
|
||
log::warn!(
|
||
"convert_legacy: invalid vendor token {:?} for driver `{}`: {}",
|
||
vendor_token,
|
||
name,
|
||
e
|
||
);
|
||
continue;
|
||
}
|
||
};
|
||
for d in dev_list {
|
||
matches.push(DriverMatch {
|
||
vendor: Some(vendor_u16),
|
||
device: Some(d),
|
||
class: legacy.class,
|
||
subclass: legacy.subclass,
|
||
prog_if: legacy.interface,
|
||
subsystem_vendor: None,
|
||
subsystem_device: None,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
if let Some(range) = legacy.device_id_range {
|
||
if range.end >= range.start {
|
||
for d in range.start..=range.end {
|
||
matches.push(DriverMatch {
|
||
vendor: None,
|
||
device: Some(d),
|
||
class: legacy.class,
|
||
subclass: legacy.subclass,
|
||
prog_if: legacy.interface,
|
||
subsystem_vendor: None,
|
||
subsystem_device: None,
|
||
});
|
||
}
|
||
} else {
|
||
log::warn!(
|
||
"convert_legacy: driver `{}` has degenerate device_id_range {}-{}; \
|
||
skipping",
|
||
name,
|
||
range.start,
|
||
range.end
|
||
);
|
||
}
|
||
}
|
||
|
||
if matches.is_empty() {
|
||
log::warn!(
|
||
"convert_legacy: driver `{}` has no matches after conversion; \
|
||
falling back to class-only",
|
||
name
|
||
);
|
||
matches.push(DriverMatch {
|
||
vendor: None,
|
||
device: None,
|
||
class: legacy.class,
|
||
subclass: legacy.subclass,
|
||
prog_if: legacy.interface,
|
||
subsystem_vendor: None,
|
||
subsystem_device: None,
|
||
});
|
||
}
|
||
|
||
DriverConfig {
|
||
name,
|
||
description: String::new(),
|
||
priority: 0,
|
||
command: legacy.command,
|
||
matches,
|
||
depends_on: Vec::new(),
|
||
exclusive_with: legacy.exclusive_with,
|
||
params: Vec::new(),
|
||
initial_power_state: PciPowerState::D0,
|
||
spawned: Mutex::new(HashMap::new()),
|
||
pid_to_device: Mutex::new(HashMap::new()),
|
||
}
|
||
}
|