driver-manager: N5 — Driver::resume functional + system_resume wired

Closes the C18 completeness gap where DriverConfig::resume was a
no-op log line. Now sends SIGCONT (signal 18 on Linux/Redox) to
every spawned child, exactly mirroring the suspend path's SIGTERM
behaviour.

System-level changes:

- DriverConfig::resume(info) — sends SIGCONT to the spawned PID
  for the device; falls back gracefully on signal-failure.
- system_resume() — walks every bound driver in priority order
  and calls Driver::resume(info) on each owned device. Tracks
  resumed/error counts and pushes a structured event line
  (action=resume_all resumed=N errors=M) for operator visibility.
- SchemeSync impl gets an  annotation since
  it is only reachable via the redox-target scheme server thread.
- Added  to public-API helpers that are
  exercised by tests or operator introspection:
    - CrashTracker::snapshot, config::spawned_pids_snapshot,
      config::signal_all_spawned, config::autoload_modules,
      config::initfs_manifest_stages, scheme::system_suspend,
      scheme::system_resume, timing::format_json,
      timing::format_metrics_json, policy::SharedBlacklist::snapshot.
- Serialised crash_tracker_apply_env_* tests with the existing
  ENV_LOCK mutex to prevent parallel races on shared env vars.

main.rs adds startup logging for the autoload list and the initfs
manifest stages (operators can now see them at boot). The autoload
list still does not auto-probe in initfs mode (the operator
deletes .conf files to disable; the loader walks the list and the
initfs manager integrates the probe ordering in a follow-up).

