Files
RedBear-OS/local/recipes/system/driver-manager/source/src/config.rs
T
vasilito 487d9e6410 driver-manager: F6d — C18 PM suspend ordering (system_suspend / system_resume)
Closes the C18 capability gap: manager-mediated system PM with
priority-ordered iteration.

Two new /scheme/driver-manager endpoints:
- /suspend (write): walk bound drivers in reverse priority order
  (lowest first) and SIGTERM each spawned PID. Dependency
  preservation: a high-priority driver that depends on a low-
  priority one is suspended last so the latter can keep
  servicing traffic until the former drains.
- /resume (write): walk bound drivers in priority order (highest
  first). Per-driver Driver::resume is currently a no-op (drivers
  re-probe through pcid on resume); the endpoint exists to record
  the operator-initiated event and to give future driver-side
  resume work a stable hook.

DriverConfig gains two pub helpers (cfg::spawned_pids_snapshot
and cfg::signal_all_spawned) that the system PM endpoints call.
The host-target cfg(with_manager stub) lets system_suspend /
system_resume compile on host without a real DeviceManager (the
error path is logged and the call returns cleanly).

Tests (2 new):
- spawned_pids_snapshot_returns_empty_for_unbound_driver
- signal_all_spawned_returns_zero_for_unbound_driver

134 driver-manager tests pass.
2026-07-27 13:05:47 +09:00

1449 lines
49 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 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;
#[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>,
/// 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 sets
/// `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(),
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;
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(),
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(),
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_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);
});
}
#[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(())
}
/// 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).
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).
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)
}
/// 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,
};
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,
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)
}
/// Remove a dead-pid entry from both `spawned` and `pid_to_device`
/// maps. Called by the SIGCHLD reaper when a spawned child has
/// been observed as exited. Idempotent — a no-op if the pid is not
/// present.
pub fn reap_pid(&self, pid: u32) {
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);
}
}
}
}
}
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;
}
}
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);
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());
}
ProbeResult::Bound
}
Err(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();
log::info!(
"resume-request: driver {} device {} (driver should re-probe through pcid)",
self.name,
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>,
}
#[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,
initial_power_state: PciPowerState::D0,
spawned: Mutex::new(HashMap::new()),
pid_to_device: Mutex::new(HashMap::new()),
}
}