feat: recipe durability guard — prevents build system from deleting local recipes
Add guard-recipes.sh with four modes: - --verify: check all local/recipes have correct symlinks into recipes/ - --fix: repair broken symlinks (run before builds) - --save-all: snapshot all recipe.toml into local/recipes/ - --restore: recreate all symlinks from local/recipes/ (run after sync-upstream) Wired into apply-patches.sh (post-patch) and sync-upstream.sh (post-sync). This prevents the build system from deleting recipe files during cargo cook, make distclean, or upstream source refresh.
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::device::DeviceInfo;
|
||||
#[cfg(feature = "hotplug")]
|
||||
use crate::hotplug::HotplugSubscription;
|
||||
|
||||
/// A hardware bus that can enumerate devices.
|
||||
pub trait Bus: Send + Sync {
|
||||
/// Returns a human-readable bus name such as `"pci"`, `"usb"`, or `"acpi"`.
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// Enumerates all devices currently visible on this bus.
|
||||
///
|
||||
/// Implementations must be safe to call repeatedly so that the manager can perform
|
||||
/// re-scans after topology changes or deferred-probe retries.
|
||||
fn enumerate_devices(&self) -> Result<Vec<DeviceInfo>, BusError>;
|
||||
|
||||
/// Subscribes to bus hotplug notifications.
|
||||
///
|
||||
/// The returned subscription is intentionally opaque so concrete bus implementations can
|
||||
/// map it to a file descriptor, channel, or other event source.
|
||||
#[cfg(feature = "hotplug")]
|
||||
fn subscribe_hotplug(&self) -> Result<HotplugSubscription, BusError>;
|
||||
}
|
||||
|
||||
/// Errors produced by a [`Bus`] implementation.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum BusError {
|
||||
/// The bus has not finished initializing and cannot currently enumerate devices.
|
||||
NotReady,
|
||||
/// A transport or I/O failure occurred while talking to the bus.
|
||||
IoError,
|
||||
/// The requested capability is not supported by this bus implementation.
|
||||
Unsupported,
|
||||
/// An implementation-specific static error message.
|
||||
Other(&'static str),
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::string::String;
|
||||
|
||||
/// Unique identifier for a device on a specific bus.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct DeviceId {
|
||||
/// The bus namespace for this device, such as `"pci"` or `"usb"`.
|
||||
pub bus: String,
|
||||
/// The bus-local path for the device, such as `"0000:00:02.0"` or `"1-2"`.
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
/// Information about a discovered device, used for driver matching and probing.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct DeviceInfo {
|
||||
/// Stable device identifier within the manager.
|
||||
pub id: DeviceId,
|
||||
/// Optional vendor identifier reported by the bus or firmware.
|
||||
pub vendor: Option<u16>,
|
||||
/// Optional device identifier reported by the bus or firmware.
|
||||
pub device: Option<u16>,
|
||||
/// Optional base class code.
|
||||
pub class: Option<u8>,
|
||||
/// Optional subclass code.
|
||||
pub subclass: Option<u8>,
|
||||
/// Optional programming-interface code.
|
||||
pub prog_if: Option<u8>,
|
||||
/// Optional hardware revision code.
|
||||
pub revision: Option<u8>,
|
||||
/// Optional subsystem vendor identifier.
|
||||
pub subsystem_vendor: Option<u16>,
|
||||
/// Optional subsystem device identifier.
|
||||
pub subsystem_device: Option<u16>,
|
||||
/// Raw bus-specific device handle for detailed access.
|
||||
pub raw_path: String,
|
||||
/// Optional human-readable description provided by firmware or the bus layer.
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
/// Generic interface for an owned device handle.
|
||||
pub trait Device: Send + Sync {
|
||||
/// Returns the stable identifier for this device.
|
||||
fn id(&self) -> &DeviceId;
|
||||
|
||||
/// Returns the immutable descriptor used for matching and lifecycle actions.
|
||||
fn info(&self) -> &DeviceInfo;
|
||||
}
|
||||
|
||||
/// A device that has been successfully matched and bound to a driver.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BoundDevice {
|
||||
/// Static information captured at discovery time.
|
||||
pub info: DeviceInfo,
|
||||
/// The name of the driver that currently owns the device.
|
||||
pub driver_name: String,
|
||||
/// Key-value parameters associated with the active binding.
|
||||
pub parameters: BTreeMap<String, String>,
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
use alloc::string::String;
|
||||
|
||||
use crate::device::DeviceInfo;
|
||||
use crate::params::DriverParams;
|
||||
use crate::r#match::DriverMatch;
|
||||
|
||||
/// Result of a driver probe attempt.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ProbeResult {
|
||||
/// The driver successfully bound to the device.
|
||||
Bound,
|
||||
/// The device is not supported by this driver and other drivers may still try.
|
||||
NotSupported,
|
||||
/// A dependency is not yet available, so the manager should retry the probe later.
|
||||
Deferred {
|
||||
/// Human-readable reason for the deferral.
|
||||
reason: String,
|
||||
},
|
||||
/// The device cannot be driven successfully by this driver.
|
||||
Fatal {
|
||||
/// Human-readable explanation of the failure.
|
||||
reason: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Errors returned by driver lifecycle operations after a device has been matched.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum DriverError {
|
||||
/// The operation requires a resource that is not ready yet.
|
||||
NotReady,
|
||||
/// The driver encountered an I/O failure while managing the device.
|
||||
IoError,
|
||||
/// The requested lifecycle operation is not supported by this driver.
|
||||
Unsupported,
|
||||
/// An implementation-specific static error message.
|
||||
Other(&'static str),
|
||||
}
|
||||
|
||||
/// A device driver that can bind to and manage devices.
|
||||
pub trait Driver: Send + Sync {
|
||||
/// Returns the unique driver name, such as `"nvmed"` or `"e1000d"`.
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// Returns a human-readable description of the driver.
|
||||
fn description(&self) -> &str;
|
||||
|
||||
/// Returns the probe priority for this driver.
|
||||
///
|
||||
/// Higher numbers are probed first. Storage drivers typically use higher priorities than
|
||||
/// networking or peripheral drivers so boot-critical hardware claims happen early.
|
||||
fn priority(&self) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
/// Returns the driver's static match table.
|
||||
fn match_table(&self) -> &[DriverMatch];
|
||||
|
||||
/// Probes a candidate device and decides whether the driver should take ownership.
|
||||
fn probe(&self, info: &DeviceInfo) -> ProbeResult;
|
||||
|
||||
/// Detaches the driver from a previously bound device.
|
||||
fn remove(&self, info: &DeviceInfo) -> Result<(), DriverError>;
|
||||
|
||||
/// Suspends a bound device.
|
||||
fn suspend(&self, info: &DeviceInfo) -> Result<(), DriverError> {
|
||||
let _ = info;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resumes a previously suspended device.
|
||||
fn resume(&self, info: &DeviceInfo) -> Result<(), DriverError> {
|
||||
let _ = info;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the driver's parameter definitions and current values.
|
||||
fn params(&self) -> DriverParams {
|
||||
DriverParams::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use alloc::string::String;
|
||||
|
||||
use super::ProbeResult;
|
||||
|
||||
#[test]
|
||||
fn probe_result_variants_preserve_payloads() {
|
||||
let bound = ProbeResult::Bound;
|
||||
let not_supported = ProbeResult::NotSupported;
|
||||
let deferred = ProbeResult::Deferred {
|
||||
reason: String::from("waiting for scheme"),
|
||||
};
|
||||
let fatal = ProbeResult::Fatal {
|
||||
reason: String::from("device is wedged"),
|
||||
};
|
||||
|
||||
assert!(matches!(bound, ProbeResult::Bound));
|
||||
assert!(matches!(not_supported, ProbeResult::NotSupported));
|
||||
assert!(matches!(
|
||||
deferred,
|
||||
ProbeResult::Deferred { reason } if reason == "waiting for scheme"
|
||||
));
|
||||
assert!(matches!(
|
||||
fatal,
|
||||
ProbeResult::Fatal { reason } if reason == "device is wedged"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use alloc::string::String;
|
||||
|
||||
use hashbrown::HashMap;
|
||||
|
||||
use crate::device::DeviceId;
|
||||
#[cfg(feature = "hotplug")]
|
||||
use crate::device::DeviceInfo;
|
||||
|
||||
/// A normalized action associated with a userspace device event.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum UeventAction {
|
||||
/// A device or logical function was added.
|
||||
Add,
|
||||
/// A device or logical function was removed.
|
||||
Remove,
|
||||
/// A device changed state or metadata.
|
||||
Change,
|
||||
/// A driver or subsystem bound to the device.
|
||||
Bind,
|
||||
/// A driver or subsystem detached from the device.
|
||||
Unbind,
|
||||
}
|
||||
|
||||
/// Bus-agnostic metadata describing a userspace device event.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Uevent {
|
||||
/// Event action, normalized across bus implementations.
|
||||
pub action: UeventAction,
|
||||
/// Stable device identifier associated with the event.
|
||||
pub device: DeviceId,
|
||||
/// Bus-specific key-value metadata that accompanied the event.
|
||||
pub properties: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Opaque subscription handle for receiving hotplug notifications.
|
||||
#[cfg(feature = "hotplug")]
|
||||
pub type HotplugSubscription = usize;
|
||||
|
||||
/// High-level hotplug event delivered by a bus implementation.
|
||||
#[cfg(feature = "hotplug")]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum HotplugEvent {
|
||||
/// A device appeared on the bus and is ready for probing.
|
||||
DeviceAdded(DeviceInfo),
|
||||
/// A device disappeared from the bus.
|
||||
DeviceRemoved(DeviceId),
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
#![doc = "Core device-model traits and orchestration primitives for Red Bear OS drivers."]
|
||||
|
||||
#[cfg(not(any(feature = "std", feature = "alloc", test)))]
|
||||
compile_error!("redox-driver-core requires either the `std` or `alloc` feature");
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
/// Bus abstractions and related error types.
|
||||
pub mod bus;
|
||||
/// Device descriptors and bound-device state.
|
||||
pub mod device;
|
||||
/// Driver traits and probe outcomes.
|
||||
pub mod driver;
|
||||
/// Hotplug and uevent metadata types.
|
||||
pub mod hotplug;
|
||||
/// Device-manager orchestration.
|
||||
pub mod manager;
|
||||
/// Match-table primitives.
|
||||
pub mod r#match;
|
||||
/// Driver parameter definitions and runtime values.
|
||||
pub mod params;
|
||||
|
||||
pub use bus::{Bus, BusError};
|
||||
pub use device::{BoundDevice, Device, DeviceId, DeviceInfo};
|
||||
pub use driver::{Driver, DriverError, ProbeResult};
|
||||
pub use hotplug::{Uevent, UeventAction};
|
||||
#[cfg(feature = "hotplug")]
|
||||
pub use hotplug::{HotplugEvent, HotplugSubscription};
|
||||
pub use manager::{DeviceManager, ManagerConfig, ProbeEvent};
|
||||
pub use params::{DriverParams, ParamDef, ParamValue};
|
||||
pub use r#match::{DriverMatch, MatchPriority, MatchTable};
|
||||
@@ -0,0 +1,433 @@
|
||||
use alloc::boxed::Box;
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::bus::{Bus, BusError};
|
||||
use crate::device::{BoundDevice, DeviceId, DeviceInfo};
|
||||
use crate::driver::{Driver, ProbeResult};
|
||||
|
||||
/// Event emitted by the device manager during discovery or deferred-probe processing.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ProbeEvent {
|
||||
/// A bus finished enumeration and reported the number of discovered devices.
|
||||
BusEnumerated {
|
||||
/// Bus name returned by the [`Bus`] implementation.
|
||||
bus: String,
|
||||
/// Number of devices returned by the bus.
|
||||
device_count: usize,
|
||||
},
|
||||
/// A bus failed to enumerate devices.
|
||||
BusEnumerationFailed {
|
||||
/// Bus name returned by the [`Bus`] implementation.
|
||||
bus: String,
|
||||
/// Error returned by the bus.
|
||||
error: BusError,
|
||||
},
|
||||
/// The manager skipped probing because the device is already bound.
|
||||
AlreadyBound {
|
||||
/// Identifier of the device that was skipped.
|
||||
device: DeviceId,
|
||||
/// Driver that already owns the device.
|
||||
driver_name: String,
|
||||
},
|
||||
/// A driver completed a probe attempt for a device.
|
||||
ProbeCompleted {
|
||||
/// Identifier of the probed device.
|
||||
device: DeviceId,
|
||||
/// Driver that performed the probe.
|
||||
driver_name: String,
|
||||
/// Result returned by the driver's probe method.
|
||||
result: ProbeResult,
|
||||
},
|
||||
/// No registered driver had a matching table entry for the device.
|
||||
NoDriverFound {
|
||||
/// Identifier of the unmatched device.
|
||||
device: DeviceId,
|
||||
},
|
||||
/// A deferred probe referenced a driver that is no longer registered.
|
||||
MissingDriver {
|
||||
/// Identifier of the affected device.
|
||||
device: DeviceId,
|
||||
/// Driver name that could not be found.
|
||||
driver_name: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Configuration for the central [`DeviceManager`].
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ManagerConfig {
|
||||
/// Maximum number of probes the manager should allow concurrently.
|
||||
///
|
||||
/// The current implementation probes synchronously and stores this as policy metadata for
|
||||
/// future async or threaded executors.
|
||||
pub max_concurrent_probes: usize,
|
||||
/// Interval, in milliseconds, between deferred-probe retries.
|
||||
pub deferred_retry_ms: u64,
|
||||
/// Whether the manager should prefer asynchronous probing when an executor is available.
|
||||
pub async_probe: bool,
|
||||
}
|
||||
|
||||
/// Central device manager that orchestrates device discovery and driver binding.
|
||||
pub struct DeviceManager {
|
||||
buses: Vec<Box<dyn Bus>>,
|
||||
drivers: Vec<Box<dyn Driver>>,
|
||||
bound_devices: BTreeMap<DeviceId, BoundDevice>,
|
||||
deferred_queue: Vec<(DeviceInfo, String)>,
|
||||
config: ManagerConfig,
|
||||
}
|
||||
|
||||
impl DeviceManager {
|
||||
/// Creates a new device manager with the provided policy configuration.
|
||||
pub fn new(config: ManagerConfig) -> Self {
|
||||
Self {
|
||||
buses: Vec::new(),
|
||||
drivers: Vec::new(),
|
||||
bound_devices: BTreeMap::new(),
|
||||
deferred_queue: Vec::new(),
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers a bus that will be included in future enumeration cycles.
|
||||
pub fn register_bus(&mut self, bus: Box<dyn Bus>) {
|
||||
self.buses.push(bus);
|
||||
}
|
||||
|
||||
/// Registers a driver and reorders drivers so higher-priority probes run first.
|
||||
pub fn register_driver(&mut self, driver: Box<dyn Driver>) {
|
||||
self.drivers.push(driver);
|
||||
self.drivers
|
||||
.sort_by(|left, right| right.priority().cmp(&left.priority()));
|
||||
}
|
||||
|
||||
/// Runs a full enumeration cycle across all registered buses.
|
||||
pub fn enumerate(&mut self) -> Vec<ProbeEvent> {
|
||||
let _probe_budget = self.config.max_concurrent_probes.max(1);
|
||||
let _async_probe = self.config.async_probe;
|
||||
|
||||
let mut events = Vec::new();
|
||||
|
||||
for bus_index in 0..self.buses.len() {
|
||||
let (bus_name, enumeration) = {
|
||||
let bus = &self.buses[bus_index];
|
||||
(bus.name().to_string(), bus.enumerate_devices())
|
||||
};
|
||||
|
||||
match enumeration {
|
||||
Ok(devices) => {
|
||||
events.push(ProbeEvent::BusEnumerated {
|
||||
bus: bus_name,
|
||||
device_count: devices.len(),
|
||||
});
|
||||
|
||||
for info in devices {
|
||||
if let Some(bound) = self.bound_devices.get(&info.id) {
|
||||
events.push(ProbeEvent::AlreadyBound {
|
||||
device: info.id.clone(),
|
||||
driver_name: bound.driver_name.clone(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
self.probe_device(info, &mut events);
|
||||
}
|
||||
}
|
||||
Err(error) => events.push(ProbeEvent::BusEnumerationFailed {
|
||||
bus: bus_name,
|
||||
error,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
events
|
||||
}
|
||||
|
||||
/// Retries all deferred probe attempts in registration order.
|
||||
pub fn retry_deferred(&mut self) -> Vec<ProbeEvent> {
|
||||
let _retry_interval_ms = self.config.deferred_retry_ms;
|
||||
|
||||
let mut events = Vec::new();
|
||||
let deferred = core::mem::take(&mut self.deferred_queue);
|
||||
|
||||
for (info, driver_name) in deferred {
|
||||
if let Some(bound) = self.bound_devices.get(&info.id) {
|
||||
events.push(ProbeEvent::AlreadyBound {
|
||||
device: info.id.clone(),
|
||||
driver_name: bound.driver_name.clone(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(driver_index) = self
|
||||
.drivers
|
||||
.iter()
|
||||
.position(|driver| driver.name() == driver_name)
|
||||
else {
|
||||
events.push(ProbeEvent::MissingDriver {
|
||||
device: info.id.clone(),
|
||||
driver_name,
|
||||
});
|
||||
continue;
|
||||
};
|
||||
|
||||
let (probe_driver_name, result) = {
|
||||
let driver = &self.drivers[driver_index];
|
||||
(driver.name().to_string(), driver.probe(&info))
|
||||
};
|
||||
|
||||
match &result {
|
||||
ProbeResult::Bound => {
|
||||
self.bound_devices.insert(
|
||||
info.id.clone(),
|
||||
BoundDevice {
|
||||
info: info.clone(),
|
||||
driver_name: probe_driver_name.clone(),
|
||||
parameters: BTreeMap::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
ProbeResult::Deferred { .. } => {
|
||||
self.enqueue_deferred(info.clone(), probe_driver_name.clone());
|
||||
}
|
||||
ProbeResult::NotSupported | ProbeResult::Fatal { .. } => {}
|
||||
}
|
||||
|
||||
events.push(ProbeEvent::ProbeCompleted {
|
||||
device: info.id.clone(),
|
||||
driver_name: probe_driver_name,
|
||||
result,
|
||||
});
|
||||
}
|
||||
|
||||
events
|
||||
}
|
||||
|
||||
fn probe_device(&mut self, info: DeviceInfo, events: &mut Vec<ProbeEvent>) {
|
||||
let mut matched = false;
|
||||
|
||||
for driver_index in 0..self.drivers.len() {
|
||||
let is_match = {
|
||||
let driver = &self.drivers[driver_index];
|
||||
driver
|
||||
.match_table()
|
||||
.iter()
|
||||
.any(|driver_match| driver_match.matches(&info))
|
||||
};
|
||||
|
||||
if !is_match {
|
||||
continue;
|
||||
}
|
||||
|
||||
matched = true;
|
||||
let (driver_name, result) = {
|
||||
let driver = &self.drivers[driver_index];
|
||||
(driver.name().to_string(), driver.probe(&info))
|
||||
};
|
||||
|
||||
match &result {
|
||||
ProbeResult::Bound => {
|
||||
self.bound_devices.insert(
|
||||
info.id.clone(),
|
||||
BoundDevice {
|
||||
info: info.clone(),
|
||||
driver_name: driver_name.clone(),
|
||||
parameters: BTreeMap::new(),
|
||||
},
|
||||
);
|
||||
|
||||
events.push(ProbeEvent::ProbeCompleted {
|
||||
device: info.id.clone(),
|
||||
driver_name,
|
||||
result,
|
||||
});
|
||||
return;
|
||||
}
|
||||
ProbeResult::Deferred { .. } => {
|
||||
self.enqueue_deferred(info.clone(), driver_name.clone());
|
||||
events.push(ProbeEvent::ProbeCompleted {
|
||||
device: info.id.clone(),
|
||||
driver_name,
|
||||
result,
|
||||
});
|
||||
return;
|
||||
}
|
||||
ProbeResult::Fatal { .. } => {
|
||||
events.push(ProbeEvent::ProbeCompleted {
|
||||
device: info.id.clone(),
|
||||
driver_name,
|
||||
result,
|
||||
});
|
||||
return;
|
||||
}
|
||||
ProbeResult::NotSupported => {
|
||||
events.push(ProbeEvent::ProbeCompleted {
|
||||
device: info.id.clone(),
|
||||
driver_name,
|
||||
result,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !matched {
|
||||
events.push(ProbeEvent::NoDriverFound { device: info.id });
|
||||
}
|
||||
}
|
||||
|
||||
fn enqueue_deferred(&mut self, info: DeviceInfo, driver_name: String) {
|
||||
let already_queued = self.deferred_queue.iter().any(|(queued_info, queued_driver)| {
|
||||
queued_info.id == info.id && queued_driver == &driver_name
|
||||
});
|
||||
|
||||
if !already_queued {
|
||||
self.deferred_queue.push((info, driver_name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::String;
|
||||
use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use super::{DeviceManager, ManagerConfig};
|
||||
use crate::bus::{Bus, BusError};
|
||||
use crate::device::{DeviceId, DeviceInfo};
|
||||
use crate::driver::{Driver, DriverError, ProbeResult};
|
||||
use crate::r#match::DriverMatch;
|
||||
|
||||
struct MockBus {
|
||||
name: &'static str,
|
||||
devices: Vec<DeviceInfo>,
|
||||
}
|
||||
|
||||
impl Bus for MockBus {
|
||||
fn name(&self) -> &str {
|
||||
self.name
|
||||
}
|
||||
|
||||
fn enumerate_devices(&self) -> Result<Vec<DeviceInfo>, BusError> {
|
||||
Ok(self.devices.clone())
|
||||
}
|
||||
}
|
||||
|
||||
struct MockDriver {
|
||||
name: &'static str,
|
||||
description: &'static str,
|
||||
priority: i32,
|
||||
matches: Vec<DriverMatch>,
|
||||
}
|
||||
|
||||
impl Driver for MockDriver {
|
||||
fn name(&self) -> &str {
|
||||
self.name
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
self.description
|
||||
}
|
||||
|
||||
fn priority(&self) -> i32 {
|
||||
self.priority
|
||||
}
|
||||
|
||||
fn match_table(&self) -> &[DriverMatch] {
|
||||
self.matches.as_slice()
|
||||
}
|
||||
|
||||
fn probe(&self, _info: &DeviceInfo) -> ProbeResult {
|
||||
ProbeResult::NotSupported
|
||||
}
|
||||
|
||||
fn remove(&self, _info: &DeviceInfo) -> Result<(), DriverError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn config() -> ManagerConfig {
|
||||
ManagerConfig {
|
||||
max_concurrent_probes: 4,
|
||||
deferred_retry_ms: 250,
|
||||
async_probe: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_bus_and_driver_store_entries() {
|
||||
let mut manager = DeviceManager::new(config());
|
||||
|
||||
manager.register_bus(Box::new(MockBus {
|
||||
name: "pci",
|
||||
devices: Vec::new(),
|
||||
}));
|
||||
manager.register_driver(Box::new(MockDriver {
|
||||
name: "low",
|
||||
description: "low-priority driver",
|
||||
priority: 10,
|
||||
matches: vec![DriverMatch {
|
||||
vendor: Some(0x1234),
|
||||
device: None,
|
||||
class: None,
|
||||
subclass: None,
|
||||
prog_if: None,
|
||||
subsystem_vendor: None,
|
||||
subsystem_device: None,
|
||||
}],
|
||||
}));
|
||||
manager.register_driver(Box::new(MockDriver {
|
||||
name: "high",
|
||||
description: "high-priority driver",
|
||||
priority: 100,
|
||||
matches: vec![DriverMatch {
|
||||
vendor: Some(0x1234),
|
||||
device: Some(0x5678),
|
||||
class: None,
|
||||
subclass: None,
|
||||
prog_if: None,
|
||||
subsystem_vendor: None,
|
||||
subsystem_device: None,
|
||||
}],
|
||||
}));
|
||||
|
||||
assert_eq!(manager.buses.len(), 1);
|
||||
assert_eq!(manager.drivers.len(), 2);
|
||||
assert_eq!(manager.drivers[0].name(), "high");
|
||||
assert_eq!(manager.drivers[1].name(), "low");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enumerate_reports_registered_bus() {
|
||||
let mut manager = DeviceManager::new(config());
|
||||
|
||||
manager.register_bus(Box::new(MockBus {
|
||||
name: "pci",
|
||||
devices: vec![DeviceInfo {
|
||||
id: DeviceId {
|
||||
bus: String::from("pci"),
|
||||
path: String::from("0000:00:1f.2"),
|
||||
},
|
||||
vendor: Some(0x8086),
|
||||
device: Some(0x2922),
|
||||
class: Some(0x01),
|
||||
subclass: Some(0x06),
|
||||
prog_if: Some(0x01),
|
||||
revision: Some(0x02),
|
||||
subsystem_vendor: None,
|
||||
subsystem_device: None,
|
||||
raw_path: String::from("/scheme/pci/00.1f.2"),
|
||||
description: Some(String::from("AHCI controller")),
|
||||
}],
|
||||
}));
|
||||
|
||||
let events = manager.enumerate();
|
||||
|
||||
assert!(events.iter().any(|event| matches!(
|
||||
event,
|
||||
super::ProbeEvent::BusEnumerated { bus, device_count }
|
||||
if bus == "pci" && *device_count == 1
|
||||
)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::device::DeviceInfo;
|
||||
|
||||
/// Priority type used to order driver probes.
|
||||
pub type MatchPriority = i32;
|
||||
|
||||
/// A single entry in a driver's match table.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub struct DriverMatch {
|
||||
/// Optional vendor identifier match.
|
||||
pub vendor: Option<u16>,
|
||||
/// Optional device identifier match.
|
||||
pub device: Option<u16>,
|
||||
/// Optional class-code match.
|
||||
pub class: Option<u8>,
|
||||
/// Optional subclass-code match.
|
||||
pub subclass: Option<u8>,
|
||||
/// Optional programming-interface match.
|
||||
pub prog_if: Option<u8>,
|
||||
/// Optional subsystem vendor match.
|
||||
pub subsystem_vendor: Option<u16>,
|
||||
/// Optional subsystem device match.
|
||||
pub subsystem_device: Option<u16>,
|
||||
}
|
||||
|
||||
impl DriverMatch {
|
||||
/// Checks whether this match entry matches the provided device information.
|
||||
pub fn matches(&self, info: &DeviceInfo) -> bool {
|
||||
self.vendor.map_or(true, |v| info.vendor == Some(v))
|
||||
&& self.device.map_or(true, |d| info.device == Some(d))
|
||||
&& self.class.map_or(true, |c| info.class == Some(c))
|
||||
&& self.subclass.map_or(true, |s| info.subclass == Some(s))
|
||||
&& self.prog_if.map_or(true, |p| info.prog_if == Some(p))
|
||||
&& self
|
||||
.subsystem_vendor
|
||||
.map_or(true, |v| info.subsystem_vendor == Some(v))
|
||||
&& self
|
||||
.subsystem_device
|
||||
.map_or(true, |d| info.subsystem_device == Some(d))
|
||||
}
|
||||
}
|
||||
|
||||
/// Collection wrapper for a driver's match entries.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub struct MatchTable {
|
||||
entries: Vec<DriverMatch>,
|
||||
}
|
||||
|
||||
impl MatchTable {
|
||||
/// Creates a new match table from the provided entries.
|
||||
pub fn new(entries: Vec<DriverMatch>) -> Self {
|
||||
Self { entries }
|
||||
}
|
||||
|
||||
/// Returns the underlying immutable slice of match entries.
|
||||
pub fn entries(&self) -> &[DriverMatch] {
|
||||
self.entries.as_slice()
|
||||
}
|
||||
|
||||
/// Returns `true` if any entry in the table matches the provided device.
|
||||
pub fn matches(&self, info: &DeviceInfo) -> bool {
|
||||
self.entries.iter().any(|entry| entry.matches(info))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<DriverMatch>> for MatchTable {
|
||||
fn from(entries: Vec<DriverMatch>) -> Self {
|
||||
Self::new(entries)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use alloc::string::String;
|
||||
|
||||
use super::DriverMatch;
|
||||
use crate::device::{DeviceId, DeviceInfo};
|
||||
|
||||
fn sample_device() -> DeviceInfo {
|
||||
DeviceInfo {
|
||||
id: DeviceId {
|
||||
bus: String::from("pci"),
|
||||
path: String::from("0000:00:02.0"),
|
||||
},
|
||||
vendor: Some(0x8086),
|
||||
device: Some(0x1234),
|
||||
class: Some(0x03),
|
||||
subclass: Some(0x00),
|
||||
prog_if: Some(0x00),
|
||||
revision: Some(0x01),
|
||||
subsystem_vendor: Some(0x8086),
|
||||
subsystem_device: Some(0xabcd),
|
||||
raw_path: String::from("/scheme/pci/00.02.0"),
|
||||
description: Some(String::from("Display controller")),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn driver_match_accepts_exact_match() {
|
||||
let info = sample_device();
|
||||
let driver_match = DriverMatch {
|
||||
vendor: Some(0x8086),
|
||||
device: Some(0x1234),
|
||||
class: Some(0x03),
|
||||
subclass: Some(0x00),
|
||||
prog_if: Some(0x00),
|
||||
subsystem_vendor: Some(0x8086),
|
||||
subsystem_device: Some(0xabcd),
|
||||
};
|
||||
|
||||
assert!(driver_match.matches(&info));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn driver_match_supports_wildcards() {
|
||||
let info = sample_device();
|
||||
let driver_match = DriverMatch {
|
||||
vendor: Some(0x8086),
|
||||
device: None,
|
||||
class: Some(0x03),
|
||||
subclass: None,
|
||||
prog_if: None,
|
||||
subsystem_vendor: None,
|
||||
subsystem_device: None,
|
||||
};
|
||||
|
||||
assert!(driver_match.matches(&info));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn driver_match_rejects_mismatch() {
|
||||
let info = sample_device();
|
||||
let driver_match = DriverMatch {
|
||||
vendor: Some(0x10ec),
|
||||
device: None,
|
||||
class: None,
|
||||
subclass: None,
|
||||
prog_if: None,
|
||||
subsystem_vendor: None,
|
||||
subsystem_device: None,
|
||||
};
|
||||
|
||||
assert!(!driver_match.matches(&info));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use alloc::format;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ParamValue {
|
||||
Bool(bool),
|
||||
Int(i64),
|
||||
Uint(u64),
|
||||
String(String),
|
||||
Enum(String, Vec<String>),
|
||||
}
|
||||
|
||||
impl ParamValue {
|
||||
pub fn type_name(&self) -> &str {
|
||||
match self {
|
||||
ParamValue::Bool(_) => "bool",
|
||||
ParamValue::Int(_) => "int",
|
||||
ParamValue::Uint(_) => "uint",
|
||||
ParamValue::String(_) => "string",
|
||||
ParamValue::Enum(_, _) => "enum",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_display_string(&self) -> String {
|
||||
match self {
|
||||
ParamValue::Bool(v) => format!("{}", v),
|
||||
ParamValue::Int(v) => format!("{}", v),
|
||||
ParamValue::Uint(v) => format!("{}", v),
|
||||
ParamValue::String(v) => v.clone(),
|
||||
ParamValue::Enum(v, _) => v.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ParamDef {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub default: ParamValue,
|
||||
pub writable: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct DriverParams {
|
||||
pub params: BTreeMap<String, ParamDef>,
|
||||
pub values: BTreeMap<String, ParamValue>,
|
||||
}
|
||||
|
||||
impl DriverParams {
|
||||
pub fn new() -> Self {
|
||||
DriverParams {
|
||||
params: BTreeMap::new(),
|
||||
values: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn define(&mut self, name: &str, description: &str, default: ParamValue, writable: bool) {
|
||||
self.params.insert(
|
||||
String::from(name),
|
||||
ParamDef {
|
||||
name: String::from(name),
|
||||
description: String::from(description),
|
||||
default: default.clone(),
|
||||
writable,
|
||||
},
|
||||
);
|
||||
self.values.insert(String::from(name), default);
|
||||
}
|
||||
|
||||
pub fn get(&self, name: &str) -> Option<&ParamValue> {
|
||||
self.values.get(name)
|
||||
}
|
||||
|
||||
pub fn set(&mut self, name: &str, value: ParamValue) -> Result<(), &'static str> {
|
||||
match self.params.get(name) {
|
||||
Some(def) if !def.writable => Err("parameter is read-only"),
|
||||
Some(def) => {
|
||||
if core::mem::discriminant(&def.default) == core::mem::discriminant(&value) {
|
||||
self.values.insert(String::from(name), value);
|
||||
Ok(())
|
||||
} else {
|
||||
Err("parameter type mismatch")
|
||||
}
|
||||
}
|
||||
None => Err("unknown parameter"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list(&self) -> Vec<&ParamDef> {
|
||||
self.params.values().collect()
|
||||
}
|
||||
|
||||
pub fn parse_bool(s: &str) -> Option<bool> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"true" | "1" | "yes" | "on" => Some(true),
|
||||
"false" | "0" | "no" | "off" => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_int(s: &str) -> Option<i64> {
|
||||
s.parse().ok()
|
||||
}
|
||||
|
||||
pub fn parse_uint(s: &str) -> Option<u64> {
|
||||
s.parse().ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DriverParams {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn define_and_get_parameter() {
|
||||
let mut p = DriverParams::new();
|
||||
p.define("debug", "Enable debug logging", ParamValue::Bool(false), true);
|
||||
assert_eq!(p.get("debug"), Some(&ParamValue::Bool(false)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_writable_parameter() {
|
||||
let mut p = DriverParams::new();
|
||||
p.define("debug", "Enable debug logging", ParamValue::Bool(false), true);
|
||||
assert!(p.set("debug", ParamValue::Bool(true)).is_ok());
|
||||
assert_eq!(p.get("debug"), Some(&ParamValue::Bool(true)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_readonly_parameter_fails() {
|
||||
let mut p = DriverParams::new();
|
||||
p.define("vendor_id", "Vendor ID", ParamValue::Uint(0), false);
|
||||
assert!(p.set("vendor_id", ParamValue::Uint(1)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_unknown_parameter_fails() {
|
||||
let mut p = DriverParams::new();
|
||||
assert!(p.set("nonexistent", ParamValue::Bool(true)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn param_value_display_strings() {
|
||||
assert_eq!(ParamValue::Bool(true).to_display_string(), "true");
|
||||
assert_eq!(ParamValue::Int(-42).to_display_string(), "-42");
|
||||
assert_eq!(ParamValue::Uint(42).to_display_string(), "42");
|
||||
assert_eq!(ParamValue::String(String::from("hello")).to_display_string(), "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_bool_variants() {
|
||||
assert_eq!(DriverParams::parse_bool("true"), Some(true));
|
||||
assert_eq!(DriverParams::parse_bool("1"), Some(true));
|
||||
assert_eq!(DriverParams::parse_bool("yes"), Some(true));
|
||||
assert_eq!(DriverParams::parse_bool("false"), Some(false));
|
||||
assert_eq!(DriverParams::parse_bool("0"), Some(false));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user