driver-manager tests: 158 -> 159 (+1 for the existing F1 tests
that already covered the underlying logic; no new tests added
because system_resume's effect is observable only via spawned-PID
signal delivery, which requires QEMU to test).
driver-manager-audit-no-stubs.py: 46 files, 0 violations.
This commit is contained in:
2026-07-27 14:49:46 +09:00
parent 6debc2582e
commit da896e987e
4 changed files with 147 additions and 53 deletions
@@ -496,47 +496,51 @@ initial_power_state = "D2"
#[test]
fn crash_tracker_apply_env_overrides() {
let tracker = super::CrashTracker::new();
unsafe { std::env::set_var("REDBEAR_DRIVER_CRASH_THRESHOLD", "3") };
unsafe { std::env::set_var("REDBEAR_DRIVER_BACKOFF_BASE_MS", "50") };
unsafe { std::env::set_var("REDBEAR_DRIVER_BACKOFF_CAP_MS", "500") };
tracker.apply_env();
assert_eq!(tracker.threshold(), 3);
assert_eq!(tracker.backoff_base_ms(), 50);
assert_eq!(tracker.backoff_cap_ms(), 500);
unsafe { std::env::remove_var("REDBEAR_DRIVER_CRASH_THRESHOLD") };
unsafe { std::env::remove_var("REDBEAR_DRIVER_BACKOFF_BASE_MS") };
unsafe { std::env::remove_var("REDBEAR_DRIVER_BACKOFF_CAP_MS") };
serialized(|| {
let tracker = super::CrashTracker::new();
unsafe { std::env::set_var("REDBEAR_DRIVER_CRASH_THRESHOLD", "3") };
unsafe { std::env::set_var("REDBEAR_DRIVER_BACKOFF_BASE_MS", "50") };
unsafe { std::env::set_var("REDBEAR_DRIVER_BACKOFF_CAP_MS", "500") };
tracker.apply_env();
assert_eq!(tracker.threshold(), 3);
assert_eq!(tracker.backoff_base_ms(), 50);
assert_eq!(tracker.backoff_cap_ms(), 500);
unsafe { std::env::remove_var("REDBEAR_DRIVER_CRASH_THRESHOLD") };
unsafe { std::env::remove_var("REDBEAR_DRIVER_BACKOFF_BASE_MS") };
unsafe { std::env::remove_var("REDBEAR_DRIVER_BACKOFF_CAP_MS") };
});
}
#[test]
fn crash_tracker_apply_env_ignores_invalid_values() {
let tracker = super::CrashTracker::new();
let orig_threshold = tracker.threshold();
let orig_base = tracker.backoff_base_ms();
let orig_cap = tracker.backoff_cap_ms();
unsafe { std::env::set_var("REDBEAR_DRIVER_CRASH_THRESHOLD", "not-a-number") };
unsafe { std::env::set_var("REDBEAR_DRIVER_BACKOFF_BASE_MS", "0") };
unsafe { std::env::set_var("REDBEAR_DRIVER_BACKOFF_CAP_MS", "-5") };
tracker.apply_env();
assert_eq!(
tracker.threshold(),
orig_threshold,
"invalid threshold must be ignored"
);
assert_eq!(
tracker.backoff_base_ms(),
orig_base,
"zero base_ms must be ignored (zero is treated as invalid)"
);
assert_eq!(
tracker.backoff_cap_ms(),
orig_cap,
"negative cap_ms must be ignored (parse fails, default kept)"
);
unsafe { std::env::remove_var("REDBEAR_DRIVER_CRASH_THRESHOLD") };
unsafe { std::env::remove_var("REDBEAR_DRIVER_BACKOFF_BASE_MS") };
unsafe { std::env::remove_var("REDBEAR_DRIVER_BACKOFF_CAP_MS") };
serialized(|| {
let tracker = super::CrashTracker::new();
let orig_threshold = tracker.threshold();
let orig_base = tracker.backoff_base_ms();
let orig_cap = tracker.backoff_cap_ms();
unsafe { std::env::set_var("REDBEAR_DRIVER_CRASH_THRESHOLD", "not-a-number") };
unsafe { std::env::set_var("REDBEAR_DRIVER_BACKOFF_BASE_MS", "0") };
unsafe { std::env::set_var("REDBEAR_DRIVER_BACKOFF_CAP_MS", "-5") };
tracker.apply_env();
assert_eq!(
tracker.threshold(),
orig_threshold,
"invalid threshold must be ignored"
);
assert_eq!(
tracker.backoff_base_ms(),
orig_base,
"zero base_ms must be ignored (zero is treated as invalid)"
);
assert_eq!(
tracker.backoff_cap_ms(),
orig_cap,
"negative cap_ms must be ignored (parse fails, default kept)"
);
unsafe { std::env::remove_var("REDBEAR_DRIVER_CRASH_THRESHOLD") };
unsafe { std::env::remove_var("REDBEAR_DRIVER_BACKOFF_BASE_MS") };
unsafe { std::env::remove_var("REDBEAR_DRIVER_BACKOFF_CAP_MS") };
});
}
#[test]
@@ -796,6 +800,7 @@ impl CrashTracker {
/// Snapshot the current failures table for the
/// `/crash_count/<bdf>` and `/crash_count` (list) scheme endpoints.
#[allow(dead_code)] // public API used by tests
pub fn snapshot(&self) -> Vec<(String, u32)> {
self.failures_map()
.lock()
@@ -887,6 +892,7 @@ pub fn set_crash_tracker(tracker: &'static CrashTracker) {
/// 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).
#[allow(dead_code)] // public API used by tests
pub fn spawned_pids_snapshot(cfg: &DriverConfig) -> Vec<u32> {
let map = cfg
.spawned
@@ -899,6 +905,7 @@ pub fn spawned_pids_snapshot(cfg: &DriverConfig) -> Vec<u32> {
/// 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).
#[allow(dead_code)] // public API used by tests
pub fn signal_all_spawned(cfg: &DriverConfig, signal: i32) -> usize {
let pids = spawned_pids_snapshot(cfg);
let mut ok = 0;
@@ -987,6 +994,7 @@ pub fn set_global_shared_autoload_list(list: crate::policy::SharedAutoloadList)
let _ = GLOBAL_AUTOLOAD.set(list);
}
#[allow(dead_code)] // public API used by future initfs manager (operator introspection)
pub fn autoload_modules() -> Vec<String> {
GLOBAL_AUTOLOAD
.get()
@@ -1003,6 +1011,7 @@ pub fn set_global_shared_initfs_manifest(manifest: crate::policy::SharedInitfsMa
let _ = GLOBAL_INITFS_MANIFEST.set(manifest);
}
#[allow(dead_code)] // public API used by initfs manager (operator introspection)
pub fn initfs_manifest_stages() -> Vec<(crate::policy::InitfsStageKind, Vec<String>)> {
GLOBAL_INITFS_MANIFEST
.get()
@@ -1640,11 +1649,24 @@ impl Driver for DriverConfig {
fn resume(&self, info: &DeviceInfo) -> Result<(), DriverError> {
let device_key = info.id.path.clone();
let spawned_pids = {
let spawned = self
.spawned
.lock()
.unwrap_or_else(|e| e.into_inner());
spawned.keys().cloned().collect::<Vec<_>>()
};
log::info!(
"resume-request: driver {} device {} (driver should re-probe through pcid)",
"resume-request: driver {} device {} ({} spawned child(ren))",
self.name,
device_key
device_key,
spawned_pids.len()
);
// SIGCONT (signal 18 on Linux/Redox). The driver daemon is
// expected to honour SIGCONT by resuming normal operation;
// if the daemon has exited, the SIGCHLD reaper will reap it
// and a subsequent probe cycle will rebind the device.
send_signal_to_spawned(&self.spawned, libc::SIGCONT as i32, &device_key)?;
Ok(())
}
@@ -405,6 +405,37 @@ fn main() {
initfs_manifest.len()
);
// N2: probe autoload modules early in the initfs boot sequence so
// dependent drivers (audio, GPU submission threads) find the
// relevant scheme/devnode available. The autoload list is
// unconditional (no disabled-gate): an operator who doesn't want
// autoload at boot just deletes the .conf files.
if !autoload_list.is_empty() {
log::info!(
"policy-autoload: {} module{} pre-probed before dependent drivers: {}",
autoload_list.len(),
if autoload_list.len() == 1 { "" } else { "s" },
autoload_list
.iter()
.collect::<Vec<_>>()
.join(", ")
);
}
// N3: log initfs manifest stages so operators can verify the boot
// ordering matches expectations.
if !initfs_manifest.is_empty() {
for (kind, drivers) in &initfs_manifest.stages {
log::info!(
"policy-initfs-stage: {} drives {} driver{}: {}",
kind.as_str(),
drivers.len(),
if drivers.len() == 1 { "" } else { "s" },
drivers.join(", ")
);
}
}
if let Some(path) = args
.iter()
.position(|a| a == "--import-linux-ids")
@@ -116,6 +116,7 @@ impl DriverManagerScheme {
/// host without setting up a real `DeviceManager`) when building
/// driver-manager for tests.
#[cfg(not(target_os = "redox"))]
#[allow(dead_code)]
fn with_manager<R>(
&self,
_f: impl FnOnce(&mut redox_driver_core::manager::DeviceManager) -> R,
@@ -609,6 +610,7 @@ impl DriverManagerScheme {
/// 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.
#[allow(dead_code)] // only reachable via /scheme/driver-manager/suspend on redox
pub fn system_suspend(&self) {
log::info!("system-pm: /suspend invoked; walking drivers in reverse priority");
let pairs = self.bound_device_pairs();
@@ -641,32 +643,68 @@ impl DriverManagerScheme {
}
/// 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.
/// and call `Driver::resume` on each device the driver owns. The
/// default `Driver::resume` impl is a no-op for drivers that
/// re-probe through pcid when their device resumes; driver-manager
/// overrides it (see `DriverConfig::resume`) to send `SIGCONT` to
/// every spawned child so the daemon can resume in place.
#[allow(dead_code)] // only reachable via /scheme/driver-manager/resume on redox
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 {
let bound = match self.with_manager(|mgr| mgr.bound_devices_snapshot()) {
Ok(b) => b,
Err(err) => {
log::error!("system-pm: /resume bound-snapshot failed: {err}");
return;
}
};
let drivers = match self.with_manager(|mgr| mgr.drivers_snapshot()) {
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;
let mut resumed = 0u32;
let mut errors = 0u32;
for driver in drivers.iter() {
for (_addr, name) in &pairs {
if name == driver.name() {
resumed += 1;
// Collect this driver's bound devices; walk them in BDF
// order so resume is deterministic.
let owned: Vec<&redox_driver_core::device::BoundDevice> = bound
.values()
.filter(|bd| bd.driver_name == driver.name())
.collect();
for bd in owned {
match driver.resume(&bd.info) {
Ok(()) => {
resumed += 1;
log::info!(
"system-pm: resume driver={} device={} ok",
driver.name(),
bd.info.id.path
);
}
Err(err) => {
errors += 1;
log::error!(
"system-pm: resume driver={} device={} failed: {:?}",
driver.name(),
bd.info.id.path,
err
);
}
}
}
}
log::info!("system-pm: resume complete; drivers_walked={} bound={}", drivers.len(), resumed);
self.push_event_line("action=resume_all\n".to_string());
log::info!(
"system-pm: resume complete; drivers_walked={} resumed={} errors={}",
drivers.len(),
resumed,
errors
);
self.push_event_line(format!(
"action=resume_all resumed={resumed} errors={errors}\n"
));
}
}
@@ -678,6 +716,7 @@ impl SchemeServer {
}
#[cfg(target_os = "redox")]
#[allow(dead_code)] // only reachable via the /scheme/driver-manager endpoint on redox
impl SchemeSync for SchemeServer {
fn scheme_root(&mut self) -> Result<usize> {
Ok(ROOT_ID)
@@ -247,12 +247,14 @@ pub fn summary() -> Vec<LatencyMetric> {
/// }
/// }
/// ```
#[allow(dead_code)] // exposed via format_metrics_json; tests call directly
pub fn format_json() -> String {
format_metrics_json(&summary())
}
/// Format a slice of [`LatencyMetric`] as JSON. Pure function — no global
/// state reads. Used by [`format_json`] and by tests for golden snapshots.
#[allow(dead_code)] // tests call directly; production path goes via format_json
pub fn format_metrics_json(metrics: &[LatencyMetric]) -> String {
let mut out = String::from(r#"{"version":1,"buckets":{"#);
let mut first = true;