From d41c0fd16336aa9abbe4de43b0ccc0a0432671f9 Mon Sep 17 00:00:00 2001 From: vasilito Date: Mon, 27 Jul 2026 12:53:02 +0900 Subject: [PATCH] =?UTF-8?q?driver-manager:=20F6a=20=E2=80=94=20C9=20Runtim?= =?UTF-8?q?e=20PM=20spawn=20wiring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the C9 capability gap: driver-manager now honours a per-driver initial_power_state via a new TOML key. When the value is non-default (D3hot), driver-manager emits REDBEAR_DRIVER_INITIAL_POWER_STATE= in the spawned daemon's env so the daemon can call set_power_state on its granted channel. Mirrors Linux's pci_power_state (include/linux/pci.h). Supported values: D0 (default), D3hot. Unknown values default to D0 with a WARN log so a typo never aborts config loading. Rationale for env-var handoff (vs direct pcid call): the spawned daemon already holds the granted channel and can call pcid directly. Driver-manager doesn't need to extend pcid_interface for a feature the driver can use itself. The env var is the contract; the daemon implementation can land in its own crate. DriverConfig gains: - field: initial_power_state: PciPowerState (default D0) - TOML key: initial_power_state = "D3hot" - legacy TOML converter defaults to D0 (no migration path needed) Tests (7 new): - pci_power_state_from_toml_round_trips (D0 / D3hot) - pci_power_state_from_toml_rejects_unknown_values (D1/D2/D3cold/lower-case/empty) - pci_power_state_is_default_for_d0_only - pci_power_state_as_str_matches_linux_pci_power_state_names - load_all_parses_initial_power_state (D3hot from TOML) - load_all_defaults_to_d0_when_initial_power_state_absent - load_all_warns_and_defaults_on_invalid_initial_power_state 132 driver-manager tests pass. --- .../driver-manager/source/src/config.rs | 235 +++++++++++++++++- 1 file changed, 223 insertions(+), 12 deletions(-) diff --git a/local/recipes/system/driver-manager/source/src/config.rs b/local/recipes/system/driver-manager/source/src/config.rs index 6a801bb195..d527562cf0 100644 --- a/local/recipes/system/driver-manager/source/src/config.rs +++ b/local/recipes/system/driver-manager/source/src/config.rs @@ -37,10 +37,52 @@ pub struct DriverConfig { pub matches: Vec, pub depends_on: Vec, pub exclusive_with: Vec, + /// 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=` in the child's env so + /// the daemon can call `set_power_state` on its granted channel. + pub initial_power_state: PciPowerState, spawned: Mutex>, pid_to_device: Mutex>, } +/// 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 { + 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 { @@ -51,6 +93,7 @@ impl Clone for DriverConfig { 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()), } @@ -196,7 +239,7 @@ command = ["/usr/bin/redbear-acmd"] assert_eq!(cfg.matches[2].device, Some(0x1002)); } - #[test] +#[test] fn empty_legacy_does_not_panic_falls_back_to_class() { let entry = super::RawLegacyEntry { name: Some("emptily".to_string()), @@ -215,6 +258,141 @@ command = ["/usr/bin/redbear-acmd"] 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(|| { @@ -483,17 +661,33 @@ impl DriverConfig { for driver in parsed.driver { let matches: Vec = driver.r#match.into_iter().map(DriverMatch::from).collect(); - 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, - spawned: Mutex::new(HashMap::new()), - pid_to_device: Mutex::new(HashMap::new()), - }); + 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) => { @@ -791,6 +985,20 @@ impl Driver for DriverConfig { 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!( @@ -1020,6 +1228,8 @@ struct RawDriverEntry { depends_on: Vec, #[serde(default)] exclusive_with: Vec, + #[serde(default)] + initial_power_state: Option, } #[derive(Deserialize)] @@ -1162,6 +1372,7 @@ fn convert_legacy(legacy: RawLegacyEntry) -> DriverConfig { 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()), }