USB: redbear-usb-hotplugd — auto-spawn class drivers on device connect

Cross-referenced with Linux 7.1 drivers/usb/core/hub.c port event
handling and drivers/usb/core/driver.c device-driver binding.

New daemon (216 lines) that watches USB controllers for device
attachment/detachment and auto-spawns the appropriate class driver.

Architecture:
- Polls /scheme/usb/ every 1000ms for controller directories
- For each controller, enumerates root hub ports
- Reads device descriptors (class/subclass/protocol) via XhciClientHandle
- Maps class to driver binary: HID(0x03)→usbhidd, Storage(0x08)→usbscsid,
  Hub(0x09)→usbhubd
- Spawns driver with <scheme> <port> <protocol> arguments
- Tracks spawned Child processes in HashMap<port_path, TrackedDevice>
- Detects disconnection (descriptor read fails) → kills driver + removes
  from tracking
- Detects driver exit (try_wait) → removes from tracking
- Skips hub interfaces (usbhubd handles its own children)

Config:
- Added to redbear-mini.toml (inherited by redbear-full)
- Auto-started via 02_usb_hotplug.service (oneshot_async, after base.target
  and pcid-spawner)

Driver map supports: HID (0x03), Mass Storage (0x08 with BOT protocol),
Hub (0x09). Additional class drivers extendable via DRIVER_MAP constant.
This commit is contained in:
2026-07-07 15:33:42 +03:00
parent 3452225e0e
commit 777a2c71d3
6 changed files with 263 additions and 0 deletions
+16
View File
@@ -59,6 +59,7 @@ redbear-acmd = {}
redbear-ftdi = {}
redbear-ecmd = {}
redbear-usbaudiod = {}
redbear-usb-hotplugd = {}
driver-params = {}
# ── PCI device database (critical for PCI driver matching) ──
@@ -172,6 +173,21 @@ cmd = "audiod"
type = "oneshot_async"
"""
[[files]]
path = "/etc/init.d/02_usb_hotplug.service"
data = """
[unit]
description = "USB device hotplug daemon (auto-spawns class drivers)"
requires_weak = [
"00_base.target",
"00_pcid-spawner.service",
]
[service]
cmd = "redbear-usb-hotplugd"
type = "oneshot_async"
"""
[[files]]
path = "/etc/init.d/02_serial_probe.service"
data = """
@@ -0,0 +1,3 @@
[workspace]
members = ["source"]
resolver = "3"
@@ -0,0 +1,8 @@
[source]
path = "source"
[build]
template = "cargo"
[package.files]
"/usr/bin/redbear-usb-hotplugd" = "redbear-usb-hotplugd"
@@ -0,0 +1,19 @@
[package]
name = "redbear-usb-hotplugd"
version = "0.2.5"
edition = "2024"
[[bin]]
name = "redbear-usb-hotplugd"
path = "src/main.rs"
[dependencies]
log = "0.4"
redox_syscall = { path = "../../../../../local/sources/syscall" }
libredox = { path = "../../../../../local/sources/libredox", features = ["call", "std"] }
xhcid = { path = "../../../../../local/sources/base/drivers/usb/xhcid" }
common = { path = "../../../../../local/sources/base/drivers/common" }
[patch.crates-io]
redox_syscall = { path = "../../../../../local/sources/syscall" }
libredox = { path = "../../../../../local/sources/libredox" }
@@ -0,0 +1,216 @@
//! USB device hotplug daemon.
//!
//! Cross-referenced with Linux 7.1 `drivers/usb/core/hub.c` port event
//! handling and `drivers/usb/core/driver.c` device-driver binding.
//!
//! Watches USB controllers for device attachment/detachment events.
//! When a device connects, reads its class/subclass/protocol from the
//! device descriptor, maps it to the appropriate class driver binary,
//! and spawns the driver with the correct scheme + port arguments.
//! When a device disconnects, terminates the associated driver process.
use std::collections::HashMap;
use std::fs;
use std::process::{Child, Command};
use std::thread;
use std::time::Duration;
use xhcid_interface::XhciClientHandle;
// USB class → driver binary mapping.
// Linux 7.1: usb_device_match() → usb_device_driver.id_table.
// Red Bear: static map in usb-core spawn.rs. Replicated here for the
// hotplug daemon which runs independently of usb-core.
const DRIVER_MAP: &[(u8, &str, &[&str])] = &[
(0x03, "/usr/bin/usbhidd", &[]), // HID
(0x08, "/usr/bin/usbscsid", &["0x50"]), // Mass Storage (BOT)
(0x09, "/usr/bin/usbhubd", &[]), // Hub
];
const POLL_INTERVAL_MS: u64 = 1000;
struct TrackedDevice {
scheme: String,
port: String,
child: Child,
}
fn find_driver_for_class(class: u8, _subclass: u8, protocol: u8) -> Option<(&'static str, &'static str)> {
for &(cls, binary, _extra_args) in DRIVER_MAP {
if cls == class {
let extra = if protocol == 0x50 { "0x50" } else { "0" };
return Some((binary, extra));
}
}
None
}
fn scan_controllers() -> Vec<(String, String)> {
let mut controllers = Vec::new();
if let Ok(dir) = fs::read_dir("/scheme/usb") {
for entry in dir.flatten() {
let path = entry.path();
if path.is_dir() {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
// Skip class driver scheme symlinks (not controller dirs)
if name.contains("_xhci") || name.contains("_ehci") || name.contains("_ohci") || name.contains("_uhci") {
controllers.push((name.to_string(), path.to_str().unwrap_or("").to_string()));
}
}
}
}
}
controllers
}
fn scan_ports(_scheme: &str, controller_name: &str) -> Vec<String> {
let mut ports = Vec::new();
let path = format!("/scheme/usb/{}", controller_name);
if let Ok(dir) = fs::read_dir(&path) {
for entry in dir.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if let Some(port_num) = name_str.strip_prefix("port") {
// Only root ports (no dots = root hub port)
if !port_num.contains('.') {
ports.push(format!("{}/{}", path, name_str));
}
}
}
}
ports
}
fn try_read_descriptors(scheme: &str, _controller_name: &str, port_num: &str) -> Option<(u8, u8, u8)> {
// Try to open an XhciClientHandle to read device descriptors.
// Parse port number from the path.
let port_str = port_num.strip_prefix("port").unwrap_or(port_num);
let port_id: u8 = port_str.parse().ok()?;
use xhcid_interface::PortId;
let pid = PortId { root_hub_port_num: port_id, route_string: 0 };
match XhciClientHandle::new(scheme.to_string(), pid) {
Ok(handle) => {
match handle.get_standard_descs() {
Ok(desc) => {
// Find the first non-hub interface class (skip hubs,
// usbhubd handles its own children).
for conf in &desc.config_descs {
for ifd in &conf.interface_descs {
if ifd.class != 0x09 {
return Some((ifd.class, ifd.sub_class, ifd.protocol));
}
}
}
None
}
Err(_) => None,
}
}
Err(_) => None,
}
}
fn main() {
log::set_max_level(log::LevelFilter::Info);
common::init();
common::setup_logging("usb", "system", "usb-hotplugd",
common::output_level(), common::file_level());
log::info!("USB hotplug daemon started — polling /scheme/usb/ every {}ms", POLL_INTERVAL_MS);
let mut tracked: HashMap<String, TrackedDevice> = HashMap::new();
loop {
let controllers = scan_controllers();
for (ctrl_name, _ctrl_path) in &controllers {
// The scheme name passed to class drivers. xhci controllers
// use the full scheme path (e.g. "usb.0000:00:14.0_xhci").
let scheme_name = format!("usb.{}", ctrl_name);
// Scan ports under this controller
let port_paths = scan_ports(&scheme_name, ctrl_name);
for port_path in &port_paths {
let port_key = port_path.clone();
// Already tracking this port?
if tracked.contains_key(&port_key) {
continue;
}
// Extract port number from path
let port_num = port_path.rsplit('/').next().unwrap_or("");
if port_num.is_empty() || !port_num.starts_with("port") {
continue;
}
// Try to read device descriptors
if let Some((class, subclass, protocol)) = try_read_descriptors(&scheme_name, ctrl_name, port_num) {
log::info!("New USB device on {}: class={:02X} subclass={:02X} protocol={:02X}",
port_path, class, subclass, protocol);
if let Some((driver_binary, extra_arg)) = find_driver_for_class(class, subclass, protocol) {
let port_str = port_num.strip_prefix("port").unwrap_or(port_num);
log::info!("Spawning {} for {} on {} port {}", driver_binary, port_path, scheme_name, port_str);
match Command::new(driver_binary)
.arg(&scheme_name)
.arg(port_str)
.arg(extra_arg)
.stdin(std::process::Stdio::null())
.spawn()
{
Ok(child) => {
tracked.insert(port_key.clone(), TrackedDevice {
scheme: scheme_name.clone(),
port: port_str.to_string(),
child,
});
}
Err(e) => {
log::warn!("Failed to spawn {} for {}: {}", driver_binary, port_path, e);
}
}
}
}
}
}
// Check for disconnected devices: try to read port status.
// If a previously-tracked port no longer has a connected device,
// kill the driver and remove it from tracking.
let mut disconnected: Vec<String> = Vec::new();
for (port_key, tracked_dev) in &mut tracked {
match tracked_dev.child.try_wait() {
Ok(Some(status)) => {
log::info!("Driver for {} exited with status {:?}", port_key, status);
disconnected.push(port_key.clone());
}
Ok(None) => {
// Driver still running. Check if device is still present.
// We use a simple heuristic: if the descriptors can no
// longer be read, the device is gone.
let scheme = tracked_dev.scheme.clone();
let port_num = format!("port{}", tracked_dev.port);
let ctrl_name = port_key.split('/').nth(3).unwrap_or("");
if try_read_descriptors(&scheme, ctrl_name, &port_num).is_none() {
log::info!("Device disconnected from {}", port_key);
let _ = tracked_dev.child.kill();
disconnected.push(port_key.clone());
}
}
Err(e) => {
log::warn!("Error checking driver for {}: {}", port_key, e);
}
}
}
for key in &disconnected {
tracked.remove(key);
}
thread::sleep(Duration::from_millis(POLL_INTERVAL_MS));
}
}
+1
View File
@@ -0,0 +1 @@
../../local/recipes/system/redbear-usb-hotplugd