driver-manager: v2.0 — /modalias write path + smart scheduler + exclusive_with + pciehp

Eighth-round integrations of the driver-manager migration's D-phase.
This round closes the remaining gaps from the v1.9 assessment: the
/modalias write path is now wired, the smart scheduler decides
serial-vs-concurrent based on device count, exclusive_with mutual
exclusion works for the CachyOS amdgpu/radeon pattern, pci=nomsi
env var matches Linux's kernel parameter, and pciehp hotplug events
are read from /scheme/pci/pciehp.

scheme.rs:
- Added /modalias write path. Write MODALIAS string, get back the
  matching driver name (via modalias::lookup_modalias). The endpoint
  is now a real read/write interface, not a static hint.

modalias.rs:
- Added lookup_modalias(modalias) that iterates over registered
  drivers (via drivers_registered()) and computes match_modalias for
  each. Returns the driver's name if a match is found.

config.rs:
- Added REGISTERED_DRIVERS static (OnceLock<Vec<DriverConfig>>) and
  set_registered_drivers() so lookup_modalias has real data.
- Added exclusive_with: Vec<String> to DriverConfig + RawDriverEntry +
  RawLegacyEntry + convert_legacy. When two drivers in different
  [[driver]] blocks could match the same PCI ID, the first one (per
  priority) wins and the other is deferred (CachyOS amdgpu/radeon
  mutual-exclusion pattern).
- Added pci=nomsi env var handling: if pci=nomsi or pci=no_msi is set,
  the spawned child gets REDBEAR_DRIVER_PCI_IRQ_MODE=intx_only so
  it cannot use MSI or MSI-X. Matches Linux's pci=nomsi kernel parameter.

main.rs:
- Declared pciehp module. Spawned the pciehp listener thread alongside
  AER (both poll every 500ms, falling back to log-and-no-op when the
  files don't exist).

pciehp.rs (NEW):
- PciehpEvent + PciehpEventKind enum (PresenceDetectChanged,
  AttentionButton, MrlSensorChanged, DataLinkStateChanged, Unknown)
- spawn_pciehp_listener polls /scheme/pci/pciehp every 500ms and
  routes events to bound drivers via the existing hotplug fallback
- 6 unit tests cover parse_pdc_event, parse_attention_button,
  parse_mrl_sensor, parse_dll_state, parse_rejects_missing_device, and
  event_kind_label_round_trips

reaper.rs:
- Fixed the reap_flag_round_trip test to clean up after itself (was
  failing because the shared REAP_FLAG was left set by the previous
  test)

manager.rs:
- Smart scheduler: DeviceManager::enumerate now decides serial vs
  concurrent based on device count. If remaining devices >= 4 AND
  max_concurrent_probes > 1, use the concurrent worker pool
  (ConcurrentDeviceManager::from_manager). Otherwise, use serial.
  The manager's state is synced back from the concurrent path after
  enumeration.

concurrent.rs:
- Added deferred_queue_snapshot() method so the manager can sync
  state back from the concurrent path.

Test totals: 46 tests across 4 crates, all passing.
§ 0.5 audit gate: 0 violations across 38 files.
This commit is contained in:
2026-07-22 22:03:44 +09:00
parent b13d4b30c3
commit fb2922e4fd
11 changed files with 412 additions and 24 deletions
+1 -1
View File
@@ -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
+14 -10
View File
@@ -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 (C0C4) 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 (C0C4) 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,
+1 -1
View File
@@ -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)
@@ -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<ProbeEvent> {
let cap = max_concurrent.max(1);
@@ -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<ProbeEvent> {
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<DeviceInfo> = 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);
}
}
@@ -36,6 +36,7 @@ pub struct DriverConfig {
pub command: Vec<String>,
pub matches: Vec<DriverMatch>,
pub depends_on: Vec<String>,
pub exclusive_with: Vec<String>,
spawned: Mutex<HashMap<String, SpawnedDriver>>,
pid_to_device: Mutex<HashMap<u32, String>>,
}
@@ -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<crate::policy::SharedBlacklist> =
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<Vec<DriverConfig>> =
std::sync::OnceLock::new();
pub fn set_registered_drivers(drivers: Vec<DriverConfig>) {
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<DriverMatch> =
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<RawDriverMatch>,
#[serde(default)]
depends_on: Vec<String>,
#[serde(default)]
exclusive_with: Vec<String>,
}
#[derive(Deserialize)]
@@ -868,6 +924,8 @@ struct RawLegacyEntry {
#[serde(default)]
vendor: Option<u16>,
#[serde(default)]
exclusive_with: Vec<String>,
#[serde(default)]
device: Option<u16>,
#[serde(default)]
device_id_range: Option<RawLegacyRange>,
@@ -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()),
}
@@ -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 =
@@ -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<String> {
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");
}
}
@@ -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<Vec<PciehpEvent>> {
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<PciehpEvent> {
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");
}
}
@@ -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));
@@ -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<OpenResult> {
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<usize> {
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;