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:
2026-07-27 12:53:02 +09:00
parent 2d7f7880c9
commit d41c0fd163
@@ -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()),
}