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:
+1
-1
@@ -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
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user