diff --git a/config/redbear-mini.toml b/config/redbear-mini.toml index e57a3ef7f7..36a60d09fd 100644 --- a/config/redbear-mini.toml +++ b/config/redbear-mini.toml @@ -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 = """ diff --git a/local/recipes/system/redbear-usb-hotplugd/Cargo.toml b/local/recipes/system/redbear-usb-hotplugd/Cargo.toml new file mode 100644 index 0000000000..a808aa40c4 --- /dev/null +++ b/local/recipes/system/redbear-usb-hotplugd/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["source"] +resolver = "3" diff --git a/local/recipes/system/redbear-usb-hotplugd/recipe.toml b/local/recipes/system/redbear-usb-hotplugd/recipe.toml new file mode 100644 index 0000000000..3ff11222f4 --- /dev/null +++ b/local/recipes/system/redbear-usb-hotplugd/recipe.toml @@ -0,0 +1,8 @@ +[source] +path = "source" + +[build] +template = "cargo" + +[package.files] +"/usr/bin/redbear-usb-hotplugd" = "redbear-usb-hotplugd" diff --git a/local/recipes/system/redbear-usb-hotplugd/source/Cargo.toml b/local/recipes/system/redbear-usb-hotplugd/source/Cargo.toml new file mode 100644 index 0000000000..2a68c2bf3d --- /dev/null +++ b/local/recipes/system/redbear-usb-hotplugd/source/Cargo.toml @@ -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" } diff --git a/local/recipes/system/redbear-usb-hotplugd/source/src/main.rs b/local/recipes/system/redbear-usb-hotplugd/source/src/main.rs new file mode 100644 index 0000000000..e0fc8296f9 --- /dev/null +++ b/local/recipes/system/redbear-usb-hotplugd/source/src/main.rs @@ -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 { + 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 = 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 = 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)); + } +} diff --git a/recipes/system/redbear-usb-hotplugd b/recipes/system/redbear-usb-hotplugd new file mode 120000 index 0000000000..cbc34a2a09 --- /dev/null +++ b/recipes/system/redbear-usb-hotplugd @@ -0,0 +1 @@ +../../local/recipes/system/redbear-usb-hotplugd \ No newline at end of file