diff --git a/local/recipes/system/driver-manager/source/src/config.rs b/local/recipes/system/driver-manager/source/src/config.rs index d527562cf0..2e054bafa9 100644 --- a/local/recipes/system/driver-manager/source/src/config.rs +++ b/local/recipes/system/driver-manager/source/src/config.rs @@ -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 { + 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 { diff --git a/local/recipes/system/driver-manager/source/src/scheme.rs b/local/recipes/system/driver-manager/source/src/scheme.rs index 833958f202..e03861bd59 100644 --- a/local/recipes/system/driver-manager/source/src/scheme.rs +++ b/local/recipes/system/driver-manager/source/src/scheme.rs @@ -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( + &self, + _f: impl FnOnce(&mut redox_driver_core::manager::DeviceManager) -> R, + ) -> std::result::Result { + Err("with_manager: host-target stub; no DeviceManager wired on host builds") + } + pub fn bound_device_addresses(&self) -> Vec { 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 ' ' 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())