487d9e6410
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.
1449 lines
49 KiB
Rust
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()),
|
|
}
|
|
}
|