driver-manager: F6a — C9 Runtime PM spawn wiring
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=<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.
This commit is contained in:
@@ -37,10 +37,52 @@ pub struct DriverConfig {
|
||||
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 {
|
||||
@@ -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<DriverMatch> =
|
||||
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<String>,
|
||||
#[serde(default)]
|
||||
exclusive_with: Vec<String>,
|
||||
#[serde(default)]
|
||||
initial_power_state: Option<String>,
|
||||
}
|
||||
|
||||
#[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()),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user