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:
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user