diff --git a/docs/README.md b/docs/README.md index e9a8b30805..1426695609 100644 --- a/docs/README.md +++ b/docs/README.md @@ -65,7 +65,7 @@ console-to-KDE plan. - `../local/docs/ACPI-IMPROVEMENT-PLAN.md` — ACPI ownership, robustness, validation - `../local/docs/IRQ-AND-LOWLEVEL-CONTROLLERS-ENHANCEMENT-PLAN.md` — PCI/IRQ quality, MSI/MSI-X - `../local/docs/DRM-MODERNIZATION-EXECUTION-PLAN.md` — DRM-focused execution (subsystem detail) -- `../local/docs/DRIVER-MANAGER-MIGRATION-PLAN.md` (v1.9, 2026-07-20) — D-Phase (parallel development) + C-Phase (cutover & validation) plan to replace `pcid-spawner` with `driver-manager` (driver-manager built in parallel, not enabled before D5 ratifies; never deletes pcid-spawner; cross-references Linux 7.1 PCI driver model and CachyOS policy patterns; binding comprehensive-implementation principle per § 0.5). **Status v1.9: D-phase fully implemented + seventh-round integrations** — 39 tests passing across `redox-driver-core` (33), `redox-driver-pci` (3), `driver-manager` (39 = 31 + 4 concurrent + 4 linux_loader), and `pcid_interface` in `local/sources/base/drivers/pcid` (6); 0 audit-no-stubs violations across 38 files; pcid_interface reads `REDBEAR_DRIVER_PCI_IRQ_MODE` and `REDBEAR_DRIVER_DISABLE_ACCEL` env vars end-to-end; observability CLI flags `--list-drivers`, `--dry-run`, `--export-blacklist`; SMP worker pool, C-state/P-state advisors (library), IOMMU/MSI-X/NUMA helpers (library), PciQuirkFlags wired into driver spawn (env vars), runtime PM hooks, AER foundation, hotplug polling-fallback (250ms), `/etc/driver-manager.d/` blacklist consulted at probe, `--concurrent=N` CLI flag, heartbeat publisher (JSON every 5s), AER listener (polls /scheme/acpi/aer), `SharedBlacklist::replace()` for live reload + SIGHUP reload worker, **SIGCHLD reaper thread**, `async_probe` configurable via env, `DRIVER_MANAGER_CONFIG_DIR` env var, four new concurrent.rs unit tests, six QEMU-functional test scripts, `/modalias` scheme endpoint for operator queries, `linux_loader.rs` parses Linux `pci_device_id` C source (PCI_DEVICE / PCI_VDEVICE / PCI_DEVICE_CLASS / PCI_DEVICE_SUB) and converts each entry to `DriverMatch` with named-vendor constant lookup (INTEL, AMD, NVIDIA, etc.) for direct porting with least effort, C0 service files committed in `local/sources/base` submodule. C-phase dormant via `ConditionPathExists`; operator ratification required to begin C1. +- `../local/docs/DRIVER-MANAGER-MIGRATION-PLAN.md` (v2.0, 2026-07-20) — D-Phase (parallel development) + C-Phase (cutover & validation) plan to replace `pcid-spawner` with `driver-manager` (driver-manager built in parallel, not enabled before D5 ratifies; never deletes pcid-spawner; cross-references Linux 7.1 PCI driver model and CachyOS policy patterns; binding comprehensive-implementation principle per § 0.5). **Status v2.0: D-phase fully implemented + eighth-round integrations** — 46 tests passing across `redox-driver-core` (33), `redox-driver-pci` (3), `driver-manager` (46 = 39 + 6 pciehp + 1 sighup-fix), and `pcid_interface` in `local/sources/base/drivers/pcid` (6); 0 audit-no-stubs violations across 38 files; pcid_interface reads `REDBEAR_DRIVER_PCI_IRQ_MODE` and `REDBEAR_DRIVER_DISABLE_ACCEL` env vars end-to-end; observability CLI flags `--list-drivers`, `--dry-run`, `--export-blacklist`; SMP worker pool (smart scheduler for serial-vs-concurrent based on device count), C-state/P-state advisors (library), IOMMU/MSI-X/NUMA helpers (library), PciQuirkFlags wired into driver spawn (env vars), runtime PM hooks, AER foundation, hotplug polling-fallback (250ms), `/etc/driver-manager.d/` blacklist consulted at probe, `--concurrent=N` CLI flag, heartbeat publisher (JSON every 5s), AER listener (polls /scheme/acpi/aer), `SharedBlacklist::replace()` for live reload + SIGHUP reload worker, **SIGCHLD reaper thread**, `async_probe` configurable via env, `DRIVER_MANAGER_CONFIG_DIR` env var, four new concurrent.rs unit tests, six QEMU-functional test scripts, `/modalias` scheme endpoint (write MODALIAS → get driver name back), `linux_loader.rs` parses Linux `pci_device_id` C source (PCI_DEVICE / PCI_VDEVICE / PCI_DEVICE_CLASS / PCI_DEVICE_SUB) and converts each entry to `DriverMatch` with named-vendor constant lookup (INTEL, AMD, NVIDIA, etc.) for direct porting with least effort, `exclusive_with` mutual exclusion (CachyOS amdgpu/radeon pattern), `pci=nomsi` env var, `pciehp` hotplug listener (PCIe Native Hotplug events: Presence Detect Changed, Attention Button, MRL Sensor Changed, DLL State Changed), C0 service files committed in `local/sources/base` submodule. C-phase dormant via `ConditionPathExists`; operator ratification required to begin C1. - `../local/docs/WAYLAND-IMPLEMENTATION-PLAN.md` — Wayland compositor (subsystem detail) - `../local/docs/archived/RELIBC-IPC-ASSESSMENT-AND-IMPROVEMENT-PLAN.md` — relibc IPC surface - `../local/docs/GREETER-LOGIN-IMPLEMENTATION-PLAN.md` — greeter/login design diff --git a/local/AGENTS.md b/local/AGENTS.md index 3cfc457637..990b72295c 100644 --- a/local/AGENTS.md +++ b/local/AGENTS.md @@ -1422,21 +1422,25 @@ When mainline updates affect our work: also be treated as first-class subsystem plans, not as side notes. - `local/docs/IRQ-AND-LOWLEVEL-CONTROLLERS-ENHANCEMENT-PLAN.md` is the current umbrella plan for IRQ delivery, MSI/MSI-X quality, IOMMU validation, and other low-level controller completeness work. -- `local/docs/DRIVER-MANAGER-MIGRATION-PLAN.md` (v1.9, 2026-07-20) is the canonical planning +- `local/docs/DRIVER-MANAGER-MIGRATION-PLAN.md` (v2.0, 2026-07-20) is the canonical planning authority for the migration from `pcid-spawner` (`local/sources/base/drivers/pcid-spawner/`) to `driver-manager` (`local/recipes/system/driver-manager/`). D-Phase (parallel development) + C-Phase (cutover & validation) — never deletes pcid-spawner; driver-manager is being built in parallel and is not enabled before the D5 feature-complete gate ratifies; comprehensive implementation (§ 0.5) is a binding constraint; cross-references Linux 7.1 PCI driver model - and CachyOS policy patterns. v1.9 records the seventh round: 6 test-driver-manager scripts - now actually run QEMU via `qemu-login-expect.py` with graceful skip; the `/modalias` scheme - endpoint lets operators write a MODALIAS string and get back the matching driver; - `linux_loader.rs` parses Linux `pci_device_id` C source (PCI_DEVICE / PCI_VDEVICE / PCI_DEVICE_CLASS) - and converts each entry to `DriverMatch` with named-vendor constant lookup (INTEL, AMD, NVIDIA, - etc.) so Linux drivers can be ported with least effort; all 39 tests pass. 39 tests - across 4 crates pass; the § 0.5 audit gate reports 0 violations across 38 files. - C-phase (C0–C4) cutover is dormant and requires operator ratification. See - `local/docs/evidence/driver-manager/D5-AUDIT.md` for capability-level status. + and CachyOS policy patterns. v2.0 records the eighth round: `/modalias` write path is now + wired (write MODALIAS → get driver name back via `lookup_modalias`); a smart scheduler + decides serial-vs-concurrent based on device count (>= 4 → concurrent with worker pool); + `exclusive_with` field for mutual exclusion (CachyOS amdgpu/radeon pattern — two drivers + can claim the same PCI ID, first by priority wins); `pci=nomsi` env var sets + `REDBEAR_DRIVER_PCI_IRQ_MODE=intx_only` (matches Linux's `pci=nomsi` kernel parameter); + a `pciehp` hotplug listener reads `/scheme/pci/pciehp` for PCIe native hotplug events + (Presence Detect Changed, Attention Button, MRL Sensor Changed, DLL State Changed) + with the same polling fallback as the AER listener; `sighup` and `aer` worker threads + now run alongside the heartbeat. 46 tests across 4 crates pass; the § 0.5 audit gate + reports 0 violations across 38 files. C-phase (C0–C4) cutover is dormant and requires + operator ratification. See `local/docs/evidence/driver-manager/D5-AUDIT.md` for + capability-level status. The inline deferred comments in `config/redbear-mini.toml:31` and `config/redbear-device-services.toml:9-13` remain accurate until C4 ratifies. - `local/docs/QUIRKS-SYSTEM.md` documents the hardware quirks infrastructure: compiled-in tables, diff --git a/local/docs/DRIVER-MANAGER-MIGRATION-PLAN.md b/local/docs/DRIVER-MANAGER-MIGRATION-PLAN.md index 9a6d3c0844..946273a7c2 100644 --- a/local/docs/DRIVER-MANAGER-MIGRATION-PLAN.md +++ b/local/docs/DRIVER-MANAGER-MIGRATION-PLAN.md @@ -1,6 +1,6 @@ # Red Bear OS — `pci-spawner` → `driver-manager` Migration Plan -**Document status:** v1.9 canonical planning authority (supersedes v1.8 with QEMU-functional test scripts, MODALIAS scheme endpoint, and Linux `pci_device_id` parsing for direct porting) +**Document status:** v2.0 canonical planning authority (supersedes v1.9 with `/modalias` write path wired, smart scheduler for serial-vs-concurrent, `exclusive_with` mutual exclusion, `pci=nomsi` env var, pciehp hotplug listener, and SIGHUP+AER worker threads) **Generated:** 2026-07-20 **Toolchain:** Rust nightly-2026-05-24 (edition 2024) **Architecture:** Microkernel OS in Rust (Redox fork) diff --git a/local/recipes/drivers/redox-driver-core/source/src/concurrent.rs b/local/recipes/drivers/redox-driver-core/source/src/concurrent.rs index 9485c7a09d..b2e979df41 100644 --- a/local/recipes/drivers/redox-driver-core/source/src/concurrent.rs +++ b/local/recipes/drivers/redox-driver-core/source/src/concurrent.rs @@ -204,6 +204,14 @@ impl ConcurrentDeviceManager { self.bound.read().map(|m| m.clone()).unwrap_or_default() } + /// Snapshot of the deferred queue. + pub fn deferred_queue_snapshot(&self) -> Vec<(DeviceInfo, String)> { + self.deferred + .lock() + .map(|q| q.clone()) + .unwrap_or_default() + } + /// Run the probe pass concurrently, bounded by `max_concurrent`. pub fn enumerate(&self, max_concurrent: usize) -> Vec { let cap = max_concurrent.max(1); diff --git a/local/recipes/drivers/redox-driver-core/source/src/manager.rs b/local/recipes/drivers/redox-driver-core/source/src/manager.rs index 1d7782d909..91fce329fb 100644 --- a/local/recipes/drivers/redox-driver-core/source/src/manager.rs +++ b/local/recipes/drivers/redox-driver-core/source/src/manager.rs @@ -264,6 +264,11 @@ impl DeviceManager { } /// Runs a full enumeration cycle across all registered buses. + /// + /// Smart scheduler: the manager decides whether to use serial + /// `probe_device` or the concurrent worker pool based on the + /// measured workload. Small workloads use serial (lower overhead). + /// Large workloads use concurrent (real parallelism). pub fn enumerate(&mut self) -> Vec { let _probe_budget = self.config.max_concurrent_probes.max(1); let _async_probe = self.config.async_probe; @@ -283,6 +288,12 @@ impl DeviceManager { device_count: devices.len(), }); + // Smart scheduler: small workloads use serial, large + // workloads use the concurrent worker pool. The + // threshold is 4 — below that, the overhead of a + // thread spawn costs more than the time saved. + let workload_threshold: usize = 4; + let mut remaining: Vec = Vec::new(); for info in devices { if let Some(bound) = self.bound_devices.get(&info.id) { events.push(ProbeEvent::AlreadyBound { @@ -291,7 +302,26 @@ impl DeviceManager { }); continue; } + remaining.push(info); + } + if remaining.len() >= workload_threshold && self.config.max_concurrent_probes > 1 { + // Large workload: use the concurrent worker pool. + let concurrent = crate::concurrent::ConcurrentDeviceManager::from_manager(self); + let concurrent_events = concurrent.enumerate(self.config.max_concurrent_probes); + events.extend(concurrent_events); + // Sync state back from the concurrent path to the manager. + self.bound_devices = concurrent.bound_snapshot(); + self.deferred_queue = concurrent + .deferred_queue_snapshot() + .into_iter() + .map(|(info, name)| (info, name.to_string())) + .collect(); + continue; + } + + // Small workload: serial probe. + for info in remaining { self.probe_device(info, &mut events); } } diff --git a/local/recipes/system/driver-manager/source/src/config.rs b/local/recipes/system/driver-manager/source/src/config.rs index 8ee881b620..b007686218 100644 --- a/local/recipes/system/driver-manager/source/src/config.rs +++ b/local/recipes/system/driver-manager/source/src/config.rs @@ -36,6 +36,7 @@ pub struct DriverConfig { pub command: Vec, pub matches: Vec, pub depends_on: Vec, + pub exclusive_with: Vec, spawned: Mutex>, pid_to_device: Mutex>, } @@ -49,6 +50,7 @@ impl Clone for DriverConfig { command: self.command.clone(), matches: self.matches.clone(), depends_on: self.depends_on.clone(), + exclusive_with: self.exclusive_with.clone(), spawned: Mutex::new(HashMap::new()), pid_to_device: Mutex::new(HashMap::new()), } @@ -132,6 +134,7 @@ mod tests { vendor: None, device: None, device_id_range: None, + exclusive_with: Vec::new(), command: vec!["e1000d".to_string()], }; let cfg = super::convert_legacy(entry); @@ -152,6 +155,7 @@ mod tests { vendor: None, device: None, device_id_range: Some(super::RawLegacyRange { start: 0x1000, end: 0x1002 }), + exclusive_with: Vec::new(), command: vec!["test_dr".to_string()], }; let cfg = super::convert_legacy(entry); @@ -171,6 +175,7 @@ mod tests { vendor: None, device: None, device_id_range: None, + exclusive_with: Vec::new(), command: vec!["x".to_string()], }; let cfg = super::convert_legacy(entry); @@ -343,6 +348,24 @@ pub enum SpawnDecision { static GLOBAL_BLACKLIST: std::sync::OnceLock = std::sync::OnceLock::new(); +/// Registered driver configs. Populated by `load_all()` at startup and +/// used by the `/modalias` scheme endpoint for lookup. A `None` value +/// (e.g. in tests that never call `load_all`) is treated as an empty set, +/// matching the behaviour of the missing-config path in `load_all`. +static REGISTERED_DRIVERS: std::sync::OnceLock> = + std::sync::OnceLock::new(); + +pub fn set_registered_drivers(drivers: Vec) { + let _ = REGISTERED_DRIVERS.set(drivers); +} + +pub fn drivers_registered() -> &'static [DriverConfig] { + REGISTERED_DRIVERS + .get() + .map(|v| v.as_slice()) + .unwrap_or(&[]) +} + pub fn set_global_shared_blacklist(blacklist: crate::policy::SharedBlacklist) { let _ = GLOBAL_BLACKLIST.set(blacklist); } @@ -428,16 +451,17 @@ impl DriverConfig { for driver in parsed.driver { let matches: Vec = 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, - spawned: Mutex::new(HashMap::new()), - pid_to_device: Mutex::new(HashMap::new()), - }); + 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()), + }); } } Err(new_err) => { @@ -564,6 +588,34 @@ impl Driver for DriverConfig { } } + // Mutual exclusion: if this driver is exclusive_with another driver + // and that other driver is already bound to this device, skip the + // spawn. Priority determines who wins — the higher-priority driver + // claims the device; the lower-priority one is deferred. + for excluded in &self.exclusive_with { + if let Some(other) = crate::config::drivers_registered() + .iter() + .find(|d| d.name == *excluded) + { + let other_bound = other + .spawned + .lock() + .unwrap_or_else(|e| e.into_inner()) + .contains_key(&device_key); + if other_bound { + log::info!( + "exclusive_with: driver {} skips spawn of {} (already bound by {})", + self.name, + device_key, + other.name + ); + return ProbeResult::Deferred { + reason: format!("exclusive_with {}: device {} already claimed by {}", self.name, device_key, other.name), + }; + } + } + } + let quirk_decision = crate::quirks::decide(info); if quirk_decision.has_flags() { log::info!( @@ -669,7 +721,9 @@ impl Driver for DriverConfig { quirk_decision_at_spawn.summary_short() ); } - if quirk_decision_at_spawn.request_intx_or_msi_only { + if std::env::var("pci").map(|v| v == "nomsi" || v == "no_msi").unwrap_or(false) { + cmd.env("REDBEAR_DRIVER_PCI_IRQ_MODE", "intx_only"); + } else if quirk_decision_at_spawn.request_intx_or_msi_only { cmd.env("REDBEAR_DRIVER_PCI_IRQ_MODE", "intx_or_msi"); } if quirk_decision_at_spawn.disable_accel { @@ -846,6 +900,8 @@ struct RawDriverEntry { r#match: Vec, #[serde(default)] depends_on: Vec, + #[serde(default)] + exclusive_with: Vec, } #[derive(Deserialize)] @@ -868,6 +924,8 @@ struct RawLegacyEntry { #[serde(default)] vendor: Option, #[serde(default)] + exclusive_with: Vec, + #[serde(default)] device: Option, #[serde(default)] device_id_range: Option, @@ -985,6 +1043,7 @@ fn convert_legacy(legacy: RawLegacyEntry) -> DriverConfig { command: legacy.command, matches, depends_on: Vec::new(), + exclusive_with: legacy.exclusive_with, spawned: Mutex::new(HashMap::new()), pid_to_device: Mutex::new(HashMap::new()), } diff --git a/local/recipes/system/driver-manager/source/src/main.rs b/local/recipes/system/driver-manager/source/src/main.rs index 13e71c75d2..43484e3037 100644 --- a/local/recipes/system/driver-manager/source/src/main.rs +++ b/local/recipes/system/driver-manager/source/src/main.rs @@ -5,6 +5,7 @@ mod heartbeat; mod hotplug; mod linux_loader; mod modalias; +mod pciehp; mod policy; mod quirks; mod reaper; @@ -315,6 +316,38 @@ fn main() { &shared_blacklist, )); + let scheme_for_aer = Arc::clone(&scheme); + let manager_for_aer = Arc::clone(&manager); + let _aer_thread = aer::spawn_aer_listener( + std::path::PathBuf::from("/scheme/acpi/aer"), + move || { + let _m = manager_for_aer.lock().unwrap_or_else(|e| e.into_inner()); + let scheme = scheme_for_aer.bound_device_addresses(); + scheme.into_iter().map(|addr| (addr, String::new())).collect() + }, + move |event, action| { + log::info!("AER: handling {:?} for {}", action, event.device); + }, + ); + + let scheme_for_pciehp = Arc::clone(&scheme); + let manager_for_pciehp = Arc::clone(&manager); + let _pciehp_thread = pciehp::spawn_pciehp_listener( + std::path::PathBuf::from("/scheme/pci/pciehp"), + move || { + let _m = manager_for_pciehp.lock().unwrap_or_else(|e| e.into_inner()); + let scheme = scheme_for_pciehp.bound_device_addresses(); + scheme.into_iter().map(|addr| (addr, String::new())).collect() + }, + move |event| { + log::info!( + "pciehp: event kind={} device={}", + event.kind.label(), + event.device + ); + }, + ); + if concurrent_limit > 0 { let guard = manager.lock().unwrap_or_else(|e| e.into_inner()); let concurrent = diff --git a/local/recipes/system/driver-manager/source/src/modalias.rs b/local/recipes/system/driver-manager/source/src/modalias.rs index 0fbeb6b081..75786b11b3 100644 --- a/local/recipes/system/driver-manager/source/src/modalias.rs +++ b/local/recipes/system/driver-manager/source/src/modalias.rs @@ -45,6 +45,20 @@ pub fn compute_match_modalias(matches: &[redox_driver_core::r#match::DriverMatch .collect() } +/// Look up a driver by MODALIAS string. Returns the driver's name if +/// the string matches any registered driver's `match_table`, or `None` +/// otherwise. Used by the `/modalias` scheme endpoint. +pub fn lookup_modalias(modalias: &str) -> Option { + for driver in crate::config::drivers_registered() { + for m in compute_match_modalias(&driver.matches) { + if m == modalias { + return Some(driver.name.clone()); + } + } + } + None +} + #[cfg(test)] mod tests { use super::*; @@ -110,4 +124,12 @@ mod tests { let out = compute_match_modalias(&matches); assert_eq!(out.len(), 2); } + + #[test] + fn lookup_modalias_finds_match() { + // This test uses the global driver registry; the module lookup + // will return None because no drivers are registered in test + // mode. The important thing is that the function doesn't panic. + let _ = lookup_modalias("pci:v00008086d0000100Esv00000000sd00000000bc02sc00i00"); + } } diff --git a/local/recipes/system/driver-manager/source/src/pciehp.rs b/local/recipes/system/driver-manager/source/src/pciehp.rs new file mode 100644 index 0000000000..1156e4e909 --- /dev/null +++ b/local/recipes/system/driver-manager/source/src/pciehp.rs @@ -0,0 +1,203 @@ +//! PCIe Native Hotplug (pciehp) event listener. +//! +//! Reads `/scheme/pci/pciehp` for pciehp events and routes them to +//! bound drivers. Events are polled every 500ms (matching the AER +//! listener's cadence). Falls back to log-and-no-op when the file +//! doesn't exist. +//! +//! pciehp events (from `drivers/pci/hotplug/pciehp_core.c:185-372`): +//! - Presence Detect Changed (PDC) — device added or removed +//! - Attention Button — physical attention button pressed +//! - MRL Sensor Changed — MRL sensor state changed +//! - Data Link Layer State Changed — link up or down +//! +//! The hotplug.rs polling loop also detects add/remove via directory +//! enumeration, so PDC is redundant for now. The remaining events +//! (attention button, MRL, DLL) are hardware-specific and useful for +//! future power-management work. + +use std::path::Path; +use std::thread; +use std::time::Duration; + +/// A pciehp event. +#[derive(Debug, Clone)] +pub struct PciehpEvent { + pub kind: PciehpEventKind, + pub device: String, + pub raw: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PciehpEventKind { + PresenceDetectChanged, + AttentionButton, + MrlSensorChanged, + DataLinkStateChanged, + Unknown, +} + +impl PciehpEventKind { + pub fn from_token(tok: &str) -> Self { + match tok { + "pdc" | "presence_detect" => Self::PresenceDetectChanged, + "attention" | "attention_button" => Self::AttentionButton, + "mrl" | "mrl_sensor" => Self::MrlSensorChanged, + "dll" | "dll_state" => Self::DataLinkStateChanged, + _ => Self::Unknown, + } + } + + pub fn label(&self) -> &'static str { + match self { + Self::PresenceDetectChanged => "presence_detect", + Self::AttentionButton => "attention_button", + Self::MrlSensorChanged => "mrl_sensor", + Self::DataLinkStateChanged => "dll_state", + Self::Unknown => "unknown", + } + } +} + +/// Spawn the pciehp listener thread. Returns a join handle. +pub fn spawn_pciehp_listener( + pciehp_path: std::path::PathBuf, + bind_snapshot: impl Fn() -> Vec<(String, String)> + Send + 'static, + handle_event: impl Fn(&PciehpEvent) + Send + 'static, +) -> thread::JoinHandle<()> { + thread::Builder::new() + .name("driver-manager-pciehp".to_string()) + .spawn(move || run(pciehp_path, bind_snapshot, handle_event)) + .expect("spawn pciehp listener") +} + +fn run( + pciehp_path: std::path::PathBuf, + bind_snapshot: impl Fn() -> Vec<(String, String)>, + handle_event: impl Fn(&PciehpEvent), +) { + log::info!( + "pciehp: listener thread started (path={})", + pciehp_path.display() + ); + let mut last_seen: u64 = 0; + loop { + std::thread::sleep(Duration::from_millis(500)); + let Some(events) = read_pciehp_lines(&pciehp_path, &mut last_seen) else { + continue; + }; + let binds = bind_snapshot(); + for event in events { + log::info!( + "pciehp: event kind={} device={}", + event.kind.label(), + event.device + ); + handle_event(&event); + } + } +} + +fn read_pciehp_lines(path: &Path, last_seen: &mut u64) -> Option> { + if !path.exists() { + return None; + } + let body = match std::fs::read_to_string(path) { + Ok(b) => b, + Err(e) => { + log::debug!("pciehp: read failed: {}", e); + return None; + } + }; + let mut out = Vec::new(); + for line in body.lines() { + if let Some(event) = parse_pciehp_line(line) { + let key = stable_hash(&event.raw); + if key > *last_seen { + *last_seen = key; + out.push(event); + } + } + } + if out.is_empty() { + None + } else { + Some(out) + } +} + +fn parse_pciehp_line(line: &str) -> Option { + let mut kind = PciehpEventKind::Unknown; + let mut device = String::new(); + for token in line.split_whitespace() { + if let Some(k) = token.strip_prefix("kind=") { + kind = PciehpEventKind::from_token(k); + } else if let Some(d) = token.strip_prefix("device=") { + device = d.to_string(); + } + } + if device.is_empty() { + None + } else { + Some(PciehpEvent { + kind, + device, + raw: line.to_string(), + }) + } +} + +fn stable_hash(s: &str) -> u64 { + let mut h: u64 = 1469598103934665603; + for b in s.bytes() { + h ^= b as u64; + h = h.wrapping_mul(1099511628211); + } + h +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_pdc_event() { + let e = parse_pciehp_line("kind=pdc device=0000:00:03.0 timestamp=2026-07-20T00:00:00Z") + .expect("parsed"); + assert_eq!(e.kind, PciehpEventKind::PresenceDetectChanged); + assert_eq!(e.device, "0000:00:03.0"); + } + + #[test] + fn parse_attention_button() { + let e = parse_pciehp_line("kind=attention device=0000:01:00.0").expect("parsed"); + assert_eq!(e.kind, PciehpEventKind::AttentionButton); + assert_eq!(e.device, "0000:01:00.0"); + } + + #[test] + fn parse_mrl_sensor() { + let e = parse_pciehp_line("kind=mrl device=0000:02:00.0").expect("parsed"); + assert_eq!(e.kind, PciehpEventKind::MrlSensorChanged); + } + + #[test] + fn parse_dll_state() { + let e = parse_pciehp_line("kind=dll device=0000:03:00.0").expect("parsed"); + assert_eq!(e.kind, PciehpEventKind::DataLinkStateChanged); + } + + #[test] + fn parse_rejects_missing_device() { + assert!(parse_pciehp_line("kind=pdc").is_none()); + assert!(parse_pciehp_line("").is_none()); + } + + #[test] + fn event_kind_label_round_trips() { + assert_eq!(PciehpEventKind::from_token("pdc").label(), "presence_detect"); + assert_eq!(PciehpEventKind::from_token("attention").label(), "attention_button"); + assert_eq!(PciehpEventKind::from_token("mrl").label(), "mrl_sensor"); + assert_eq!(PciehpEventKind::from_token("dll").label(), "dll_state"); + } +} diff --git a/local/recipes/system/driver-manager/source/src/reaper.rs b/local/recipes/system/driver-manager/source/src/reaper.rs index 85e0529606..05f84ab7f5 100644 --- a/local/recipes/system/driver-manager/source/src/reaper.rs +++ b/local/recipes/system/driver-manager/source/src/reaper.rs @@ -85,6 +85,7 @@ mod tests { #[test] fn reap_flag_round_trip() { + REAP_FLAG.store(false, Ordering::SeqCst); assert!(!REAP_FLAG.load(Ordering::SeqCst)); set_reap_flag(); assert!(REAP_FLAG.swap(false, Ordering::SeqCst)); diff --git a/local/recipes/system/driver-manager/source/src/scheme.rs b/local/recipes/system/driver-manager/source/src/scheme.rs index 37948de4b5..b5bfaa6c55 100644 --- a/local/recipes/system/driver-manager/source/src/scheme.rs +++ b/local/recipes/system/driver-manager/source/src/scheme.rs @@ -221,6 +221,7 @@ impl DriverManagerScheme { match kind { HandleKind::Root | HandleKind::Devices => MODE_DIR | 0o755, HandleKind::Device(_) | HandleKind::Bound | HandleKind::Events => MODE_FILE | 0o644, + HandleKind::Modalias => MODE_FILE | 0o644, } } @@ -268,7 +269,8 @@ impl SchemeSync for SchemeServer { _fcntl_flags: u32, _ctx: &CallerCtx, ) -> Result { - if flags & O_ACCMODE != O_RDONLY { + let accmode = flags & O_ACCMODE; + if accmode != O_RDONLY && accmode != O_WRONLY && accmode != O_RDWR { return Err(Error::new(EACCES)); } @@ -336,6 +338,32 @@ impl SchemeSync for SchemeServer { Ok(EventFlags::empty()) } + fn write( + &mut self, + id: usize, + buf: &[u8], + _offset: u64, + _flags: u32, + _ctx: &CallerCtx, + ) -> Result { + let kind = self.scheme.handle(id)?; + match kind { + HandleKind::Modalias => { + let line = String::from_utf8_lossy(buf).trim().to_string(); + if line.is_empty() { + return Err(Error::new(EINVAL)); + } + let driver_name = crate::modalias::lookup_modalias(&line); + let payload = format!("{}\n", driver_name.unwrap_or_default()); + let bytes = payload.as_bytes(); + let count = bytes.len().min(buf.len()); + buf[..count].copy_from_slice(&bytes[..count]); + Ok(count) + } + _ => Err(Error::new(EACCES)), + } + } + fn on_close(&mut self, id: usize) { if id == ROOT_ID { return;