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.
This commit is contained in:
2026-07-27 13:05:47 +09:00
parent 6fc0366abb
commit 487d9e6410
2 changed files with 178 additions and 2 deletions
@@ -113,7 +113,12 @@ struct RawDriverMatch {
#[cfg(test)]
mod tests {
use super::{is_process_alive, signal_then_collect, DriverConfig};
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;
@@ -176,6 +181,42 @@ command = ["/usr/bin/redbear-acmd"]
);
}
#[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#"
@@ -540,6 +581,34 @@ fn send_signal_to_spawned(
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 {
@@ -57,6 +57,8 @@ enum HandleKind {
DriverOverride,
Rescan,
Recover,
Suspend,
Resume,
Timing,
}
@@ -106,6 +108,19 @@ impl DriverManagerScheme {
Ok(f(&mut mgr))
}
/// Host-target stub. Real implementation lives in the redox
/// `with_manager` above; this exists so `system_suspend` and
/// `system_resume` can compile (and exercise the code path on
/// host without setting up a real `DeviceManager`) when building
/// driver-manager for tests.
#[cfg(not(target_os = "redox"))]
fn with_manager<R>(
&self,
_f: impl FnOnce(&mut redox_driver_core::manager::DeviceManager) -> R,
) -> std::result::Result<R, &'static str> {
Err("with_manager: host-target stub; no DeviceManager wired on host builds")
}
pub fn bound_device_addresses(&self) -> Vec<String> {
match self.sorted_bound_addresses() {
Ok(addresses) => addresses,
@@ -171,6 +186,8 @@ impl DriverManagerScheme {
["driver_override"] => Ok(HandleKind::DriverOverride),
["rescan"] => Ok(HandleKind::Rescan),
["recover"] => Ok(HandleKind::Recover),
["suspend"] => Ok(HandleKind::Suspend),
["resume"] => Ok(HandleKind::Resume),
["timing"] => Ok(HandleKind::Timing),
["devices", pci_addr] if Self::valid_pci_addr(pci_addr) => {
let _ = self.device_status(pci_addr)?;
@@ -292,6 +309,13 @@ impl DriverManagerScheme {
HandleKind::RemoveId => Ok("write the same fields as new_id to remove\n".to_string()),
HandleKind::Rescan => Ok("write anything to re-enumerate all buses\n".to_string()),
HandleKind::Recover => Ok("write '<pci_addr> <reset_device|rescan_bus|disconnect>' to trigger AER recovery\n".to_string()),
HandleKind::Suspend => Ok(
"write anything to suspend all bound drivers (reverse-priority order)\n"
.to_string(),
),
HandleKind::Resume => Ok(
"write anything to resume all bound drivers (priority order)\n".to_string(),
),
HandleKind::Timing => Ok(crate::timing::format_json()),
}
}
@@ -312,6 +336,8 @@ impl DriverManagerScheme {
HandleKind::DriverOverride => format!("{SCHEME_NAME}:/driver_override"),
HandleKind::Rescan => format!("{SCHEME_NAME}:/rescan"),
HandleKind::Recover => format!("{SCHEME_NAME}:/recover"),
HandleKind::Suspend => format!("{SCHEME_NAME}:/suspend"),
HandleKind::Resume => format!("{SCHEME_NAME}:/resume"),
HandleKind::Timing => format!("{SCHEME_NAME}:/timing"),
}
}
@@ -331,6 +357,8 @@ impl DriverManagerScheme {
| HandleKind::DriverOverride
| HandleKind::Rescan
| HandleKind::Recover
| HandleKind::Suspend
| HandleKind::Resume
| HandleKind::Timing => MODE_FILE | 0o644,
}
}
@@ -517,6 +545,14 @@ impl DriverManagerScheme {
self.dispatch_recovery(addr, action);
Ok(())
}
HandleKind::Suspend => {
self.system_suspend();
Ok(())
}
HandleKind::Resume => {
self.system_resume();
Ok(())
}
_ => Err(Error::new(EACCES)),
}
}
@@ -534,6 +570,75 @@ impl DriverManagerScheme {
}
}
}
/// Walk every bound driver in **reverse priority order** (lowest
/// priority first, highest last) and send `SIGTERM` to every
/// spawned child. Records the dispatch on the events log so
/// operators can correlate with kernel/daemon behaviour.
///
/// This is the C18 PM suspend half of the system-wide PM
/// scheme: it preserves dependency ordering — a high-priority
/// driver that depends on a low-priority one (e.g. ahcid depends
/// on the storage subsystem) is suspended last.
pub fn system_suspend(&self) {
log::info!("system-pm: /suspend invoked; walking drivers in reverse priority");
let pairs = self.bound_device_pairs();
let drivers_result = self.with_manager(|mgr| mgr.drivers_snapshot());
let drivers = match drivers_result {
Ok(d) => d,
Err(err) => {
log::error!("system-pm: /suspend driver-snapshot failed: {err}");
return;
}
};
for driver in drivers.iter().rev() {
for (addr, name) in &pairs {
if name == driver.name() {
let signaled = crate::config::drivers_registered()
.iter()
.find(|d| d.name == driver.name())
.map(|d| crate::config::signal_all_spawned(d, 15 /* SIGTERM */))
.unwrap_or(0);
if signaled > 0 {
log::info!(
"system-pm: suspend driver={} addr={} signalled={}",
name, addr, signaled
);
}
}
}
}
self.push_event_line("action=suspend_all\n".to_string());
}
/// Walk every bound driver in **priority order** (highest first)
/// and log the resume intent. The per-driver `Driver::resume` is
/// currently a no-op (drivers re-probe through pcid when their
/// device resumes); this endpoint exists so operators can record
/// the operator-initiated resume event and so future driver-side
/// resume work has a stable hook to land against.
pub fn system_resume(&self) {
log::info!("system-pm: /resume invoked; walking drivers in priority order");
let drivers_result = self.with_manager(|mgr| mgr.drivers_snapshot());
let drivers = match drivers_result {
Ok(d) => d,
Err(err) => {
log::error!("system-pm: /resume driver-snapshot failed: {err}");
return;
}
};
let pairs = self.bound_device_pairs();
let mut resumed = 0;
for driver in drivers.iter() {
for (_addr, name) in &pairs {
if name == driver.name() {
resumed += 1;
}
}
}
log::info!("system-pm: resume complete; drivers_walked={} bound={}", drivers.len(), resumed);
self.push_event_line("action=resume_all\n".to_string());
}
}
#[cfg(target_os = "redox")]
@@ -657,7 +762,9 @@ impl SchemeSync for SchemeServer {
| HandleKind::RemoveId
| HandleKind::DriverOverride
| HandleKind::Rescan
| HandleKind::Recover => {
| HandleKind::Recover
| HandleKind::Suspend
| HandleKind::Resume => {
let line = String::from_utf8_lossy(buf).trim().to_string();
self.scheme.write_operator(kind, &line)?;
Ok(buf.len())