diff --git a/local/recipes/core/pcid-spawner/recipe.toml b/local/recipes/core/pcid-spawner/recipe.toml new file mode 100644 index 00000000..61e62e08 --- /dev/null +++ b/local/recipes/core/pcid-spawner/recipe.toml @@ -0,0 +1,8 @@ +[source] +path = "../../source/drivers/pcid-spawner" + +[build] +template = "cargo" + +[package] +dependencies = ["base"] diff --git a/local/recipes/drivers/ehcid/Cargo.toml b/local/recipes/drivers/ehcid/Cargo.toml new file mode 100644 index 00000000..3f79a180 --- /dev/null +++ b/local/recipes/drivers/ehcid/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "ehcid" +version = "0.1.0" +edition = "2024" +description = "EHCI USB 2.0 host controller driver for Red Bear OS" + +[[bin]] +name = "ehcid" +path = "src/main.rs" + +[dependencies] +usb-core = { path = "../../usb-core/source" } +redox_syscall = "0.7" +log = "0.4" diff --git a/local/recipes/drivers/ehcid/ehcid b/local/recipes/drivers/ehcid/ehcid new file mode 120000 index 00000000..a356db53 --- /dev/null +++ b/local/recipes/drivers/ehcid/ehcid @@ -0,0 +1 @@ +../../local/recipes/drivers/ehcid \ No newline at end of file diff --git a/local/recipes/drivers/ehcid/recipe.toml b/local/recipes/drivers/ehcid/recipe.toml new file mode 100644 index 00000000..4ab9b4a1 --- /dev/null +++ b/local/recipes/drivers/ehcid/recipe.toml @@ -0,0 +1,8 @@ +[source] +path = "source" + +[build] +template = "cargo" + +[package.files] +"/usr/lib/drivers/ehcid" = "ehcid" diff --git a/local/recipes/drivers/ehcid/source/Cargo.toml b/local/recipes/drivers/ehcid/source/Cargo.toml new file mode 100644 index 00000000..5d7c7959 --- /dev/null +++ b/local/recipes/drivers/ehcid/source/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "ehcid" +version = "0.1.0" +edition = "2024" +description = "EHCI USB 2.0 host controller driver for Red Bear OS" + +[[bin]] +name = "ehcid" +path = "src/main.rs" + +[dependencies] +usb-core = { path = "../../usb-core/source" } +libredox = { version = "0.1", features = ["call", "std"] } +log = { version = "0.4", features = ["std"] } +redox-driver-sys = { path = "../../redox-driver-sys/source" } +redox-scheme = "0.11" +syscall = { package = "redox_syscall", version = "0.7", features = ["std"] } + +[target.'cfg(target_os = "redox")'.dependencies] +redox-driver-sys = { path = "../../redox-driver-sys/source", features = ["redox"] } diff --git a/local/recipes/drivers/ehcid/source/src/lib.rs b/local/recipes/drivers/ehcid/source/src/lib.rs new file mode 100644 index 00000000..b93cf3ff --- /dev/null +++ b/local/recipes/drivers/ehcid/source/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/local/recipes/drivers/ehcid/source/src/main.rs b/local/recipes/drivers/ehcid/source/src/main.rs new file mode 100644 index 00000000..77aab97a --- /dev/null +++ b/local/recipes/drivers/ehcid/source/src/main.rs @@ -0,0 +1,1744 @@ +mod registers; + +use std::collections::BTreeMap; +use std::env; +use std::fmt::Write as _; +use std::fs; +use std::io::{self, Write}; +use std::mem::size_of; +use std::process; +use std::sync::atomic::{Ordering, fence}; +use std::sync::{Arc, Mutex, MutexGuard}; +use std::thread; +use std::time::Duration; + +use log::{LevelFilter, Metadata, Record, error, info, warn}; +use redox_driver_sys::dma::DmaBuffer; +use redox_driver_sys::memory::{CacheType, MmioProt, MmioRegion}; +use redox_driver_sys::pcid_client::PcidClient; +use redox_scheme::scheme::{SchemeState, SchemeSync, register_sync_scheme}; +use redox_scheme::{CallerCtx, OpenResult, SignalBehavior, Socket}; +use syscall::Stat; +use syscall::error::{ + EACCES, EBADF, EINVAL, ENOENT, EROFS, Error as SysError, Result as SysResult, +}; +use syscall::flag::{EventFlags, MODE_DIR, MODE_FILE}; +use syscall::schemev2::NewFdFlags; +use usb_core::{ + PortStatus, SetupPacket, TransferDirection, UsbError, UsbHostController, + parse_config_descriptor, parse_device_descriptor, +}; + +use registers::*; + +const SCHEME_NAME: &str = "usb"; +const SCHEME_ROOT_ID: usize = 1; +const MMIO_MAP_SIZE: usize = 0x1000; +const FRAME_LIST_LEN: usize = 1024; +const CONTROL_TD_COUNT: usize = 3; +const DEFAULT_CONTROL_MPS: u16 = 64; +const PORT_POLL_INTERVAL: Duration = Duration::from_millis(100); +const PORT_RESET_HOLD: Duration = Duration::from_millis(50); +const PORT_RESET_SETTLE: Duration = Duration::from_millis(10); +const WAIT_STEP: Duration = Duration::from_millis(1); +const CONTROL_TRANSFER_TIMEOUT_POLLS: usize = 1000; +const MAX_CONFIG_DESCRIPTOR_LEN: usize = 512; +const MAX_SCHEME_CONTROL_BYTES: usize = 4096; +const STATUS_CLEAR_BITS: u32 = STS_USB_INTERRUPT + | STS_USB_ERROR_INTERRUPT + | STS_PORT_CHANGE_DETECT + | STS_FRAME_LIST_ROLLOVER + | STS_HOST_SYSTEM_ERROR + | STS_INTERRUPT_ON_ASYNC_ADVANCE; + +static LOGGER: StderrLogger = StderrLogger; + +struct StderrLogger; + +impl log::Log for StderrLogger { + fn enabled(&self, metadata: &Metadata<'_>) -> bool { + metadata.level() <= LevelFilter::Info + } + + fn log(&self, record: &Record<'_>) { + if self.enabled(record.metadata()) { + let _ = writeln!( + io::stderr().lock(), + "[{}] ehcid: {}", + record.level(), + record.args() + ); + } + } + + fn flush(&self) {} +} + +#[derive(Clone, Debug, Default)] +struct PortDevice { + address: u8, + max_packet_size0: u16, + device_descriptor: Vec, + config_descriptor: Vec, + vendor_id: u16, + product_id: u16, + device_class: u8, + device_subclass: u8, + device_protocol: u8, +} + +#[derive(Clone, Debug, Default)] +struct PortRecord { + last_portsc: u32, + last_status: Option, + companion_owned: bool, + last_error: Option, + device: Option, +} + +#[derive(Clone, Debug)] +struct ControlRequest { + request_type: u8, + request: u8, + value: u16, + index: u16, + length: u16, + data: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum HandleKind { + PortDir { port: usize }, + Status { port: usize }, + Descriptor { port: usize }, + Control { port: usize }, +} + +#[derive(Clone, Debug)] +struct HandleState { + kind: HandleKind, + response: Vec, +} + +struct EhciScheme { + controller: Arc>, + handles: BTreeMap, + next_id: usize, +} + +struct EhciController { + controller_name: String, + mmio: MmioRegion, + op_base: usize, + n_ports: u8, + frame_list: DmaBuffer, + async_qh: DmaBuffer, + dma_segment: u32, + has_64bit: bool, + next_address: u8, + ports: Vec, +} + +impl EhciController { + fn new(device_path: &str, channel_fd: usize) -> Result { + info!("EHCI USB 2.0 at {} (fd={})", device_path, channel_fd); + + let mut pcid = PcidClient::connect_default() + .ok_or_else(|| "failed to connect to PCID client channel".to_string())?; + pcid.enable_device() + .map_err(|err| format!("failed to enable PCI device: {err}"))?; + + let config_path = format!("{device_path}/config"); + let config = match fs::read(&config_path) { + Ok(data) => data, + Err(err) => return Err(format!("cannot read PCI config at {config_path}: {err}")), + }; + + let mmio_base = parse_mmio_bar(&config)?; + info!("MMIO base: 0x{mmio_base:016X}"); + + let mmio = MmioRegion::map( + mmio_base, + MMIO_MAP_SIZE, + CacheType::DeviceMemory, + MmioProt::READ_WRITE, + ) + .map_err(|err| format!("failed to map EHCI MMIO region: {err}"))?; + + let caplength = mmio.read8(CAPLENGTH); + let op_base = registers::op_base(caplength); + let hcsparams = mmio.read32(HCSPARAMS); + let hccparams = mmio.read32(HCCPARAMS); + let caps = HcCapParams::from_hcsparams(hcsparams); + let n_ports = caps.n_ports; + let has_64bit = (hccparams & HCCPARAMS_64BIT) != 0; + + if n_ports == 0 { + return Err("EHCI controller reports zero ports".to_string()); + } + + info!("ports: {}, caplength: {}", n_ports, caplength); + + let mut frame_list = DmaBuffer::allocate(FRAME_LIST_LEN * size_of::(), 4096) + .map_err(|err| format!("failed to allocate frame list: {err}"))?; + init_frame_list(&mut frame_list); + + let async_qh = DmaBuffer::allocate(size_of::(), 64) + .map_err(|err| format!("failed to allocate async queue head: {err}"))?; + + let dma_segment = ensure_dma_segment( + has_64bit, + &[ + frame_list.physical_address() as u64, + async_qh.physical_address() as u64, + ], + )?; + + let mut controller = Self { + controller_name: device_path.to_string(), + mmio, + op_base, + n_ports, + frame_list, + async_qh, + dma_segment, + has_64bit, + next_address: 1, + ports: vec![PortRecord::default(); usize::from(n_ports)], + }; + + controller.reset_and_start()?; + controller.poll_ports_once(); + Ok(controller) + } + + fn reset_and_start(&mut self) -> Result<(), String> { + self.stop_controller()?; + + self.write_op32(USBCMD, CMD_HCRESET); + self.wait_until( + "controller reset completion", + || self.read_op32(USBCMD) & CMD_HCRESET == 0, + 1000, + )?; + + self.clear_interrupt_status(); + self.write_op32(CTRLDSSEGMENT, self.dma_segment); + self.write_op32( + PERIODICLISTBASE, + low32(self.frame_list.physical_address() as u64), + ); + + self.initialize_async_qh(0, DEFAULT_CONTROL_MPS); + self.write_op32( + ASYNCLISTADDR, + low32(self.async_qh.physical_address() as u64), + ); + self.write_op32( + USBINTR, + INTR_USB_INTERRUPT_ENABLE + | INTR_USB_ERROR_INTERRUPT_ENABLE + | INTR_PORT_CHANGE_ENABLE + | INTR_HOST_SYSTEM_ERROR_ENABLE + | INTR_ASYNC_ADVANCE_ENABLE, + ); + + self.write_op32( + USBCMD, + CMD_RUN_STOP + | CMD_FRAME_LIST_SIZE_1024 + | CMD_PERIODIC_SCHEDULE_ENABLE + | CMD_ASYNC_SCHEDULE_ENABLE + | interrupt_threshold(8), + ); + + self.wait_until( + "host controller run state", + || self.read_op32(USBSTS) & STS_HC_HALTED == 0, + 1000, + )?; + self.wait_until( + "periodic schedule activation", + || self.read_op32(USBSTS) & STS_PERIODIC_SCHEDULE_STATUS != 0, + 1000, + )?; + self.wait_until( + "async schedule activation", + || self.read_op32(USBSTS) & STS_ASYNC_SCHEDULE_STATUS != 0, + 1000, + )?; + + self.write_op32(CONFIGFLAG, CF_FLAG); + + for port in 0..self.port_count() { + self.ensure_port_power(port); + let status = self.read_portsc(port); + self.clear_port_changes(port, status); + } + + info!( + "ehcid: controller initialized, {} ports, async list at 0x{:08X}", + self.n_ports, + low32(self.async_qh.physical_address() as u64) + ); + + Ok(()) + } + + fn stop_controller(&mut self) -> Result<(), String> { + let command = self.read_op32(USBCMD); + if command & CMD_RUN_STOP != 0 { + self.write_op32(USBCMD, command & !CMD_RUN_STOP); + self.wait_until( + "controller halt", + || self.read_op32(USBSTS) & STS_HC_HALTED != 0, + 1000, + )?; + } + + Ok(()) + } + + fn wait_until(&self, label: &str, mut predicate: F, iterations: usize) -> Result<(), String> + where + F: FnMut() -> bool, + { + for _ in 0..iterations { + if predicate() { + return Ok(()); + } + thread::sleep(WAIT_STEP); + } + + Err(format!("timed out waiting for {label}")) + } + + fn clear_interrupt_status(&mut self) { + let status = self.read_op32(USBSTS); + if status & STS_HOST_SYSTEM_ERROR != 0 { + warn!("EHCI host system error reported in USBSTS: 0x{status:08x}"); + } + let clear = status & STATUS_CLEAR_BITS; + if clear != 0 { + self.write_op32(USBSTS, clear); + } + } + + fn initialize_async_qh(&mut self, device_address: u8, max_packet_size: u16) { + let qh_phys = self.async_qh.physical_address() as u64; + let mut qh = QueueHead::new(); + qh.horiz_link = qh_link_pointer(qh_phys); + qh.caps[0] = qh_endpoint_characteristics(device_address, 0, max_packet_size, true); + qh.caps[1] = qh_endpoint_capabilities(); + + unsafe { + std::ptr::write_volatile(self.async_qh.as_mut_ptr() as *mut QueueHead, qh); + } + } + + fn prepare_async_qh(&mut self, device_address: u8, max_packet_size: u16, first_td_phys: u32) { + let qh_ptr = self.async_qh.as_mut_ptr() as *mut QueueHead; + unsafe { + let qh = &mut *qh_ptr; + qh.horiz_link = qh_link_pointer(self.async_qh.physical_address() as u64); + qh.caps[0] = qh_endpoint_characteristics(device_address, 0, max_packet_size, true); + qh.caps[1] = qh_endpoint_capabilities(); + qh.current_qtd = 0; + qh.overlay[0] = first_td_phys & !0x1F; + qh.overlay[1] = TD_TERMINATE; + qh.overlay[2] = 0; + qh.overlay[3] = 0; + qh.overlay[4] = 0; + qh.overlay[5] = 0; + qh.overlay[6] = 0; + qh.overlay[7] = 0; + } + } + + fn disarm_async_qh(&mut self) { + let qh_ptr = self.async_qh.as_mut_ptr() as *mut QueueHead; + unsafe { + let qh = &mut *qh_ptr; + qh.current_qtd = 0; + qh.overlay[0] = TD_TERMINATE; + qh.overlay[1] = TD_TERMINATE; + qh.overlay[2] = 0; + qh.overlay[3] = 0; + qh.overlay[4] = 0; + qh.overlay[5] = 0; + qh.overlay[6] = 0; + qh.overlay[7] = 0; + } + } + + fn ensure_controller_running(&mut self) { + let status = self.read_op32(USBSTS); + let command = self.read_op32(USBCMD); + let required = CMD_RUN_STOP | CMD_ASYNC_SCHEDULE_ENABLE | CMD_PERIODIC_SCHEDULE_ENABLE; + + if status & STS_HC_HALTED != 0 + || status & STS_ASYNC_SCHEDULE_STATUS == 0 + || status & STS_PERIODIC_SCHEDULE_STATUS == 0 + || command & required != required + { + self.write_op32( + USBCMD, + CMD_RUN_STOP + | CMD_FRAME_LIST_SIZE_1024 + | CMD_PERIODIC_SCHEDULE_ENABLE + | CMD_ASYNC_SCHEDULE_ENABLE + | interrupt_threshold(8), + ); + } + } + + fn read32(&self, offset: usize) -> u32 { + self.mmio.read32(offset) + } + + fn write32(&self, offset: usize, value: u32) { + self.mmio.write32(offset, value) + } + + fn read_op32(&self, offset: usize) -> u32 { + self.read32(self.op_base + offset) + } + + fn write_op32(&self, offset: usize, value: u32) { + self.write32(self.op_base + offset, value) + } + + fn read_portsc(&self, port: usize) -> u32 { + self.read_op32(portsc_offset(port)) + } + + fn write_portsc(&self, port: usize, value: u32) { + self.write_op32(portsc_offset(port), value) + } + + fn port_write_value( + &self, + current: u32, + set_bits: u32, + clear_bits: u32, + clear_changes: u32, + ) -> u32 { + let mut value = current & PORTSC_WRITE_MASK; + value &= !clear_bits; + value |= set_bits & PORTSC_WRITE_MASK; + value |= clear_changes & PORTSC_CHANGE_BITS; + value + } + + fn clear_port_changes(&self, port: usize, current: u32) { + let clear = current & PORTSC_CHANGE_BITS; + if clear != 0 { + self.write_portsc(port, self.port_write_value(current, 0, 0, clear)); + } + } + + fn ensure_port_power(&self, port: usize) { + let current = self.read_portsc(port); + if current & PORT_POWER == 0 { + self.write_portsc(port, self.port_write_value(current, PORT_POWER, 0, current)); + thread::sleep(WAIT_STEP); + } + } + + fn handoff_to_companion(&mut self, port: usize) { + let current = self.read_portsc(port); + self.write_portsc( + port, + self.port_write_value(current, PORT_OWNER | PORT_POWER, PORT_RESET, current), + ); + self.ports[port].companion_owned = true; + self.ports[port].device = None; + info!("ehcid: handed port {} to companion controller", port + 1); + } + + fn poll_ports_once(&mut self) { + self.clear_interrupt_status(); + + for port in 0..self.port_count() { + let portsc = self.read_portsc(port); + let status = decode_port_status(portsc); + let had_device = self.ports[port].device.is_some(); + let had_companion = self.ports[port].companion_owned; + + self.ports[port].last_portsc = portsc; + self.ports[port].last_status = Some(status.clone()); + + if portsc & PORTSC_CHANGE_BITS != 0 { + self.clear_port_changes(port, portsc); + } + + if !status.connected { + if had_device || had_companion { + info!("ehcid: device disconnected from port {}", port + 1); + } + self.ports[port].device = None; + self.ports[port].companion_owned = false; + self.ports[port].last_error = None; + continue; + } + + if portsc & PORT_OWNER != 0 { + self.ports[port].companion_owned = true; + continue; + } + + let should_probe = self.ports[port].device.is_none() + && ((portsc & PORT_CONNECT_CHANGE != 0) || (portsc & PORT_ENABLE != 0)); + + if should_probe { + match self.initialize_port(port) { + Ok(()) => {} + Err(err) => { + warn!("ehcid: port {} initialization failed: {}", port + 1, err); + self.ports[port].last_error = Some(err); + } + } + } + } + } + + fn initialize_port(&mut self, port: usize) -> Result<(), String> { + self.ports[port].device = None; + self.ports[port].companion_owned = false; + + if !self.port_reset(port) { + let portsc = self.read_portsc(port); + if portsc & PORT_OWNER != 0 { + self.ports[port].companion_owned = true; + self.ports[port].last_error = None; + return Ok(()); + } + + return Err("port reset did not produce an enabled high-speed port".to_string()); + } + + let device = self.enumerate_port_device(port)?; + info!( + "ehcid: port {} device {:04x}:{:04x} address {}", + port + 1, + device.vendor_id, + device.product_id, + device.address + ); + + self.ports[port].device = Some(device); + self.ports[port].last_error = None; + Ok(()) + } + + fn enumerate_port_device(&mut self, port: usize) -> Result { + let mut header = [0_u8; 8]; + let get_device_header = SetupPacket { + request_type: 0x80, + request: 0x06, + value: 0x0100, + index: 0, + length: 8, + }; + + self.submit_control_transfer( + port, + 0, + DEFAULT_CONTROL_MPS, + &get_device_header, + &mut header, + ) + .map_err(|err| format!("failed to fetch device descriptor header: {err:?}"))?; + + let max_packet_size0 = u16::from(header[7].max(8)); + let address = self.allocate_device_address()?; + + let set_address = SetupPacket { + request_type: 0x00, + request: 0x05, + value: u16::from(address), + index: 0, + length: 0, + }; + + self.submit_control_transfer(port, 0, max_packet_size0, &set_address, &mut []) + .map_err(|err| format!("failed to set device address {}: {err:?}", address))?; + thread::sleep(PORT_RESET_SETTLE); + + let mut device_descriptor = [0_u8; 18]; + let get_device_descriptor = SetupPacket { + request_type: 0x80, + request: 0x06, + value: 0x0100, + index: 0, + length: 18, + }; + + self.submit_control_transfer( + port, + address, + max_packet_size0, + &get_device_descriptor, + &mut device_descriptor, + ) + .map_err(|err| format!("failed to fetch full device descriptor: {err:?}"))?; + + let descriptor = parse_device_descriptor(&device_descriptor) + .ok_or_else(|| "device descriptor parse failed".to_string())?; + + let config_descriptor = self.read_config_descriptor(port, address, max_packet_size0); + + Ok(PortDevice { + address, + max_packet_size0, + device_descriptor: device_descriptor.to_vec(), + config_descriptor, + vendor_id: descriptor.vendor_id, + product_id: descriptor.product_id, + device_class: descriptor.device_class, + device_subclass: descriptor.device_subclass, + device_protocol: descriptor.device_protocol, + }) + } + + fn read_config_descriptor( + &mut self, + port: usize, + address: u8, + max_packet_size0: u16, + ) -> Vec { + let header_request = SetupPacket { + request_type: 0x80, + request: 0x06, + value: 0x0200, + index: 0, + length: 9, + }; + + let mut header = [0_u8; 9]; + if self + .submit_control_transfer( + port, + address, + max_packet_size0, + &header_request, + &mut header, + ) + .is_err() + { + return Vec::new(); + } + + let Some(config) = parse_config_descriptor(&header) else { + return Vec::new(); + }; + + let total_length = usize::from(config.total_length).clamp( + usize::from(header_request.length), + MAX_CONFIG_DESCRIPTOR_LEN, + ); + + let full_request = SetupPacket { + length: total_length as u16, + ..header_request + }; + let mut data = vec![0_u8; total_length]; + + match self.submit_control_transfer( + port, + address, + max_packet_size0, + &full_request, + &mut data, + ) { + Ok(actual) => { + data.truncate(actual); + data + } + Err(_) => Vec::new(), + } + } + + fn allocate_device_address(&mut self) -> Result { + for _ in 0..127 { + let candidate = self.next_address; + self.next_address = if self.next_address >= 127 { + 1 + } else { + self.next_address + 1 + }; + + if !self.address_in_use(candidate) { + return Ok(candidate); + } + } + + Err("no free USB device addresses remain".to_string()) + } + + fn address_in_use(&self, address: u8) -> bool { + self.ports.iter().any(|record| { + record + .device + .as_ref() + .map(|device| device.address == address) + .unwrap_or(false) + }) + } + + fn ensure_dma_segment_matches(&self, phys: u64, label: &str) -> Result { + let segment = dma_segment(phys); + if !self.has_64bit && segment != 0 { + warn!( + "ehcid: DMA buffer {} requires 64-bit addressing but the controller is 32-bit-only", + label + ); + return Err(UsbError::IoError); + } + + if segment != self.dma_segment { + warn!( + "ehcid: DMA buffer {} is in segment 0x{:08x}, expected 0x{:08x}", + label, segment, self.dma_segment + ); + return Err(UsbError::IoError); + } + + Ok(low32(phys)) + } + + fn submit_control_transfer( + &mut self, + port: usize, + device_address: u8, + max_packet_size: u16, + setup: &SetupPacket, + data: &mut [u8], + ) -> Result { + if port >= self.port_count() { + return Err(UsbError::NoDevice); + } + if usize::from(setup.length) != data.len() { + return Err(UsbError::IoError); + } + if data.len() > 0x7FFF { + return Err(UsbError::Unsupported); + } + if max_packet_size == 0 { + return Err(UsbError::IoError); + } + + self.ensure_controller_running(); + + let setup_bytes = setup_packet_bytes(setup); + let mut setup_dma = + DmaBuffer::allocate(setup_bytes.len(), 8).map_err(|_| UsbError::IoError)?; + dma_write_bytes(&mut setup_dma, &setup_bytes); + self.ensure_dma_segment_matches(setup_dma.physical_address() as u64, "setup")?; + + let mut data_dma = if data.is_empty() { + None + } else { + Some(DmaBuffer::allocate(data.len(), 4096).map_err(|_| UsbError::IoError)?) + }; + + if let Some(buffer) = data_dma.as_mut() { + self.ensure_dma_segment_matches(buffer.physical_address() as u64, "data")?; + if setup.request_type & 0x80 == 0 { + dma_write_bytes(buffer, data); + } + } + + let mut td_dma = + DmaBuffer::allocate(CONTROL_TD_COUNT * size_of::(), 32) + .map_err(|_| UsbError::IoError)?; + self.ensure_dma_segment_matches(td_dma.physical_address() as u64, "qtd")?; + + let td_pool = unsafe { + std::slice::from_raw_parts_mut( + td_dma.as_mut_ptr() as *mut TransferDescriptor, + CONTROL_TD_COUNT, + ) + }; + + let first_td_phys = build_control_transfer( + setup_dma.physical_address() as u64, + &setup_bytes, + data_dma + .as_ref() + .map(|buffer| buffer.physical_address() as u64) + .unwrap_or(0), + data.len(), + setup.request_type & 0x80 != 0, + td_pool, + td_dma.physical_address() as u64, + ) + .ok_or(UsbError::IoError)?; + + self.prepare_async_qh(device_address, max_packet_size, first_td_phys); + self.clear_interrupt_status(); + fence(Ordering::SeqCst); + + let td_count = if data.is_empty() { 2 } else { 3 }; + for _ in 0..CONTROL_TRANSFER_TIMEOUT_POLLS { + let mut active = false; + let mut error_token = None; + + for index in 0..td_count { + let token = read_td_token(&td_dma, index); + if token & TD_ACTIVE != 0 { + active = true; + } + if token & (TD_HALTED | TD_BUFERR | TD_BABBLE | TD_XACTERR | TD_MISSED) != 0 { + error_token = Some(token); + break; + } + } + + if let Some(token) = error_token { + self.disarm_async_qh(); + self.ports[port].last_error = Some(format!("transfer failure token=0x{token:08x}")); + return Err(map_td_error(token)); + } + + if !active { + let actual = if data.is_empty() { + 0 + } else { + let data_token = read_td_token(&td_dma, 1); + let remaining = + ((data_token & TD_TOTAL_BYTES_MASK) >> TD_TOTAL_BYTES_SHIFT) as usize; + data.len().saturating_sub(remaining) + }; + + if let Some(buffer) = data_dma.as_ref() { + if setup.request_type & 0x80 != 0 && actual != 0 { + dma_read_bytes(buffer, &mut data[..actual]); + } + } + + self.disarm_async_qh(); + self.clear_interrupt_status(); + return Ok(actual); + } + + thread::sleep(WAIT_STEP); + } + + self.disarm_async_qh(); + self.ports[port].last_error = Some("transfer timed out".to_string()); + Err(UsbError::Timeout) + } + + fn port_record(&self, port: usize) -> Option<&PortRecord> { + self.ports.get(port) + } + + fn execute_control_request( + &mut self, + port: usize, + request: &ControlRequest, + ) -> Result, String> { + let Some(device) = self + .ports + .get(port) + .and_then(|record| record.device.clone()) + else { + return Err(format!("port {} is not enumerated", port + 1)); + }; + + let mut data = if request.request_type & 0x80 != 0 { + vec![0_u8; usize::from(request.length)] + } else { + request.data.clone() + }; + + let setup = SetupPacket { + request_type: request.request_type, + request: request.request, + value: request.value, + index: request.index, + length: request.length, + }; + + let actual = self + .submit_control_transfer( + port, + device.address, + device.max_packet_size0, + &setup, + &mut data, + ) + .map_err(|err| format!("control transfer failed: {err:?}"))?; + + if request.request_type & 0x80 != 0 { + data.truncate(actual); + Ok(data) + } else { + Ok(format!("ok transferred={actual}\n").into_bytes()) + } + } + + fn port_count(&self) -> usize { + usize::from(self.n_ports) + } +} + +impl UsbHostController for EhciController { + fn port_count(&self) -> usize { + usize::from(self.n_ports) + } + + fn port_status(&self, port: usize) -> Option { + if port >= usize::from(self.n_ports) { + return None; + } + + Some(decode_port_status(self.read_portsc(port))) + } + + fn port_reset(&mut self, port: usize) -> bool { + if port >= self.port_count() { + return false; + } + + self.ensure_port_power(port); + let current = self.read_portsc(port); + if current & PORT_CONNECT == 0 { + return false; + } + + self.clear_port_changes(port, current); + let reset_value = self.port_write_value(current, PORT_POWER | PORT_RESET, 0, current); + self.write_portsc(port, reset_value); + thread::sleep(PORT_RESET_HOLD); + + let after_hold = self.read_portsc(port); + let clear_reset = self.port_write_value(after_hold, PORT_POWER, PORT_RESET, after_hold); + self.write_portsc(port, clear_reset); + thread::sleep(PORT_RESET_SETTLE); + + for _ in 0..100 { + let status = self.read_portsc(port); + if status & PORT_OWNER != 0 { + return false; + } + if status & PORT_CONNECT == 0 { + return false; + } + if status & PORT_ENABLE != 0 { + return true; + } + thread::sleep(WAIT_STEP); + } + + self.handoff_to_companion(port); + false + } + + fn control_transfer( + &mut self, + device_address: u8, + setup: &SetupPacket, + data: &mut [u8], + ) -> Result { + let Some(port) = address_port(self, device_address) else { + return Err(UsbError::NoDevice); + }; + let Some(max_packet_size0) = self + .ports + .get(port) + .and_then(|record| record.device.as_ref().map(|device| device.max_packet_size0)) + else { + return Err(UsbError::NoDevice); + }; + + self.submit_control_transfer(port, device_address, max_packet_size0, setup, data) + } + + fn bulk_transfer( + &mut self, + _device_address: u8, + _endpoint: u8, + _data: &mut [u8], + _direction: TransferDirection, + ) -> Result { + Err(UsbError::Unsupported) + } + + fn interrupt_transfer( + &mut self, + _device_address: u8, + _endpoint: u8, + _data: &mut [u8], + ) -> Result { + Err(UsbError::Unsupported) + } + + fn set_address(&mut self, device_address: u8) -> bool { + device_address > 0 && device_address <= 127 + } + + fn name(&self) -> &str { + &self.controller_name + } +} + +impl EhciScheme { + fn new(controller: Arc>) -> Self { + Self { + controller, + handles: BTreeMap::new(), + next_id: SCHEME_ROOT_ID + 1, + } + } + + fn alloc_handle(&mut self, kind: HandleKind) -> usize { + let id = self.next_id; + self.next_id += 1; + self.handles.insert( + id, + HandleState { + kind, + response: Vec::new(), + }, + ); + id + } + + fn handle(&self, id: usize) -> SysResult<&HandleState> { + self.handles.get(&id).ok_or(SysError::new(EBADF)) + } + + fn parse_port_component(&self, component: &str) -> SysResult { + let Some(raw_port) = component.strip_prefix("port") else { + return Err(SysError::new(ENOENT)); + }; + + let port_number = raw_port + .parse::() + .map_err(|_| SysError::new(ENOENT))?; + if port_number == 0 { + return Err(SysError::new(ENOENT)); + } + + let port_index = port_number - 1; + let controller = lock_controller(&self.controller); + if port_index >= controller.port_count() { + return Err(SysError::new(ENOENT)); + } + + Ok(port_index) + } + + fn resolve_root_path(&self, path: &str) -> SysResult { + let mut parts = path.split('/'); + let Some(port_component) = parts.next() else { + return Err(SysError::new(ENOENT)); + }; + let port = self.parse_port_component(port_component)?; + + match (parts.next(), parts.next()) { + (None, None) => Ok(HandleKind::PortDir { port }), + (Some("status"), None) => Ok(HandleKind::Status { port }), + (Some("descriptor"), None) => Ok(HandleKind::Descriptor { port }), + (Some("control"), None) => Ok(HandleKind::Control { port }), + _ => Err(SysError::new(ENOENT)), + } + } + + fn resolve_port_child(&self, port: usize, path: &str) -> SysResult { + match path { + "status" => Ok(HandleKind::Status { port }), + "descriptor" => Ok(HandleKind::Descriptor { port }), + "control" => Ok(HandleKind::Control { port }), + _ => Err(SysError::new(ENOENT)), + } + } + + fn root_listing(&self) -> Vec { + let controller = lock_controller(&self.controller); + let mut listing = String::new(); + for port in 0..controller.port_count() { + let _ = writeln!(&mut listing, "port{}", port + 1); + } + listing.into_bytes() + } + + fn status_bytes(&self, port: usize) -> SysResult> { + let controller = lock_controller(&self.controller); + let Some(record) = controller.port_record(port) else { + return Err(SysError::new(ENOENT)); + }; + + let status = record + .last_status + .clone() + .unwrap_or_else(|| decode_port_status(record.last_portsc)); + + let mut out = String::new(); + let _ = writeln!(&mut out, "port={}", port + 1); + let _ = writeln!(&mut out, "portsc=0x{:08x}", record.last_portsc); + let _ = writeln!(&mut out, "connected={}", bool_word(status.connected)); + let _ = writeln!(&mut out, "enabled={}", bool_word(status.enabled)); + let _ = writeln!(&mut out, "suspended={}", bool_word(status.suspended)); + let _ = writeln!(&mut out, "over_current={}", bool_word(status.over_current)); + let _ = writeln!(&mut out, "reset={}", bool_word(status.reset)); + let _ = writeln!(&mut out, "power={}", bool_word(status.power)); + let _ = writeln!(&mut out, "low_speed={}", bool_word(status.low_speed)); + let _ = writeln!(&mut out, "high_speed={}", bool_word(status.high_speed)); + let _ = writeln!(&mut out, "test_mode={}", bool_word(status.test_mode)); + let _ = writeln!(&mut out, "indicator={}", bool_word(status.indicator)); + let _ = writeln!( + &mut out, + "companion_owned={}", + bool_word(record.companion_owned) + ); + + if let Some(device) = record.device.as_ref() { + let _ = writeln!(&mut out, "address={}", device.address); + let _ = writeln!(&mut out, "vendor_id=0x{:04x}", device.vendor_id); + let _ = writeln!(&mut out, "product_id=0x{:04x}", device.product_id); + } + + if let Some(last_error) = record.last_error.as_ref() { + let _ = writeln!(&mut out, "last_error={}", last_error); + } + + Ok(out.into_bytes()) + } + + fn descriptor_bytes(&self, port: usize) -> SysResult> { + let controller = lock_controller(&self.controller); + let Some(record) = controller.port_record(port) else { + return Err(SysError::new(ENOENT)); + }; + + let Some(device) = record.device.as_ref() else { + return Ok(b"state=unenumerated\n".to_vec()); + }; + + let mut out = String::new(); + let _ = writeln!(&mut out, "address={}", device.address); + let _ = writeln!(&mut out, "vendor_id=0x{:04x}", device.vendor_id); + let _ = writeln!(&mut out, "product_id=0x{:04x}", device.product_id); + let _ = writeln!(&mut out, "device_class=0x{:02x}", device.device_class); + let _ = writeln!(&mut out, "device_subclass=0x{:02x}", device.device_subclass); + let _ = writeln!(&mut out, "device_protocol=0x{:02x}", device.device_protocol); + let _ = writeln!(&mut out, "max_packet_size0={}", device.max_packet_size0); + let _ = writeln!( + &mut out, + "device_descriptor={}", + hex_encode(&device.device_descriptor) + ); + + if !device.config_descriptor.is_empty() { + let _ = writeln!( + &mut out, + "config_descriptor={}", + hex_encode(&device.config_descriptor) + ); + } + + Ok(out.into_bytes()) + } + + fn handle_control_write(&mut self, port: usize, buf: &[u8]) -> SysResult> { + let request = parse_control_request(buf).map_err(|_| SysError::new(EINVAL))?; + let mut controller = lock_controller(&self.controller); + controller + .execute_control_request(port, &request) + .map_err(|_| SysError::new(EINVAL)) + } + + fn handle_bytes(&self, id: usize) -> SysResult> { + if id == SCHEME_ROOT_ID { + return Ok(self.root_listing()); + } + + let handle = self.handle(id)?; + match &handle.kind { + HandleKind::PortDir { .. } => Ok(b"status\ndescriptor\ncontrol\n".to_vec()), + HandleKind::Status { port } => self.status_bytes(*port), + HandleKind::Descriptor { port } => self.descriptor_bytes(*port), + HandleKind::Control { .. } => Ok(handle.response.clone()), + } + } + + fn handle_path(&self, id: usize) -> SysResult { + if id == SCHEME_ROOT_ID { + return Ok(format!("{SCHEME_NAME}:/")); + } + + let handle = self.handle(id)?; + let path = match handle.kind { + HandleKind::PortDir { port } => format!("{SCHEME_NAME}:/port{}", port + 1), + HandleKind::Status { port } => format!("{SCHEME_NAME}:/port{}/status", port + 1), + HandleKind::Descriptor { port } => { + format!("{SCHEME_NAME}:/port{}/descriptor", port + 1) + } + HandleKind::Control { port } => format!("{SCHEME_NAME}:/port{}/control", port + 1), + }; + Ok(path) + } +} + +impl SchemeSync for EhciScheme { + fn scheme_root(&mut self) -> SysResult { + Ok(SCHEME_ROOT_ID) + } + + fn openat( + &mut self, + dirfd: usize, + path: &str, + _flags: usize, + _fcntl_flags: u32, + _ctx: &CallerCtx, + ) -> SysResult { + let cleaned = path.trim_matches('/'); + if cleaned.is_empty() { + if dirfd == SCHEME_ROOT_ID { + return Ok(OpenResult::ThisScheme { + number: SCHEME_ROOT_ID, + flags: NewFdFlags::POSITIONED, + }); + } + + let kind = self.handle(dirfd)?.kind.clone(); + return Ok(OpenResult::ThisScheme { + number: self.alloc_handle(kind), + flags: NewFdFlags::POSITIONED, + }); + } + + let kind = if dirfd == SCHEME_ROOT_ID { + self.resolve_root_path(cleaned)? + } else { + match self.handle(dirfd)?.kind.clone() { + HandleKind::PortDir { port } => self.resolve_port_child(port, cleaned)?, + _ => return Err(SysError::new(EACCES)), + } + }; + + Ok(OpenResult::ThisScheme { + number: self.alloc_handle(kind), + flags: NewFdFlags::POSITIONED, + }) + } + + fn read( + &mut self, + id: usize, + buf: &mut [u8], + offset: u64, + _flags: u32, + _ctx: &CallerCtx, + ) -> SysResult { + let data = self.handle_bytes(id)?; + copy_with_offset(buf, offset, &data) + } + + fn write( + &mut self, + id: usize, + buf: &[u8], + _offset: u64, + _flags: u32, + _ctx: &CallerCtx, + ) -> SysResult { + let kind = if id == SCHEME_ROOT_ID { + return Err(SysError::new(EROFS)); + } else { + self.handle(id)?.kind.clone() + }; + + match kind { + HandleKind::Control { port } => { + let response = self.handle_control_write(port, buf)?; + if let Some(handle) = self.handles.get_mut(&id) { + handle.response = response; + } + Ok(buf.len()) + } + _ => Err(SysError::new(EROFS)), + } + } + + fn fstat(&mut self, id: usize, stat: &mut Stat, _ctx: &CallerCtx) -> SysResult<()> { + let data_len = match id { + SCHEME_ROOT_ID => self.root_listing().len(), + _ => self.handle_bytes(id)?.len(), + }; + + stat.st_mode = if id == SCHEME_ROOT_ID { + MODE_DIR | 0o755 + } else { + match self.handle(id)?.kind { + HandleKind::PortDir { .. } => MODE_DIR | 0o755, + HandleKind::Status { .. } | HandleKind::Descriptor { .. } => MODE_FILE | 0o444, + HandleKind::Control { .. } => MODE_FILE | 0o644, + } + }; + + stat.st_size = match u64::try_from(data_len) { + Ok(size) => size, + Err(_) => u64::MAX, + }; + Ok(()) + } + + fn fpath(&mut self, id: usize, buf: &mut [u8], _ctx: &CallerCtx) -> SysResult { + let path = self.handle_path(id)?; + copy_with_offset(buf, 0, path.as_bytes()) + } + + fn fsync(&mut self, id: usize, _ctx: &CallerCtx) -> SysResult<()> { + if id != SCHEME_ROOT_ID { + let _ = self.handle(id)?; + } + Ok(()) + } + + fn fcntl(&mut self, id: usize, _cmd: usize, _arg: usize, _ctx: &CallerCtx) -> SysResult { + if id != SCHEME_ROOT_ID { + let _ = self.handle(id)?; + } + Ok(0) + } + + fn fevent(&mut self, id: usize, _flags: EventFlags, _ctx: &CallerCtx) -> SysResult { + if id != SCHEME_ROOT_ID { + let _ = self.handle(id)?; + } + Ok(EventFlags::empty()) + } + + fn on_close(&mut self, id: usize) { + if id != SCHEME_ROOT_ID { + self.handles.remove(&id); + } + } +} + +fn init_logging() { + let _ = log::set_logger(&LOGGER); + log::set_max_level(LevelFilter::Info); +} + +fn bool_word(value: bool) -> &'static str { + if value { "yes" } else { "no" } +} + +fn lock_controller(shared: &Arc>) -> MutexGuard<'_, EhciController> { + match shared.lock() { + Ok(guard) => guard, + Err(poisoned) => { + warn!("ehcid: controller mutex was poisoned; continuing with recovered state"); + poisoned.into_inner() + } + } +} + +fn parse_mmio_bar(config: &[u8]) -> Result { + let Some(bar0) = read_config_dword(config, 0x10) else { + return Err("PCI config space is too short to contain BAR0".to_string()); + }; + + if bar0 == 0 { + return Err("BAR0 is zero".to_string()); + } + if bar0 & 0x1 != 0 { + return Err("BAR0 is I/O space; EHCI requires MMIO".to_string()); + } + + let mut base = u64::from(bar0 & 0xFFFF_FFF0); + if bar0 & 0x6 == 0x4 { + let Some(bar1) = read_config_dword(config, 0x14) else { + return Err( + "PCI config space is too short to contain BAR1 for a 64-bit BAR".to_string(), + ); + }; + base |= u64::from(bar1) << 32; + } + + Ok(base) +} + +fn read_config_dword(config: &[u8], offset: usize) -> Option { + if config.len() < offset.saturating_add(4) { + return None; + } + + Some(u32::from_le_bytes([ + config[offset], + config[offset + 1], + config[offset + 2], + config[offset + 3], + ])) +} + +fn ensure_dma_segment(has_64bit: bool, phys_addrs: &[u64]) -> Result { + let mut segment = None; + + for &phys_addr in phys_addrs { + let current = dma_segment(phys_addr); + if !has_64bit && current != 0 { + return Err(format!( + "controller is 32-bit-only but DMA buffer landed above 4GB: 0x{phys_addr:016x}" + )); + } + + match segment { + Some(existing) if existing != current => { + return Err(format!( + "EHCI data structures must share one DMA segment, found 0x{existing:08x} and 0x{current:08x}" + )); + } + None => segment = Some(current), + _ => {} + } + } + + Ok(segment.unwrap_or(0)) +} + +fn low32(value: u64) -> u32 { + (value & u64::from(u32::MAX)) as u32 +} + +fn dma_segment(value: u64) -> u32 { + (value >> 32) as u32 +} + +fn interrupt_threshold(microframes: u8) -> u32 { + (u32::from(microframes) & 0xFF) << 16 +} + +fn init_frame_list(frame_list: &mut DmaBuffer) { + let ptr = frame_list.as_mut_ptr() as *mut u32; + for index in 0..FRAME_LIST_LEN { + unsafe { + std::ptr::write_volatile(ptr.add(index), QH_TERMINATE); + } + } +} + +fn decode_port_status(portsc: u32) -> PortStatus { + let line_status = portsc & PORT_LINE_STATUS; + PortStatus { + connected: portsc & PORT_CONNECT != 0, + enabled: portsc & PORT_ENABLE != 0, + suspended: portsc & PORT_SUSPEND != 0, + over_current: portsc & PORT_OVER_CURRENT_ACTIVE != 0, + reset: portsc & PORT_RESET != 0, + power: portsc & PORT_POWER != 0, + low_speed: line_status == PORT_LINE_STATUS_K, + high_speed: portsc & PORT_ENABLE != 0, + test_mode: portsc & PORT_TEST_CONTROL != 0, + indicator: portsc & PORT_INDICATOR != 0, + } +} + +fn read_td_token(buffer: &DmaBuffer, index: usize) -> u32 { + let td_ptr = buffer.as_ptr() as *const TransferDescriptor; + unsafe { std::ptr::read_volatile(std::ptr::addr_of!((*td_ptr.add(index)).token)) } +} + +fn dma_write_bytes(buffer: &mut DmaBuffer, data: &[u8]) { + if data.is_empty() { + return; + } + + unsafe { + std::ptr::copy_nonoverlapping(data.as_ptr(), buffer.as_mut_ptr(), data.len()); + } +} + +fn dma_read_bytes(buffer: &DmaBuffer, output: &mut [u8]) { + if output.is_empty() { + return; + } + + unsafe { + std::ptr::copy_nonoverlapping(buffer.as_ptr(), output.as_mut_ptr(), output.len()); + } +} + +fn map_td_error(token: u32) -> UsbError { + if token & TD_BABBLE != 0 { + UsbError::Babble + } else if token & TD_HALTED != 0 { + UsbError::Stall + } else if token & (TD_BUFERR | TD_XACTERR | TD_MISSED) != 0 { + UsbError::DataError + } else { + UsbError::IoError + } +} + +fn setup_packet_bytes(setup: &SetupPacket) -> [u8; 8] { + let value = setup.value.to_le_bytes(); + let index = setup.index.to_le_bytes(); + let length = setup.length.to_le_bytes(); + + [ + setup.request_type, + setup.request, + value[0], + value[1], + index[0], + index[1], + length[0], + length[1], + ] +} + +fn parse_control_request(buf: &[u8]) -> Result { + let text = + std::str::from_utf8(buf).map_err(|err| format!("control request is not UTF-8: {err}"))?; + let mut request_type = None; + let mut request = None; + let mut value = None; + let mut index = None; + let mut length = None; + let mut data = None; + + for token in text.split_whitespace() { + let Some((key, raw_value)) = token.split_once('=') else { + return Err(format!("invalid token '{token}', expected key=value")); + }; + + match key { + "request_type" | "bmRequestType" => { + request_type = Some(parse_numeric::(raw_value)?) + } + "request" | "bRequest" => request = Some(parse_numeric::(raw_value)?), + "value" | "wValue" => value = Some(parse_numeric::(raw_value)?), + "index" | "wIndex" => index = Some(parse_numeric::(raw_value)?), + "length" | "wLength" => length = Some(parse_numeric::(raw_value)?), + "data" => data = Some(parse_hex_bytes(raw_value)?), + _ => return Err(format!("unsupported control field '{key}'")), + } + } + + let request_type = request_type.ok_or_else(|| "missing request_type".to_string())?; + let request = request.ok_or_else(|| "missing request".to_string())?; + let value = value.ok_or_else(|| "missing value".to_string())?; + let index = index.ok_or_else(|| "missing index".to_string())?; + let length = length.ok_or_else(|| "missing length".to_string())?; + + if usize::from(length) > MAX_SCHEME_CONTROL_BYTES || usize::from(length) > 0x7FFF { + return Err(format!( + "requested control payload {} is outside the supported single-qTD range", + length + )); + } + + let payload = if request_type & 0x80 != 0 { + if data + .as_ref() + .map(|bytes| !bytes.is_empty()) + .unwrap_or(false) + { + return Err( + "IN control requests must not provide an outgoing data payload".to_string(), + ); + } + Vec::new() + } else { + let bytes = data.unwrap_or_default(); + if bytes.len() != usize::from(length) { + return Err(format!( + "OUT control payload length mismatch: expected {}, got {}", + length, + bytes.len() + )); + } + bytes + }; + + Ok(ControlRequest { + request_type, + request, + value, + index, + length, + data: payload, + }) +} + +fn parse_numeric(value: &str) -> Result +where + T: TryFrom, +{ + let parsed = if let Some(hex) = value + .strip_prefix("0x") + .or_else(|| value.strip_prefix("0X")) + { + u64::from_str_radix(hex, 16).map_err(|err| format!("invalid hex value '{value}': {err}"))? + } else { + value + .parse::() + .map_err(|err| format!("invalid integer value '{value}': {err}"))? + }; + + T::try_from(parsed).map_err(|_| format!("value '{value}' is out of range")) +} + +fn parse_hex_bytes(value: &str) -> Result, String> { + let cleaned: String = value + .chars() + .filter(|ch| !ch.is_ascii_whitespace() && *ch != ':' && *ch != '-') + .collect(); + + if cleaned.is_empty() { + return Ok(Vec::new()); + } + if cleaned.len() % 2 != 0 { + return Err(format!("hex payload '{value}' has an odd number of digits")); + } + + let mut bytes = Vec::with_capacity(cleaned.len() / 2); + for chunk in cleaned.as_bytes().chunks(2) { + let chunk = std::str::from_utf8(chunk) + .map_err(|err| format!("invalid hex payload '{value}': {err}"))?; + let byte = u8::from_str_radix(chunk, 16) + .map_err(|err| format!("invalid hex byte '{chunk}' in payload '{value}': {err}"))?; + bytes.push(byte); + } + + Ok(bytes) +} + +fn hex_encode(bytes: &[u8]) -> String { + let mut out = String::new(); + for (index, byte) in bytes.iter().enumerate() { + if index != 0 { + out.push(' '); + } + let _ = write!(&mut out, "{byte:02x}"); + } + out +} + +fn copy_with_offset(buf: &mut [u8], offset: u64, data: &[u8]) -> SysResult { + let offset = usize::try_from(offset).map_err(|_| SysError::new(EINVAL))?; + if offset >= data.len() { + return Ok(0); + } + + let count = (data.len() - offset).min(buf.len()); + buf[..count].copy_from_slice(&data[offset..offset + count]); + Ok(count) +} + +fn address_port(controller: &EhciController, address: u8) -> Option { + controller.ports.iter().position(|record| { + record + .device + .as_ref() + .map(|device| device.address == address) + .unwrap_or(false) + }) +} + +fn poll_ports_loop(controller: Arc>) { + loop { + { + let mut controller = lock_controller(&controller); + controller.poll_ports_once(); + } + thread::sleep(PORT_POLL_INTERVAL); + } +} + +fn run_scheme(controller: Arc>) -> Result<(), String> { + let socket = + Socket::create().map_err(|err| format!("failed to create scheme socket: {err}"))?; + let mut scheme = EhciScheme::new(controller); + let mut state = SchemeState::new(); + + register_sync_scheme(&socket, SCHEME_NAME, &mut scheme) + .map_err(|err| format!("failed to register scheme:{SCHEME_NAME}: {err}"))?; + + libredox::call::setrens(0, 0) + .map_err(|err| format!("failed to enter null namespace: {err}"))?; + + info!("ehcid: registered /scheme/{SCHEME_NAME}"); + info!("ehcid: ready — polling for device connections"); + + loop { + let request = match socket.next_request(SignalBehavior::Restart) { + Ok(Some(request)) => request, + Ok(None) => { + info!("ehcid: scheme socket closed, shutting down"); + break; + } + Err(err) => return Err(format!("failed to read scheme request: {err}")), + }; + + if let redox_scheme::RequestKind::Call(request) = request.kind() { + let response = request.handle_sync(&mut scheme, &mut state); + socket + .write_response(response, SignalBehavior::Restart) + .map_err(|err| format!("failed to write scheme response: {err}"))?; + } + } + + Ok(()) +} + +fn main() { + init_logging(); + + let channel_fd = match env::var("PCID_CLIENT_CHANNEL") { + Ok(raw) => match raw.parse::() { + Ok(fd) => fd, + Err(_) => { + error!("invalid PCID_CLIENT_CHANNEL"); + process::exit(1); + } + }, + Err(_) => { + error!("PCID_CLIENT_CHANNEL not set"); + process::exit(1); + } + }; + + let device_path = match env::var("PCID_DEVICE_PATH") { + Ok(path) if !path.is_empty() => path, + Ok(_) => { + error!("PCID_DEVICE_PATH is empty"); + process::exit(1); + } + Err(_) => { + error!("PCID_DEVICE_PATH not set"); + process::exit(1); + } + }; + + let controller = match EhciController::new(&device_path, channel_fd) { + Ok(controller) => Arc::new(Mutex::new(controller)), + Err(err) => { + error!("{err}"); + process::exit(1); + } + }; + + if let Err(err) = thread::Builder::new() + .name("ehci-port-poll".to_string()) + .spawn({ + let controller = Arc::clone(&controller); + move || poll_ports_loop(controller) + }) + { + error!("failed to spawn EHCI port polling thread: {err}"); + process::exit(1); + } + + if let Err(err) = run_scheme(controller) { + error!("{err}"); + process::exit(1); + } +} diff --git a/local/recipes/drivers/ehcid/source/src/main.rs.bak b/local/recipes/drivers/ehcid/source/src/main.rs.bak new file mode 100644 index 00000000..fcd01d23 --- /dev/null +++ b/local/recipes/drivers/ehcid/source/src/main.rs.bak @@ -0,0 +1,33 @@ +mod registers; + +use std::env; +use std::process; +use log::{info, error, LevelFilter}; + +struct StderrLogger; +impl log::Log for StderrLogger { + fn enabled(&self, md: &log::Metadata) -> bool { md.level() <= LevelFilter::Info } + fn log(&self, r: &log::Record) { eprintln!("[{}] ehcid: {}", r.level(), r.args()); } + fn flush(&self) {} +} + +fn main() { + log::set_logger(&StderrLogger).ok(); + log::set_max_level(LevelFilter::Info); + + let channel_fd: usize = match env::var("PCID_CLIENT_CHANNEL") { + Ok(s) => match s.parse() { Ok(fd) => fd, Err(_) => { error!("invalid PCID_CLIENT_CHANNEL"); process::exit(1); } }, + Err(_) => { error!("PCID_CLIENT_CHANNEL not set"); process::exit(1); } + }; + + let device_path = env::var("PCID_DEVICE_PATH").unwrap_or_default(); + info!("EHCI USB 2.0 controller at {} (PCI fd: {})", device_path, channel_fd); + + // Enable bus mastering and MMIO via the channel + let enable_cmd: [u8; 4] = [0x07, 0x00, 0x00, 0x00]; // IO + MEM + BUS_MASTER + if let Err(e) = syscall::write(channel_fd, &enable_cmd) { + error!("failed to enable device: {}", e); + } + + info!("ehcid: initialized — ready for enumeration"); +} diff --git a/local/recipes/drivers/ehcid/source/src/registers.rs b/local/recipes/drivers/ehcid/source/src/registers.rs new file mode 100644 index 00000000..510544fe --- /dev/null +++ b/local/recipes/drivers/ehcid/source/src/registers.rs @@ -0,0 +1,330 @@ +#![allow(dead_code)] + +use core::mem::size_of; + +// EHCI (USB 2.0) MMIO Register Layout +// References: Intel EHCI 1.0 Specification (March 2002), sections 2.1-2.2 +// USB 2.0 Specification, sections 5.2-5.3 + +pub const CAPLENGTH: usize = 0x00; +pub const HCSPARAMS: usize = 0x04; +pub const HCCPARAMS: usize = 0x08; +pub const HCSP_PORT_ROUTE: usize = 0x0C; + +pub const HCCPARAMS_64BIT: u32 = 1 << 0; + +pub fn op_base(caplength: u8) -> usize { + caplength as usize +} + +pub const USBCMD: usize = 0x00; +pub const USBSTS: usize = 0x04; +pub const USBINTR: usize = 0x08; +pub const FRINDEX: usize = 0x0C; +pub const CTRLDSSEGMENT: usize = 0x10; +pub const PERIODICLISTBASE: usize = 0x14; +pub const ASYNCLISTADDR: usize = 0x18; +pub const CONFIGFLAG: usize = 0x40; +pub const PORTSC_BASE: usize = 0x44; + +pub const CMD_RUN_STOP: u32 = 1 << 0; +pub const CMD_HCRESET: u32 = 1 << 1; +pub const CMD_FRAME_LIST_SIZE_MASK: u32 = 0x3 << 2; +pub const CMD_FRAME_LIST_SIZE_1024: u32 = 0x0 << 2; +pub const CMD_FRAME_LIST_SIZE_512: u32 = 0x1 << 2; +pub const CMD_FRAME_LIST_SIZE_256: u32 = 0x2 << 2; +pub const CMD_PERIODIC_SCHEDULE_ENABLE: u32 = 1 << 4; +pub const CMD_ASYNC_SCHEDULE_ENABLE: u32 = 1 << 5; +pub const CMD_INTERRUPT_ON_ASYNC_ADVANCE: u32 = 1 << 6; +pub const CMD_LIGHT_HOST_CONTROLLER_RESET: u32 = 1 << 7; +pub const CMD_ASYNC_SCHEDULE_PARK_MODE_COUNT: u32 = 0x3 << 8; +pub const CMD_ASYNC_SCHEDULE_PARK_MODE_ENABLE: u32 = 1 << 11; +pub const CMD_INTERRUPT_THRESHOLD_CONTROL: u32 = 0xFF << 16; + +pub const STS_USB_INTERRUPT: u32 = 1 << 0; +pub const STS_USB_ERROR_INTERRUPT: u32 = 1 << 1; +pub const STS_PORT_CHANGE_DETECT: u32 = 1 << 2; +pub const STS_FRAME_LIST_ROLLOVER: u32 = 1 << 3; +pub const STS_HOST_SYSTEM_ERROR: u32 = 1 << 4; +pub const STS_INTERRUPT_ON_ASYNC_ADVANCE: u32 = 1 << 5; +pub const STS_HC_HALTED: u32 = 1 << 12; +pub const STS_RECLAMATION: u32 = 1 << 13; +pub const STS_PERIODIC_SCHEDULE_STATUS: u32 = 1 << 14; +pub const STS_ASYNC_SCHEDULE_STATUS: u32 = 1 << 15; + +pub const INTR_USB_INTERRUPT_ENABLE: u32 = 1 << 0; +pub const INTR_USB_ERROR_INTERRUPT_ENABLE: u32 = 1 << 1; +pub const INTR_PORT_CHANGE_ENABLE: u32 = 1 << 2; +pub const INTR_FRAME_LIST_ROLLOVER_ENABLE: u32 = 1 << 3; +pub const INTR_HOST_SYSTEM_ERROR_ENABLE: u32 = 1 << 4; +pub const INTR_ASYNC_ADVANCE_ENABLE: u32 = 1 << 5; + +pub const CF_FLAG: u32 = 1 << 0; + +pub const PORT_CONNECT: u32 = 1 << 0; +pub const PORT_CONNECT_CHANGE: u32 = 1 << 1; +pub const PORT_ENABLE: u32 = 1 << 2; +pub const PORT_ENABLE_CHANGE: u32 = 1 << 3; +pub const PORT_OVER_CURRENT_ACTIVE: u32 = 1 << 4; +pub const PORT_OVER_CURRENT_CHANGE: u32 = 1 << 5; +pub const PORT_FORCE_PORT_RESUME: u32 = 1 << 6; +pub const PORT_SUSPEND: u32 = 1 << 7; +pub const PORT_RESET: u32 = 1 << 8; +pub const PORT_LINE_STATUS: u32 = 0x3 << 10; +pub const PORT_LINE_STATUS_K: u32 = 0x1 << 10; +pub const PORT_LINE_STATUS_J: u32 = 0x2 << 10; +pub const PORT_POWER: u32 = 1 << 12; +pub const PORT_OWNER: u32 = 1 << 13; +pub const PORT_INDICATOR: u32 = 0x3 << 14; +pub const PORT_TEST_CONTROL: u32 = 0xF << 16; +pub const PORT_WAKE_CONNECT: u32 = 1 << 20; +pub const PORT_WAKE_DISCONNECT: u32 = 1 << 21; +pub const PORT_WAKE_OVER_CURRENT: u32 = 1 << 22; +pub const PORTSC_CHANGE_BITS: u32 = + PORT_CONNECT_CHANGE | PORT_ENABLE_CHANGE | PORT_OVER_CURRENT_CHANGE; +pub const PORTSC_WRITE_MASK: u32 = PORT_ENABLE + | PORT_FORCE_PORT_RESUME + | PORT_SUSPEND + | PORT_RESET + | PORT_POWER + | PORT_OWNER + | PORT_INDICATOR + | PORT_TEST_CONTROL + | PORT_WAKE_CONNECT + | PORT_WAKE_DISCONNECT + | PORT_WAKE_OVER_CURRENT; + +pub fn portsc_offset(port: usize) -> usize { + PORTSC_BASE + (port * 4) +} + +#[derive(Debug)] +pub struct HcCapParams { + pub n_ports: u8, + pub port_routing_rules: bool, + pub n_cc: u8, + pub n_pcc: u8, + pub port_route: u32, +} + +impl HcCapParams { + pub fn from_hcsparams(hcsparams: u32) -> Self { + HcCapParams { + n_ports: (hcsparams & 0xF) as u8, + port_routing_rules: (hcsparams >> 4) & 1 != 0, + n_cc: ((hcsparams >> 8) & 0xF) as u8, + n_pcc: ((hcsparams >> 12) & 0xF) as u8, + port_route: 0, + } + } +} + +pub struct EhciRegisters { + pub mmio_base: usize, + pub mmio_size: usize, + pub op_base: usize, + pub n_ports: u8, + pub frame_list_size: u32, + pub has_64bit: bool, +} + +impl EhciRegisters { + pub fn read32(&self, offset: usize) -> u32 { + let addr = self.mmio_base + offset; + unsafe { (addr as *const u32).read_volatile() } + } + + pub fn write32(&self, offset: usize, value: u32) { + let addr = self.mmio_base + offset; + unsafe { (addr as *mut u32).write_volatile(value) } + } + + pub fn read_op32(&self, offset: usize) -> u32 { + self.read32(self.op_base + offset) + } + + pub fn write_op32(&self, offset: usize, value: u32) { + self.write32(self.op_base + offset, value) + } +} + +#[derive(Clone, Copy, Debug)] +#[repr(C, align(64))] +pub struct QueueHead { + pub horiz_link: u32, + pub caps: [u32; 2], + pub current_qtd: u32, + pub overlay: [u32; 8], +} + +pub const QH_TERMINATE: u32 = 1; +pub const QH_HEAD_MASK: u32 = !0x1F; +pub const QH_LINK_TYPE_QH: u32 = 0x2; +pub const QH_ENDPOINT_DTC: u32 = 1 << 14; +pub const QH_ENDPOINT_HEAD: u32 = 1 << 15; +pub const QH_ENDPOINT_SPEED_HIGH: u32 = 0x2 << 12; +pub const QH_NAK_RELOAD_4: u32 = 0x4 << 28; +pub const QH_CAP_MULT_ONE: u32 = 0x1 << 30; + +impl QueueHead { + pub fn new() -> Self { + QueueHead { + horiz_link: QH_TERMINATE, + caps: [0; 2], + current_qtd: 0, + overlay: [TD_TERMINATE, TD_TERMINATE, 0, 0, 0, 0, 0, 0], + } + } +} + +#[derive(Clone, Copy, Debug)] +#[repr(C, align(32))] +pub struct TransferDescriptor { + pub next_qtd: u32, + pub alt_qtd: u32, + pub token: u32, + pub buffers: [u32; 5], +} + +pub const TD_TERMINATE: u32 = 1; +pub const TD_ACTIVE: u32 = 1 << 7; +pub const TD_HALTED: u32 = 1 << 6; +pub const TD_BUFERR: u32 = 1 << 5; +pub const TD_BABBLE: u32 = 1 << 4; +pub const TD_XACTERR: u32 = 1 << 3; +pub const TD_MISSED: u32 = 1 << 2; +pub const TD_C_PAGE_MASK: u32 = 0x7 << 12; +pub const TD_IOC: u32 = 1 << 15; +pub const TD_ERROR_COUNTER_3: u32 = 0x3 << 10; +pub const TD_TOTAL_BYTES_SHIFT: u32 = 16; +pub const TD_TOTAL_BYTES_MASK: u32 = 0x7FFF << TD_TOTAL_BYTES_SHIFT; +pub const TD_PID_IN: u32 = 1 << 8; +pub const TD_PID_OUT: u32 = 0 << 8; +pub const TD_PID_SETUP: u32 = 2 << 8; +pub const TD_PID_MASK: u32 = 3 << 8; + +pub fn qh_link_pointer(phys_addr: u64) -> u32 { + ((phys_addr as u32) & QH_HEAD_MASK) | QH_LINK_TYPE_QH +} + +pub fn qh_endpoint_characteristics( + device_address: u8, + endpoint: u8, + max_packet_size: u16, + head_of_reclamation: bool, +) -> u32 { + let mut value = u32::from(device_address & 0x7F) + | (u32::from(endpoint & 0x0F) << 8) + | QH_ENDPOINT_SPEED_HIGH + | QH_ENDPOINT_DTC + | (u32::from(max_packet_size & 0x07FF) << 16) + | QH_NAK_RELOAD_4; + + if head_of_reclamation { + value |= QH_ENDPOINT_HEAD; + } + + value +} + +pub fn qh_endpoint_capabilities() -> u32 { + QH_CAP_MULT_ONE +} + +fn fill_qtd_buffers(td: &mut TransferDescriptor, phys_addr: u64) { + let addr = phys_addr as u32; + td.buffers[0] = addr; + + let page_base = addr & !0xFFF; + for (index, slot) in td.buffers.iter_mut().enumerate().skip(1) { + *slot = page_base.wrapping_add((index as u32) * 0x1000); + } +} + +pub fn build_setup_td(phys_addr: u64, _setup_data: &[u8; 8], toggle: bool) -> TransferDescriptor { + let mut td = TransferDescriptor { + next_qtd: TD_TERMINATE, + alt_qtd: TD_TERMINATE, + token: TD_ACTIVE | TD_PID_SETUP | TD_ERROR_COUNTER_3 | (8 << TD_TOTAL_BYTES_SHIFT), + buffers: [0; 5], + }; + fill_qtd_buffers(&mut td, phys_addr); + if toggle { + td.token |= 1 << 31; + } + td +} + +pub fn build_data_td(phys_addr: u64, len: usize, dir_in: bool, toggle: bool) -> TransferDescriptor { + let pid = if dir_in { TD_PID_IN } else { TD_PID_OUT }; + let mut td = TransferDescriptor { + next_qtd: TD_TERMINATE, + alt_qtd: TD_TERMINATE, + token: TD_ACTIVE + | pid + | TD_ERROR_COUNTER_3 + | (((len as u32) & 0x7FFF) << TD_TOTAL_BYTES_SHIFT), + buffers: [0; 5], + }; + fill_qtd_buffers(&mut td, phys_addr); + if toggle { + td.token |= 1 << 31; + } + td +} + +pub fn build_status_td(dir_in: bool) -> TransferDescriptor { + let pid = if dir_in { TD_PID_OUT } else { TD_PID_IN }; + TransferDescriptor { + next_qtd: TD_TERMINATE, + alt_qtd: TD_TERMINATE, + token: TD_ACTIVE | pid | TD_ERROR_COUNTER_3 | TD_IOC | (1 << 31), + buffers: [0; 5], + } +} + +fn td_phys(td_pool_phys: u64, index: usize) -> Option { + let offset = index.checked_mul(size_of::())?; + let phys = td_pool_phys.checked_add(offset as u64)?; + Some((phys as u32) & !0x1F) +} + +pub fn build_control_transfer( + setup_phys: u64, + setup_data: &[u8; 8], + data_phys: u64, + data_len: usize, + dir_in: bool, + td_pool: &mut [TransferDescriptor], + td_pool_phys: u64, +) -> Option { + if td_pool.len() < 2 { + return None; + } + + let td_count = if data_len > 0 { 3 } else { 2 }; + if td_pool.len() < td_count { + return None; + } + + let setup_td_phys = td_phys(td_pool_phys, 0)?; + let status_td_phys = td_phys(td_pool_phys, td_count - 1)?; + + td_pool[0] = build_setup_td(setup_phys, setup_data, false); + + if data_len > 0 { + let data_td_phys = td_phys(td_pool_phys, 1)?; + td_pool[0].next_qtd = data_td_phys; + + td_pool[1] = build_data_td(data_phys, data_len, dir_in, true); + td_pool[1].next_qtd = status_td_phys; + td_pool[1].alt_qtd = status_td_phys; + + td_pool[2] = build_status_td(dir_in); + } else { + td_pool[0].next_qtd = status_td_phys; + td_pool[1] = build_status_td(dir_in); + } + + Some(setup_td_phys) +} diff --git a/local/recipes/drivers/ohcid/Cargo.toml b/local/recipes/drivers/ohcid/Cargo.toml new file mode 100644 index 00000000..8c41a743 --- /dev/null +++ b/local/recipes/drivers/ohcid/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "ohcid" +version = "0.1.0" +edition = "2024" +description = "OHCI USB 1.1 host controller driver for Red Bear OS" + +[[bin]] +name = "ohcid" +path = "src/main.rs" + +[dependencies] +usb-core = { path = "../../usb-core/source" } +redox_syscall = "0.7" +log = "0.4" diff --git a/local/recipes/drivers/ohcid/ohcid b/local/recipes/drivers/ohcid/ohcid new file mode 120000 index 00000000..98ff3190 --- /dev/null +++ b/local/recipes/drivers/ohcid/ohcid @@ -0,0 +1 @@ +../../local/recipes/drivers/ohcid \ No newline at end of file diff --git a/local/recipes/drivers/ohcid/recipe.toml b/local/recipes/drivers/ohcid/recipe.toml new file mode 100644 index 00000000..7f04f876 --- /dev/null +++ b/local/recipes/drivers/ohcid/recipe.toml @@ -0,0 +1,6 @@ +[source] +path = "source" +[build] +template = "cargo" +[package.files] +"/usr/lib/drivers/ohcid" = "ohcid" diff --git a/local/recipes/drivers/ohcid/source/Cargo.toml b/local/recipes/drivers/ohcid/source/Cargo.toml new file mode 100644 index 00000000..8c41a743 --- /dev/null +++ b/local/recipes/drivers/ohcid/source/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "ohcid" +version = "0.1.0" +edition = "2024" +description = "OHCI USB 1.1 host controller driver for Red Bear OS" + +[[bin]] +name = "ohcid" +path = "src/main.rs" + +[dependencies] +usb-core = { path = "../../usb-core/source" } +redox_syscall = "0.7" +log = "0.4" diff --git a/local/recipes/drivers/ohcid/source/src/lib.rs b/local/recipes/drivers/ohcid/source/src/lib.rs new file mode 100644 index 00000000..b93cf3ff --- /dev/null +++ b/local/recipes/drivers/ohcid/source/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/local/recipes/drivers/ohcid/source/src/main.rs b/local/recipes/drivers/ohcid/source/src/main.rs new file mode 100644 index 00000000..9a8bf515 --- /dev/null +++ b/local/recipes/drivers/ohcid/source/src/main.rs @@ -0,0 +1,23 @@ +mod registers; + +use std::env; +use std::process; +use log::{info, error, LevelFilter}; + +struct StderrLogger; +impl log::Log for StderrLogger { + fn enabled(&self, md: &log::Metadata) -> bool { md.level() <= LevelFilter::Info } + fn log(&self, r: &log::Record) { eprintln!("[{}] ohcid: {}", r.level(), r.args()); } + fn flush(&self) {} +} + +fn main() { + log::set_logger(&StderrLogger).ok(); + log::set_max_level(LevelFilter::Info); + let channel_fd: usize = match env::var("PCID_CLIENT_CHANNEL") { + Ok(s) => match s.parse() { Ok(fd) => fd, Err(_) => { error!("invalid PCID_CLIENT_CHANNEL"); process::exit(1); } }, + Err(_) => { error!("PCID_CLIENT_CHANNEL not set"); process::exit(1); } + }; + info!("OHCI USB 1.1 controller (PCI fd: {})", channel_fd); + info!("ohcid: ready"); +} diff --git a/local/recipes/drivers/ohcid/source/src/main.rs.bak b/local/recipes/drivers/ohcid/source/src/main.rs.bak new file mode 100644 index 00000000..9a8bf515 --- /dev/null +++ b/local/recipes/drivers/ohcid/source/src/main.rs.bak @@ -0,0 +1,23 @@ +mod registers; + +use std::env; +use std::process; +use log::{info, error, LevelFilter}; + +struct StderrLogger; +impl log::Log for StderrLogger { + fn enabled(&self, md: &log::Metadata) -> bool { md.level() <= LevelFilter::Info } + fn log(&self, r: &log::Record) { eprintln!("[{}] ohcid: {}", r.level(), r.args()); } + fn flush(&self) {} +} + +fn main() { + log::set_logger(&StderrLogger).ok(); + log::set_max_level(LevelFilter::Info); + let channel_fd: usize = match env::var("PCID_CLIENT_CHANNEL") { + Ok(s) => match s.parse() { Ok(fd) => fd, Err(_) => { error!("invalid PCID_CLIENT_CHANNEL"); process::exit(1); } }, + Err(_) => { error!("PCID_CLIENT_CHANNEL not set"); process::exit(1); } + }; + info!("OHCI USB 1.1 controller (PCI fd: {})", channel_fd); + info!("ohcid: ready"); +} diff --git a/local/recipes/drivers/ohcid/source/src/registers.rs b/local/recipes/drivers/ohcid/source/src/registers.rs new file mode 100644 index 00000000..f7d8c472 --- /dev/null +++ b/local/recipes/drivers/ohcid/source/src/registers.rs @@ -0,0 +1,44 @@ +#![allow(dead_code)] +pub const HCREVISION: usize = 0x00; +pub const HCCONTROL: usize = 0x04; +pub const HCCOMMANDSTATUS: usize = 0x08; +pub const HCINTERRUPTSTATUS: usize = 0x0C; +pub const HCINTERRUPTENABLE: usize = 0x10; +pub const HCHCCA: usize = 0x18; +pub const HCCONTROLHEADED: usize = 0x20; +pub const HCBULKHEADED: usize = 0x28; +pub const HCDONEHEAD: usize = 0x30; +pub const HCFMINTERVAL: usize = 0x34; +pub const HCFMREMAINING: usize = 0x38; +pub const HCFMNUMBER: usize = 0x3C; +pub const HCRHDESCRIPTORA: usize = 0x48; +pub const HCRHSTATUS: usize = 0x50; +pub const HCRHPORTSTATUS1: usize = 0x54; + +pub const CONTROL_BULK_ENABLE: u32 = 1 << 3; +pub const PERIODIC_ENABLE: u32 = 1 << 4; +pub const CONTROL_ENABLE: u32 = 1 << 6; +pub const BULK_ENABLE: u32 = 1 << 7; +pub const HC_FUNCTIONAL_STATE_MASK: u32 = 0x3 << 6; +pub const HC_RESET: u32 = 0; +pub const HC_RESUME: u32 = 1 << 6; +pub const HC_OPERATIONAL: u32 = 2 << 6; +pub const HC_SUSPEND: u32 = 3 << 6; + +pub const PORT_CURRENT_CONNECT: u32 = 1 << 0; +pub const PORT_ENABLE: u32 = 1 << 1; +pub const PORT_SUSPEND: u32 = 1 << 2; +pub const PORT_OVER_CURRENT: u32 = 1 << 3; +pub const PORT_RESET: u32 = 1 << 4; +pub const PORT_POWER: u32 = 1 << 8; +pub const PORT_LOW_SPEED: u32 = 1 << 9; +pub const PORT_CONNECT_CHANGE: u32 = 1 << 16; +pub const PORT_ENABLE_CHANGE: u32 = 1 << 17; + +pub const WRITE_BACK_DONE_HEAD: u32 = 1 << 1; +pub const START_OF_FRAME: u32 = 1 << 2; +pub const RESUME_DETECTED: u32 = 1 << 3; +pub const ROOT_HUB_STATUS_CHANGE: u32 = 1 << 6; + +pub const HCCA_SIZE: usize = 256; +pub const HCCA_ALIGN: usize = 256; diff --git a/local/recipes/drivers/redox-driver-core/Cargo.toml b/local/recipes/drivers/redox-driver-core/Cargo.toml new file mode 100644 index 00000000..cfcfab8a --- /dev/null +++ b/local/recipes/drivers/redox-driver-core/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "redox-driver-core" +version = "0.1.0" +edition = "2024" +description = "Core device-model traits and orchestration for Red Bear drivers" + +[features] +default = ["std"] +alloc = [] +std = ["alloc"] +hotplug = [] + +[dependencies] +hashbrown = { version = "0.15", default-features = false, features = ["default-hasher"] } diff --git a/local/recipes/drivers/redox-driver-core/recipe.toml b/local/recipes/drivers/redox-driver-core/recipe.toml new file mode 100644 index 00000000..4e47e6bb --- /dev/null +++ b/local/recipes/drivers/redox-driver-core/recipe.toml @@ -0,0 +1,5 @@ +[source] +path = "source" + +[build] +template = "cargo" diff --git a/local/recipes/drivers/redox-driver-core/source/Cargo.toml b/local/recipes/drivers/redox-driver-core/source/Cargo.toml new file mode 100644 index 00000000..cfcfab8a --- /dev/null +++ b/local/recipes/drivers/redox-driver-core/source/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "redox-driver-core" +version = "0.1.0" +edition = "2024" +description = "Core device-model traits and orchestration for Red Bear drivers" + +[features] +default = ["std"] +alloc = [] +std = ["alloc"] +hotplug = [] + +[dependencies] +hashbrown = { version = "0.15", default-features = false, features = ["default-hasher"] } diff --git a/local/recipes/drivers/redox-driver-core/source/src/bus.rs b/local/recipes/drivers/redox-driver-core/source/src/bus.rs new file mode 100644 index 00000000..1ef64c80 --- /dev/null +++ b/local/recipes/drivers/redox-driver-core/source/src/bus.rs @@ -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, 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; +} + +/// 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), +} diff --git a/local/recipes/drivers/redox-driver-core/source/src/device.rs b/local/recipes/drivers/redox-driver-core/source/src/device.rs new file mode 100644 index 00000000..76f33a3f --- /dev/null +++ b/local/recipes/drivers/redox-driver-core/source/src/device.rs @@ -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, + /// Optional device identifier reported by the bus or firmware. + pub device: Option, + /// Optional base class code. + pub class: Option, + /// Optional subclass code. + pub subclass: Option, + /// Optional programming-interface code. + pub prog_if: Option, + /// Optional hardware revision code. + pub revision: Option, + /// Optional subsystem vendor identifier. + pub subsystem_vendor: Option, + /// Optional subsystem device identifier. + pub subsystem_device: Option, + /// 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, +} + +/// 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, +} diff --git a/local/recipes/drivers/redox-driver-core/source/src/driver.rs b/local/recipes/drivers/redox-driver-core/source/src/driver.rs new file mode 100644 index 00000000..a64268ce --- /dev/null +++ b/local/recipes/drivers/redox-driver-core/source/src/driver.rs @@ -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" + )); + } +} diff --git a/local/recipes/drivers/redox-driver-core/source/src/hotplug.rs b/local/recipes/drivers/redox-driver-core/source/src/hotplug.rs new file mode 100644 index 00000000..19cccb00 --- /dev/null +++ b/local/recipes/drivers/redox-driver-core/source/src/hotplug.rs @@ -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, +} + +/// 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), +} diff --git a/local/recipes/drivers/redox-driver-core/source/src/lib.rs b/local/recipes/drivers/redox-driver-core/source/src/lib.rs new file mode 100644 index 00000000..c3210e40 --- /dev/null +++ b/local/recipes/drivers/redox-driver-core/source/src/lib.rs @@ -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}; diff --git a/local/recipes/drivers/redox-driver-core/source/src/manager.rs b/local/recipes/drivers/redox-driver-core/source/src/manager.rs new file mode 100644 index 00000000..5f1182ca --- /dev/null +++ b/local/recipes/drivers/redox-driver-core/source/src/manager.rs @@ -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>, + drivers: Vec>, + bound_devices: BTreeMap, + 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) { + self.buses.push(bus); + } + + /// Registers a driver and reorders drivers so higher-priority probes run first. + pub fn register_driver(&mut self, driver: Box) { + 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 { + 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 { + 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) { + 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, + } + + impl Bus for MockBus { + fn name(&self) -> &str { + self.name + } + + fn enumerate_devices(&self) -> Result, BusError> { + Ok(self.devices.clone()) + } + } + + struct MockDriver { + name: &'static str, + description: &'static str, + priority: i32, + matches: Vec, + } + + 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 + ))); + } +} diff --git a/local/recipes/drivers/redox-driver-core/source/src/match.rs b/local/recipes/drivers/redox-driver-core/source/src/match.rs new file mode 100644 index 00000000..8e795d12 --- /dev/null +++ b/local/recipes/drivers/redox-driver-core/source/src/match.rs @@ -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, + /// Optional device identifier match. + pub device: Option, + /// Optional class-code match. + pub class: Option, + /// Optional subclass-code match. + pub subclass: Option, + /// Optional programming-interface match. + pub prog_if: Option, + /// Optional subsystem vendor match. + pub subsystem_vendor: Option, + /// Optional subsystem device match. + pub subsystem_device: Option, +} + +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, +} + +impl MatchTable { + /// Creates a new match table from the provided entries. + pub fn new(entries: Vec) -> 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> for MatchTable { + fn from(entries: Vec) -> 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)); + } +} diff --git a/local/recipes/drivers/redox-driver-core/source/src/params.rs b/local/recipes/drivers/redox-driver-core/source/src/params.rs new file mode 100644 index 00000000..65257732 --- /dev/null +++ b/local/recipes/drivers/redox-driver-core/source/src/params.rs @@ -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), +} + +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, + pub values: BTreeMap, +} + +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 { + 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 { + s.parse().ok() + } + + pub fn parse_uint(s: &str) -> Option { + 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)); + } +} diff --git a/local/recipes/drivers/redox-driver-core/src/bus.rs b/local/recipes/drivers/redox-driver-core/src/bus.rs new file mode 100644 index 00000000..1ef64c80 --- /dev/null +++ b/local/recipes/drivers/redox-driver-core/src/bus.rs @@ -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, 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; +} + +/// 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), +} diff --git a/local/recipes/drivers/redox-driver-core/src/device.rs b/local/recipes/drivers/redox-driver-core/src/device.rs new file mode 100644 index 00000000..76f33a3f --- /dev/null +++ b/local/recipes/drivers/redox-driver-core/src/device.rs @@ -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, + /// Optional device identifier reported by the bus or firmware. + pub device: Option, + /// Optional base class code. + pub class: Option, + /// Optional subclass code. + pub subclass: Option, + /// Optional programming-interface code. + pub prog_if: Option, + /// Optional hardware revision code. + pub revision: Option, + /// Optional subsystem vendor identifier. + pub subsystem_vendor: Option, + /// Optional subsystem device identifier. + pub subsystem_device: Option, + /// 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, +} + +/// 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, +} diff --git a/local/recipes/drivers/redox-driver-core/src/driver.rs b/local/recipes/drivers/redox-driver-core/src/driver.rs new file mode 100644 index 00000000..a64268ce --- /dev/null +++ b/local/recipes/drivers/redox-driver-core/src/driver.rs @@ -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" + )); + } +} diff --git a/local/recipes/drivers/redox-driver-core/src/hotplug.rs b/local/recipes/drivers/redox-driver-core/src/hotplug.rs new file mode 100644 index 00000000..19cccb00 --- /dev/null +++ b/local/recipes/drivers/redox-driver-core/src/hotplug.rs @@ -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, +} + +/// 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), +} diff --git a/local/recipes/drivers/redox-driver-core/src/lib.rs b/local/recipes/drivers/redox-driver-core/src/lib.rs new file mode 100644 index 00000000..c3210e40 --- /dev/null +++ b/local/recipes/drivers/redox-driver-core/src/lib.rs @@ -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}; diff --git a/local/recipes/drivers/redox-driver-core/src/manager.rs b/local/recipes/drivers/redox-driver-core/src/manager.rs new file mode 100644 index 00000000..5f1182ca --- /dev/null +++ b/local/recipes/drivers/redox-driver-core/src/manager.rs @@ -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>, + drivers: Vec>, + bound_devices: BTreeMap, + 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) { + self.buses.push(bus); + } + + /// Registers a driver and reorders drivers so higher-priority probes run first. + pub fn register_driver(&mut self, driver: Box) { + 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 { + 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 { + 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) { + 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, + } + + impl Bus for MockBus { + fn name(&self) -> &str { + self.name + } + + fn enumerate_devices(&self) -> Result, BusError> { + Ok(self.devices.clone()) + } + } + + struct MockDriver { + name: &'static str, + description: &'static str, + priority: i32, + matches: Vec, + } + + 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 + ))); + } +} diff --git a/local/recipes/drivers/redox-driver-core/src/match.rs b/local/recipes/drivers/redox-driver-core/src/match.rs new file mode 100644 index 00000000..8e795d12 --- /dev/null +++ b/local/recipes/drivers/redox-driver-core/src/match.rs @@ -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, + /// Optional device identifier match. + pub device: Option, + /// Optional class-code match. + pub class: Option, + /// Optional subclass-code match. + pub subclass: Option, + /// Optional programming-interface match. + pub prog_if: Option, + /// Optional subsystem vendor match. + pub subsystem_vendor: Option, + /// Optional subsystem device match. + pub subsystem_device: Option, +} + +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, +} + +impl MatchTable { + /// Creates a new match table from the provided entries. + pub fn new(entries: Vec) -> 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> for MatchTable { + fn from(entries: Vec) -> 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)); + } +} diff --git a/local/recipes/drivers/redox-driver-core/src/params.rs b/local/recipes/drivers/redox-driver-core/src/params.rs new file mode 100644 index 00000000..65257732 --- /dev/null +++ b/local/recipes/drivers/redox-driver-core/src/params.rs @@ -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), +} + +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, + pub values: BTreeMap, +} + +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 { + 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 { + s.parse().ok() + } + + pub fn parse_uint(s: &str) -> Option { + 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)); + } +} diff --git a/local/recipes/drivers/redox-driver-pci/Cargo.toml b/local/recipes/drivers/redox-driver-pci/Cargo.toml new file mode 100644 index 00000000..52beeb71 --- /dev/null +++ b/local/recipes/drivers/redox-driver-pci/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "redox-driver-pci" +version = "0.1.0" +edition = "2024" +description = "PCI bus backend for redox-driver-core" + +[dependencies] +redox-driver-core = { path = "../redox-driver-core" } +redox_syscall = "0.7" diff --git a/local/recipes/drivers/redox-driver-pci/recipe.toml b/local/recipes/drivers/redox-driver-pci/recipe.toml new file mode 100644 index 00000000..cebe0dc7 --- /dev/null +++ b/local/recipes/drivers/redox-driver-pci/recipe.toml @@ -0,0 +1,8 @@ +[source] +path = "source" + +[build] +template = "cargo" + +[dependencies] +redox-driver-core = {} diff --git a/local/recipes/drivers/redox-driver-pci/source/Cargo.toml b/local/recipes/drivers/redox-driver-pci/source/Cargo.toml new file mode 100644 index 00000000..cc061370 --- /dev/null +++ b/local/recipes/drivers/redox-driver-pci/source/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "redox-driver-pci" +version = "0.1.0" +edition = "2024" +description = "PCI bus backend for redox-driver-core" + +[dependencies] +redox-driver-core = { path = "../../redox-driver-core/source" } +redox_syscall = "0.7" diff --git a/local/recipes/drivers/redox-driver-pci/source/src/bus.rs b/local/recipes/drivers/redox-driver-pci/source/src/bus.rs new file mode 100644 index 00000000..77b788fa --- /dev/null +++ b/local/recipes/drivers/redox-driver-pci/source/src/bus.rs @@ -0,0 +1,138 @@ +use std::fs; +use std::string::String; +use std::vec::Vec; + +use redox_driver_core::bus::{Bus, BusError}; +use redox_driver_core::device::{DeviceId, DeviceInfo}; + +pub struct PciBus { + pci_root: String, +} + +impl PciBus { + pub fn new() -> Self { + PciBus { + pci_root: String::from("/scheme/pci"), + } + } + + pub fn with_root(root: &str) -> Self { + PciBus { + pci_root: String::from(root), + } + } +} + +impl Bus for PciBus { + fn name(&self) -> &str { + "pci" + } + + fn enumerate_devices(&self) -> Result, BusError> { + let dir = fs::read_dir(&self.pci_root).map_err(|_| BusError::IoError)?; + + let mut devices = Vec::new(); + + for entry in dir { + let entry = match entry { + Ok(e) => e, + Err(_) => continue, + }; + + let path = entry.path(); + let file_name = match entry.file_name().into_string() { + Ok(n) => n, + Err(_) => continue, + }; + + if file_name == "." || file_name == ".." { + continue; + } + + let config_path = path.join("config"); + let config_data = match fs::read(&config_path) { + Ok(d) => d, + Err(_) => continue, + }; + + if config_data.len() < 64 { + continue; + } + + let vendor = u16::from_le_bytes([config_data[0], config_data[1]]); + let device = u16::from_le_bytes([config_data[2], config_data[3]]); + let revision = config_data[8]; + let prog_if = config_data[9]; + let subclass = config_data[10]; + let class = config_data[11]; + + let subsystem_vendor = if config_data.len() > 0x2E { + Some(u16::from_le_bytes([config_data[0x2C], config_data[0x2D]])) + } else { + None + }; + let subsystem_device = if config_data.len() > 0x2E { + Some(u16::from_le_bytes([config_data[0x2E], config_data[0x2F]])) + } else { + None + }; + + if vendor == 0xFFFF && device == 0xFFFF { + continue; + } + + let device_path = format!("{}/{}", self.pci_root, file_name); + + devices.push(DeviceInfo { + id: DeviceId { + bus: String::from("pci"), + path: file_name, + }, + vendor: Some(vendor), + device: Some(device), + class: Some(class), + subclass: Some(subclass), + prog_if: Some(prog_if), + revision: Some(revision), + subsystem_vendor, + subsystem_device, + raw_path: device_path, + description: None, + }); + } + + Ok(devices) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn pci_bus_name_is_pci() { + let bus = PciBus::new(); + assert_eq!(bus.name(), "pci"); + } + + #[test] + fn pci_bus_with_custom_root() { + let bus = PciBus::with_root("/tmp/fake-pci"); + assert_eq!(bus.name(), "pci"); + let result = bus.enumerate_devices(); + assert!(result.is_err() || result.is_ok()); + } + + #[test] + fn device_id_ordering_allows_btree_map() { + let a = DeviceId { + bus: String::from("pci"), + path: String::from("00.00.0"), + }; + let b = DeviceId { + bus: String::from("pci"), + path: String::from("00.02.0"), + }; + assert!(a < b); + } +} diff --git a/local/recipes/drivers/redox-driver-pci/source/src/lib.rs b/local/recipes/drivers/redox-driver-pci/source/src/lib.rs new file mode 100644 index 00000000..24602af3 --- /dev/null +++ b/local/recipes/drivers/redox-driver-pci/source/src/lib.rs @@ -0,0 +1,3 @@ +pub mod bus; + +pub use bus::PciBus; diff --git a/local/recipes/drivers/redox-driver-pci/src/bus.rs b/local/recipes/drivers/redox-driver-pci/src/bus.rs new file mode 100644 index 00000000..77b788fa --- /dev/null +++ b/local/recipes/drivers/redox-driver-pci/src/bus.rs @@ -0,0 +1,138 @@ +use std::fs; +use std::string::String; +use std::vec::Vec; + +use redox_driver_core::bus::{Bus, BusError}; +use redox_driver_core::device::{DeviceId, DeviceInfo}; + +pub struct PciBus { + pci_root: String, +} + +impl PciBus { + pub fn new() -> Self { + PciBus { + pci_root: String::from("/scheme/pci"), + } + } + + pub fn with_root(root: &str) -> Self { + PciBus { + pci_root: String::from(root), + } + } +} + +impl Bus for PciBus { + fn name(&self) -> &str { + "pci" + } + + fn enumerate_devices(&self) -> Result, BusError> { + let dir = fs::read_dir(&self.pci_root).map_err(|_| BusError::IoError)?; + + let mut devices = Vec::new(); + + for entry in dir { + let entry = match entry { + Ok(e) => e, + Err(_) => continue, + }; + + let path = entry.path(); + let file_name = match entry.file_name().into_string() { + Ok(n) => n, + Err(_) => continue, + }; + + if file_name == "." || file_name == ".." { + continue; + } + + let config_path = path.join("config"); + let config_data = match fs::read(&config_path) { + Ok(d) => d, + Err(_) => continue, + }; + + if config_data.len() < 64 { + continue; + } + + let vendor = u16::from_le_bytes([config_data[0], config_data[1]]); + let device = u16::from_le_bytes([config_data[2], config_data[3]]); + let revision = config_data[8]; + let prog_if = config_data[9]; + let subclass = config_data[10]; + let class = config_data[11]; + + let subsystem_vendor = if config_data.len() > 0x2E { + Some(u16::from_le_bytes([config_data[0x2C], config_data[0x2D]])) + } else { + None + }; + let subsystem_device = if config_data.len() > 0x2E { + Some(u16::from_le_bytes([config_data[0x2E], config_data[0x2F]])) + } else { + None + }; + + if vendor == 0xFFFF && device == 0xFFFF { + continue; + } + + let device_path = format!("{}/{}", self.pci_root, file_name); + + devices.push(DeviceInfo { + id: DeviceId { + bus: String::from("pci"), + path: file_name, + }, + vendor: Some(vendor), + device: Some(device), + class: Some(class), + subclass: Some(subclass), + prog_if: Some(prog_if), + revision: Some(revision), + subsystem_vendor, + subsystem_device, + raw_path: device_path, + description: None, + }); + } + + Ok(devices) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn pci_bus_name_is_pci() { + let bus = PciBus::new(); + assert_eq!(bus.name(), "pci"); + } + + #[test] + fn pci_bus_with_custom_root() { + let bus = PciBus::with_root("/tmp/fake-pci"); + assert_eq!(bus.name(), "pci"); + let result = bus.enumerate_devices(); + assert!(result.is_err() || result.is_ok()); + } + + #[test] + fn device_id_ordering_allows_btree_map() { + let a = DeviceId { + bus: String::from("pci"), + path: String::from("00.00.0"), + }; + let b = DeviceId { + bus: String::from("pci"), + path: String::from("00.02.0"), + }; + assert!(a < b); + } +} diff --git a/local/recipes/drivers/redox-driver-pci/src/lib.rs b/local/recipes/drivers/redox-driver-pci/src/lib.rs new file mode 100644 index 00000000..24602af3 --- /dev/null +++ b/local/recipes/drivers/redox-driver-pci/src/lib.rs @@ -0,0 +1,3 @@ +pub mod bus; + +pub use bus::PciBus; diff --git a/local/recipes/drivers/uhcid/Cargo.toml b/local/recipes/drivers/uhcid/Cargo.toml new file mode 100644 index 00000000..32f52e4c --- /dev/null +++ b/local/recipes/drivers/uhcid/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "uhcid" +version = "0.1.0" +edition = "2024" +description = "UHCI USB 1.1 host controller driver for Red Bear OS" + +[[bin]] +name = "uhcid" +path = "src/main.rs" + +[dependencies] +usb-core = { path = "../../usb-core/source" } +redox_syscall = "0.7" +log = "0.4" diff --git a/local/recipes/drivers/uhcid/recipe.toml b/local/recipes/drivers/uhcid/recipe.toml new file mode 100644 index 00000000..735afa97 --- /dev/null +++ b/local/recipes/drivers/uhcid/recipe.toml @@ -0,0 +1,6 @@ +[source] +path = "source" +[build] +template = "cargo" +[package.files] +"/usr/lib/drivers/uhcid" = "uhcid" diff --git a/local/recipes/drivers/uhcid/source/Cargo.toml b/local/recipes/drivers/uhcid/source/Cargo.toml new file mode 100644 index 00000000..32f52e4c --- /dev/null +++ b/local/recipes/drivers/uhcid/source/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "uhcid" +version = "0.1.0" +edition = "2024" +description = "UHCI USB 1.1 host controller driver for Red Bear OS" + +[[bin]] +name = "uhcid" +path = "src/main.rs" + +[dependencies] +usb-core = { path = "../../usb-core/source" } +redox_syscall = "0.7" +log = "0.4" diff --git a/local/recipes/drivers/uhcid/source/src/lib.rs b/local/recipes/drivers/uhcid/source/src/lib.rs new file mode 100644 index 00000000..b93cf3ff --- /dev/null +++ b/local/recipes/drivers/uhcid/source/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/local/recipes/drivers/uhcid/source/src/main.rs b/local/recipes/drivers/uhcid/source/src/main.rs new file mode 100644 index 00000000..49bf6357 --- /dev/null +++ b/local/recipes/drivers/uhcid/source/src/main.rs @@ -0,0 +1,23 @@ +mod registers; + +use std::env; +use std::process; +use log::{info, error, LevelFilter}; + +struct StderrLogger; +impl log::Log for StderrLogger { + fn enabled(&self, md: &log::Metadata) -> bool { md.level() <= LevelFilter::Info } + fn log(&self, r: &log::Record) { eprintln!("[{}] uhcid: {}", r.level(), r.args()); } + fn flush(&self) {} +} + +fn main() { + log::set_logger(&StderrLogger).ok(); + log::set_max_level(LevelFilter::Info); + let channel_fd: usize = match env::var("PCID_CLIENT_CHANNEL") { + Ok(s) => match s.parse() { Ok(fd) => fd, Err(_) => { error!("invalid PCID_CLIENT_CHANNEL"); process::exit(1); } }, + Err(_) => { error!("PCID_CLIENT_CHANNEL not set"); process::exit(1); } + }; + info!("UHCI USB 1.1 controller (PCI fd: {})", channel_fd); + info!("uhcid: ready"); +} diff --git a/local/recipes/drivers/uhcid/source/src/main.rs.bak b/local/recipes/drivers/uhcid/source/src/main.rs.bak new file mode 100644 index 00000000..49bf6357 --- /dev/null +++ b/local/recipes/drivers/uhcid/source/src/main.rs.bak @@ -0,0 +1,23 @@ +mod registers; + +use std::env; +use std::process; +use log::{info, error, LevelFilter}; + +struct StderrLogger; +impl log::Log for StderrLogger { + fn enabled(&self, md: &log::Metadata) -> bool { md.level() <= LevelFilter::Info } + fn log(&self, r: &log::Record) { eprintln!("[{}] uhcid: {}", r.level(), r.args()); } + fn flush(&self) {} +} + +fn main() { + log::set_logger(&StderrLogger).ok(); + log::set_max_level(LevelFilter::Info); + let channel_fd: usize = match env::var("PCID_CLIENT_CHANNEL") { + Ok(s) => match s.parse() { Ok(fd) => fd, Err(_) => { error!("invalid PCID_CLIENT_CHANNEL"); process::exit(1); } }, + Err(_) => { error!("PCID_CLIENT_CHANNEL not set"); process::exit(1); } + }; + info!("UHCI USB 1.1 controller (PCI fd: {})", channel_fd); + info!("uhcid: ready"); +} diff --git a/local/recipes/drivers/uhcid/source/src/registers.rs b/local/recipes/drivers/uhcid/source/src/registers.rs new file mode 100644 index 00000000..aaeebaf8 --- /dev/null +++ b/local/recipes/drivers/uhcid/source/src/registers.rs @@ -0,0 +1,31 @@ +#![allow(dead_code)] +pub const USBCMD: u16 = 0x00; +pub const USBSTS: u16 = 0x02; +pub const USBINTR: u16 = 0x04; +pub const FRNUM: u16 = 0x06; +pub const FRBASEADD: u16 = 0x08; +pub const SOFMOD: u16 = 0x0C; +pub const PORTSC1: u16 = 0x10; +pub const PORTSC2: u16 = 0x12; + +pub const CMD_RUN_STOP: u16 = 1 << 0; +pub const CMD_HOST_RESET: u16 = 1 << 1; +pub const CMD_GLOBAL_RESET: u16 = 1 << 2; +pub const CMD_CONFIGURE: u16 = 1 << 6; +pub const CMD_MAX_PACKET_64: u16 = 1 << 7; + +pub const STS_INTERRUPT: u16 = 1 << 0; +pub const STS_ERROR: u16 = 1 << 1; +pub const STS_RESUME: u16 = 1 << 2; +pub const STS_HOST_ERROR: u16 = 1 << 3; +pub const STS_HALTED: u16 = 1 << 5; + +pub const PORT_CONNECT: u16 = 1 << 0; +pub const PORT_ENABLE: u16 = 1 << 1; +pub const PORT_SUSPEND: u16 = 1 << 2; +pub const PORT_OVER_CURRENT: u16 = 1 << 3; +pub const PORT_RESET: u16 = 1 << 4; +pub const PORT_LOW_SPEED: u16 = 1 << 8; + +pub const FRAME_COUNT: usize = 1024; +pub const FRAME_LIST_ALIGN: usize = 4096; diff --git a/local/recipes/drivers/uhcid/uhcid b/local/recipes/drivers/uhcid/uhcid new file mode 120000 index 00000000..f96a3ee6 --- /dev/null +++ b/local/recipes/drivers/uhcid/uhcid @@ -0,0 +1 @@ +../../local/recipes/drivers/uhcid \ No newline at end of file diff --git a/local/recipes/drivers/usb-core/Cargo.toml b/local/recipes/drivers/usb-core/Cargo.toml new file mode 100644 index 00000000..13a75cef --- /dev/null +++ b/local/recipes/drivers/usb-core/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "usb-core" +version = "0.1.0" +edition = "2024" +description = "Shared USB types and primitives for Red Bear host controller drivers" + +[features] +default = ["std"] +std = [] + +[lib] +path = "source/src/lib.rs" diff --git a/local/recipes/drivers/usb-core/recipe.toml b/local/recipes/drivers/usb-core/recipe.toml new file mode 100644 index 00000000..4e47e6bb --- /dev/null +++ b/local/recipes/drivers/usb-core/recipe.toml @@ -0,0 +1,5 @@ +[source] +path = "source" + +[build] +template = "cargo" diff --git a/local/recipes/drivers/usb-core/source/Cargo.toml b/local/recipes/drivers/usb-core/source/Cargo.toml new file mode 100644 index 00000000..e8562f1a --- /dev/null +++ b/local/recipes/drivers/usb-core/source/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "usb-core" +version = "0.1.0" +edition = "2024" +description = "Shared USB types and primitives for Red Bear host controller drivers" + +[features] +default = ["std"] +std = [] diff --git a/local/recipes/drivers/usb-core/source/src/dma.rs b/local/recipes/drivers/usb-core/source/src/dma.rs new file mode 100644 index 00000000..63c3b0cd --- /dev/null +++ b/local/recipes/drivers/usb-core/source/src/dma.rs @@ -0,0 +1,141 @@ +use alloc::boxed::Box; +use alloc::vec::Vec; + +/// DMA buffer for USB transfers. +/// +/// This crate intentionally avoids Redox-specific allocation APIs, so `allocate` +/// produces an owned staging buffer and leaves `physical_addr` unset (`0`). +/// Controller-specific code must install a real physical address before using +/// the buffer for hardware DMA. +pub struct DmaBuffer { + pub virtual_addr: usize, + pub physical_addr: u64, + pub size: usize, + data: Box<[u8]>, + mapped: bool, +} + +impl DmaBuffer { + /// Allocate an owned staging buffer for a future DMA mapping. + /// + /// The returned buffer is not DMA-mapped yet. `physical_addr` remains `0` + /// until the caller supplies a real mapping with `set_physical_addr`. + pub fn allocate(size: usize) -> Result { + if size > isize::MAX as usize { + return Err(DmaError::TooLarge); + } + + if size == 0 { + return Ok(Self { + virtual_addr: 0, + physical_addr: 0, + size: 0, + data: Vec::new().into_boxed_slice(), + mapped: true, + }); + } + + let mut data = Vec::new(); + if data.try_reserve_exact(size).is_err() { + return Err(DmaError::NoMemory); + } + + data.resize(size, 0); + let mut data = data.into_boxed_slice(); + let virtual_addr = data.as_mut_ptr() as usize; + + Ok(Self { + virtual_addr, + physical_addr: 0, + size, + data, + mapped: false, + }) + } + + pub fn as_slice(&self) -> &[u8] { + &self.data + } + + pub fn as_mut_slice(&mut self) -> &mut [u8] { + &mut self.data + } + + /// Returns `true` once the controller-specific layer has installed a + /// hardware-usable physical address for this buffer. + pub fn is_dma_mapped(&self) -> bool { + self.mapped + } + + /// Attach a controller/OS-specific physical address to the staging buffer. + pub fn set_physical_addr(&mut self, physical_addr: u64) -> Result<(), DmaError> { + if self.size != 0 && physical_addr == 0 { + return Err(DmaError::AllocFailed); + } + + self.physical_addr = physical_addr; + self.mapped = true; + Ok(()) + } +} + +#[derive(Debug)] +pub enum DmaError { + AllocFailed, + TooLarge, + NoMemory, +} + +#[cfg(test)] +mod tests { + use super::{DmaBuffer, DmaError}; + + #[test] + fn allocated_buffers_start_unmapped() { + let buffer = match DmaBuffer::allocate(16) { + Ok(buffer) => buffer, + Err(error) => panic!("expected allocation to succeed: {error:?}"), + }; + + assert_eq!(buffer.physical_addr, 0); + assert!(!buffer.is_dma_mapped()); + assert_eq!(buffer.as_slice().len(), 16); + } + + #[test] + fn attaching_a_physical_address_marks_the_buffer_mapped() { + let mut buffer = match DmaBuffer::allocate(16) { + Ok(buffer) => buffer, + Err(error) => panic!("expected allocation to succeed: {error:?}"), + }; + + let result = buffer.set_physical_addr(0x1000); + assert!(result.is_ok()); + assert_eq!(buffer.physical_addr, 0x1000); + assert!(buffer.is_dma_mapped()); + } + + #[test] + fn nonempty_buffers_reject_a_zero_physical_address() { + let mut buffer = match DmaBuffer::allocate(8) { + Ok(buffer) => buffer, + Err(error) => panic!("expected allocation to succeed: {error:?}"), + }; + + let result = buffer.set_physical_addr(0); + assert!(matches!(result, Err(DmaError::AllocFailed))); + } + + #[test] + fn direct_field_mutation_does_not_forge_mapping_state() { + let mut buffer = match DmaBuffer::allocate(8) { + Ok(buffer) => buffer, + Err(error) => panic!("expected allocation to succeed: {error:?}"), + }; + + buffer.physical_addr = 0x2000; + + assert_eq!(buffer.physical_addr, 0x2000); + assert!(!buffer.is_dma_mapped()); + } +} diff --git a/local/recipes/drivers/usb-core/source/src/lib.rs b/local/recipes/drivers/usb-core/source/src/lib.rs new file mode 100644 index 00000000..1779c10b --- /dev/null +++ b/local/recipes/drivers/usb-core/source/src/lib.rs @@ -0,0 +1,21 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#![doc = "Shared USB types and primitives for Red Bear host controller drivers."] + +extern crate alloc; + +pub mod dma; +pub mod scheme; +pub mod spawn; +pub mod transfer; +pub mod types; + +pub use dma::{DmaBuffer, DmaError}; +pub use scheme::{UsbError, UsbHostController}; +pub use spawn::spawn_usb_driver; +pub use transfer::{ + control_transfer, parse_config_descriptor, parse_device_descriptor, parse_endpoint_descriptor, +}; +pub use types::{ + ConfigDescriptor, DeviceDescriptor, EndpointDescriptor, PortStatus, SetupPacket, + TransferDirection, TransferType, Urb, UrbStatus, +}; diff --git a/local/recipes/drivers/usb-core/source/src/scheme.rs b/local/recipes/drivers/usb-core/source/src/scheme.rs new file mode 100644 index 00000000..df0d714b --- /dev/null +++ b/local/recipes/drivers/usb-core/source/src/scheme.rs @@ -0,0 +1,57 @@ +use crate::types::{PortStatus, SetupPacket, TransferDirection}; + +/// Trait that all USB host controller drivers must implement. +/// This provides a uniform scheme interface regardless of HC type (XHCI/EHCI/OHCI/UHCI). +pub trait UsbHostController { + /// Get the number of ports on this controller + fn port_count(&self) -> usize; + + /// Get status of a specific port + fn port_status(&self, port: usize) -> Option; + + /// Reset a port (USB enumeration step 1) + fn port_reset(&mut self, port: usize) -> bool; + + /// Submit a control transfer to endpoint 0 + fn control_transfer( + &mut self, + device_address: u8, + setup: &SetupPacket, + data: &mut [u8], + ) -> Result; + + /// Submit a bulk transfer + fn bulk_transfer( + &mut self, + device_address: u8, + endpoint: u8, + data: &mut [u8], + direction: TransferDirection, + ) -> Result; + + /// Submit an interrupt transfer + fn interrupt_transfer( + &mut self, + device_address: u8, + endpoint: u8, + data: &mut [u8], + ) -> Result; + + /// Set device address (after reset, before config) + fn set_address(&mut self, device_address: u8) -> bool; + + /// Get the controller name for logging + fn name(&self) -> &str; +} + +#[derive(Debug)] +pub enum UsbError { + Timeout, + Stall, + DataError, + Babble, + NoDevice, + NotConfigured, + IoError, + Unsupported, +} diff --git a/local/recipes/drivers/usb-core/source/src/spawn.rs b/local/recipes/drivers/usb-core/source/src/spawn.rs new file mode 100644 index 00000000..06a67005 --- /dev/null +++ b/local/recipes/drivers/usb-core/source/src/spawn.rs @@ -0,0 +1,52 @@ +/// Spawn a child USB class driver (hub, HID, storage). +/// On Redox, this forks and execs the driver binary with the USB device path. +#[cfg(feature = "std")] +pub fn spawn_usb_driver(driver_binary: &str, device_path: &str) { + if driver_binary.is_empty() + || device_path.is_empty() + || !driver_binary.starts_with('/') + || !device_path.starts_with('/') + || !is_trusted_usb_driver(driver_binary) + { + return; + } + + let mut command = std::process::Command::new(driver_binary); + command.env_clear(); + command.stdin(std::process::Stdio::null()); + command.arg(device_path); + let _ = command.spawn(); +} + +#[cfg(feature = "std")] +fn is_trusted_usb_driver(driver_binary: &str) -> bool { + matches!( + driver_binary, + "/usr/bin/usbhubd" | "/usr/bin/usbhidd" | "/usr/bin/usbscsid" + ) +} + +/// Spawn a child USB class driver (hub, HID, storage). +/// On no_std builds, class-driver spawning is not available. +#[cfg(not(feature = "std"))] +pub fn spawn_usb_driver(_driver_binary: &str, _device_path: &str) {} + +#[cfg(all(test, feature = "std"))] +mod tests { + use super::is_trusted_usb_driver; + + #[test] + fn trusted_driver_whitelist_allows_expected_binaries() { + assert!(is_trusted_usb_driver("/usr/bin/usbhubd")); + assert!(is_trusted_usb_driver("/usr/bin/usbhidd")); + assert!(is_trusted_usb_driver("/usr/bin/usbscsid")); + } + + #[test] + fn trusted_driver_whitelist_rejects_other_binaries() { + assert!(!is_trusted_usb_driver("/usr/bin/sh")); + assert!(!is_trusted_usb_driver("/tmp/usbhubd")); + assert!(!is_trusted_usb_driver("relative/usbhubd")); + assert!(!is_trusted_usb_driver("")); + } +} diff --git a/local/recipes/drivers/usb-core/source/src/transfer.rs b/local/recipes/drivers/usb-core/source/src/transfer.rs new file mode 100644 index 00000000..a27e7ab7 --- /dev/null +++ b/local/recipes/drivers/usb-core/source/src/transfer.rs @@ -0,0 +1,181 @@ +use alloc::vec::Vec; + +use crate::types::{ + ConfigDescriptor, DeviceDescriptor, EndpointDescriptor, SetupPacket, TransferDirection, + TransferType, Urb, UrbStatus, +}; + +/// Build a standard control transfer setup packet + data stage +pub fn control_transfer( + device_address: u8, + request_type: u8, + request: u8, + value: u16, + index: u16, + data: &[u8], + direction: TransferDirection, +) -> Urb { + let data = if data.len() > u16::MAX as usize { + &data[..u16::MAX as usize] + } else { + data + }; + + let setup = SetupPacket { + request_type, + request, + value, + index, + length: data.len() as u16, + }; + + let mut buffer = Vec::with_capacity(data.len().saturating_add(8)); + buffer.extend_from_slice(&setup_packet_bytes(&setup)); + buffer.extend_from_slice(data); + + Urb { + device_address, + endpoint: 0, + transfer_type: TransferType::Control, + direction, + buffer, + actual_length: 0, + status: UrbStatus::Pending, + timeout_ms: 1000, + } +} + +/// Parse a Device Descriptor from raw bytes +pub fn parse_device_descriptor(data: &[u8]) -> Option { + if data.len() < 18 || data[0] != 18 || data[1] != 0x01 { + return None; + } + + Some(DeviceDescriptor { + length: data[0], + descriptor_type: data[1], + usb_version: u16::from_le_bytes([data[2], data[3]]), + device_class: data[4], + device_subclass: data[5], + device_protocol: data[6], + max_packet_size0: data[7], + vendor_id: u16::from_le_bytes([data[8], data[9]]), + product_id: u16::from_le_bytes([data[10], data[11]]), + device_version: u16::from_le_bytes([data[12], data[13]]), + manufacturer_idx: data[14], + product_idx: data[15], + serial_idx: data[16], + num_configurations: data[17], + }) +} + +/// Parse a Configuration Descriptor from raw bytes +pub fn parse_config_descriptor(data: &[u8]) -> Option { + if data.len() < 9 || data[0] != 9 || data[1] != 0x02 { + return None; + } + + Some(ConfigDescriptor { + length: data[0], + descriptor_type: data[1], + total_length: u16::from_le_bytes([data[2], data[3]]), + num_interfaces: data[4], + config_value: data[5], + config_idx: data[6], + attributes: data[7], + max_power: data[8], + }) +} + +/// Parse an Endpoint Descriptor from raw bytes +pub fn parse_endpoint_descriptor(data: &[u8]) -> Option { + if data.len() < 7 || data[0] != 7 || data[1] != 0x05 { + return None; + } + + Some(EndpointDescriptor { + length: data[0], + descriptor_type: data[1], + endpoint_address: data[2], + attributes: data[3], + max_packet_size: u16::from_le_bytes([data[4], data[5]]), + interval: data[6], + }) +} + +fn setup_packet_bytes(setup: &SetupPacket) -> [u8; 8] { + let value = setup.value.to_le_bytes(); + let index = setup.index.to_le_bytes(); + let length = setup.length.to_le_bytes(); + + [ + setup.request_type, + setup.request, + value[0], + value[1], + index[0], + index[1], + length[0], + length[1], + ] +} + +#[cfg(test)] +mod tests { + use super::{ + parse_config_descriptor, parse_device_descriptor, parse_endpoint_descriptor, + }; + + #[test] + fn parses_valid_device_descriptor() { + let raw = [ + 18, 0x01, 0x00, 0x02, 0xff, 0x00, 0x00, 64, 0x34, 0x12, 0x78, 0x56, 0x00, 0x01, + 1, 2, 3, 1, + ]; + + match parse_device_descriptor(&raw) { + Some(descriptor) => { + assert_eq!(descriptor.length, 18); + assert_eq!(descriptor.descriptor_type, 0x01); + assert_eq!(descriptor.usb_version, 0x0200); + assert_eq!(descriptor.vendor_id, 0x1234); + assert_eq!(descriptor.product_id, 0x5678); + assert_eq!(descriptor.num_configurations, 1); + } + None => panic!("valid device descriptor should parse"), + } + } + + #[test] + fn parses_valid_config_descriptor() { + let raw = [9, 0x02, 32, 0, 1, 1, 0, 0x80, 50]; + + match parse_config_descriptor(&raw) { + Some(descriptor) => { + assert_eq!(descriptor.length, 9); + assert_eq!(descriptor.descriptor_type, 0x02); + assert_eq!(descriptor.total_length, 32); + assert_eq!(descriptor.num_interfaces, 1); + assert_eq!(descriptor.max_power, 50); + } + None => panic!("valid config descriptor should parse"), + } + } + + #[test] + fn parses_valid_endpoint_descriptor() { + let raw = [7, 0x05, 0x81, 0x03, 8, 0, 10]; + + match parse_endpoint_descriptor(&raw) { + Some(descriptor) => { + assert_eq!(descriptor.length, 7); + assert_eq!(descriptor.descriptor_type, 0x05); + assert_eq!(descriptor.endpoint_address, 0x81); + assert_eq!(descriptor.attributes, 0x03); + assert_eq!(descriptor.max_packet_size, 8); + assert_eq!(descriptor.interval, 10); + } + None => panic!("valid endpoint descriptor should parse"), + } + } +} diff --git a/local/recipes/drivers/usb-core/source/src/types.rs b/local/recipes/drivers/usb-core/source/src/types.rs new file mode 100644 index 00000000..cc4405cd --- /dev/null +++ b/local/recipes/drivers/usb-core/source/src/types.rs @@ -0,0 +1,151 @@ +use alloc::vec::Vec; + +// USB Standard Device Descriptor (USB 2.0 §9.6.1) +#[derive(Clone, Debug)] +pub struct DeviceDescriptor { + pub length: u8, + pub descriptor_type: u8, + pub usb_version: u16, + pub device_class: u8, + pub device_subclass: u8, + pub device_protocol: u8, + pub max_packet_size0: u8, + pub vendor_id: u16, + pub product_id: u16, + pub device_version: u16, + pub manufacturer_idx: u8, + pub product_idx: u8, + pub serial_idx: u8, + pub num_configurations: u8, +} + +// USB Configuration Descriptor (USB 2.0 §9.6.3) +#[derive(Clone, Debug)] +pub struct ConfigDescriptor { + pub length: u8, + pub descriptor_type: u8, + pub total_length: u16, + pub num_interfaces: u8, + pub config_value: u8, + pub config_idx: u8, + pub attributes: u8, + pub max_power: u8, +} + +// USB Endpoint Descriptor (USB 2.0 §9.6.6) +#[derive(Clone, Debug)] +pub struct EndpointDescriptor { + pub length: u8, + pub descriptor_type: u8, + pub endpoint_address: u8, + pub attributes: u8, + pub max_packet_size: u16, + pub interval: u8, +} + +// Standard Setup Packet (USB 2.0 §9.3) +#[derive(Clone, Debug)] +pub struct SetupPacket { + pub request_type: u8, + pub request: u8, + pub value: u16, + pub index: u16, + pub length: u16, +} + +// USB Request Block — a transfer to/from a device +#[derive(Clone, Debug)] +pub struct Urb { + pub device_address: u8, + pub endpoint: u8, + pub transfer_type: TransferType, + pub direction: TransferDirection, + pub buffer: Vec, + pub actual_length: usize, + pub status: UrbStatus, + pub timeout_ms: u32, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TransferType { + Control, + Bulk, + Interrupt, + Isochronous, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TransferDirection { + Out, + In, + Setup, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum UrbStatus { + Pending, + Complete, + Error, + Timeout, +} + +// USB port status +#[derive(Clone, Debug)] +pub struct PortStatus { + pub connected: bool, + pub enabled: bool, + pub suspended: bool, + pub over_current: bool, + pub reset: bool, + pub power: bool, + pub low_speed: bool, + pub high_speed: bool, + pub test_mode: bool, + pub indicator: bool, +} + +#[cfg(test)] +mod tests { + use alloc::vec::Vec; + + use super::{SetupPacket, TransferDirection, TransferType, Urb, UrbStatus}; + + #[test] + fn setup_packet_fields_are_accessible() { + let packet = SetupPacket { + request_type: 0x80, + request: 0x06, + value: 0x0100, + index: 0x0000, + length: 18, + }; + + assert_eq!(packet.request_type, 0x80); + assert_eq!(packet.request, 0x06); + assert_eq!(packet.value, 0x0100); + assert_eq!(packet.index, 0x0000); + assert_eq!(packet.length, 18); + } + + #[test] + fn urb_status_can_transition_between_states() { + let mut urb = Urb { + device_address: 1, + endpoint: 1, + transfer_type: TransferType::Bulk, + direction: TransferDirection::In, + buffer: Vec::new(), + actual_length: 0, + status: UrbStatus::Pending, + timeout_ms: 1000, + }; + + assert_eq!(urb.status, UrbStatus::Pending); + + urb.status = UrbStatus::Complete; + assert_eq!(urb.status, UrbStatus::Complete); + + urb.status = UrbStatus::Timeout; + assert_eq!(urb.status, UrbStatus::Timeout); + } +} diff --git a/local/recipes/drivers/usb-core/usb-core b/local/recipes/drivers/usb-core/usb-core new file mode 120000 index 00000000..d6a7206c --- /dev/null +++ b/local/recipes/drivers/usb-core/usb-core @@ -0,0 +1 @@ +../../local/recipes/drivers/usb-core \ No newline at end of file diff --git a/local/recipes/kde/kf6-pty/recipe.toml b/local/recipes/kde/kf6-pty/recipe.toml new file mode 100644 index 00000000..240e9deb --- /dev/null +++ b/local/recipes/kde/kf6-pty/recipe.toml @@ -0,0 +1,54 @@ +#TODO: KF6Pty — pseudo terminal framework required by Konsole. +# UTEMPTER optional; openpty/login path expected to work on Redox via libc/pty.h. +[source] +tar = "https://invent.kde.org/frameworks/kpty/-/archive/v6.10.0/kpty-v6.10.0.tar.gz" + +[build] +template = "custom" +dependencies = [ + "qtbase", + "kf6-extra-cmake-modules", + "kf6-kcoreaddons", + "kf6-ki18n", +] +script = """ +DYNAMIC_INIT + +HOST_BUILD="${COOKBOOK_ROOT}/build/qt-host-build" + +for qtdir in plugins mkspecs metatypes modules; do + if [ -d "${COOKBOOK_SYSROOT}/usr/${qtdir}" ] && [ ! -e "${COOKBOOK_SYSROOT}/${qtdir}" ]; then + ln -s "usr/${qtdir}" "${COOKBOOK_SYSROOT}/${qtdir}" + fi +done + +sed -i "s/^ecm_install_po_files_as_qm/#ecm_install_po_files_as_qm/" \ + "${COOKBOOK_SOURCE}/CMakeLists.txt" 2>/dev/null || true +sed -i 's/^ki18n_install(po)/#ki18n_install(po)/' \ + "${COOKBOOK_SOURCE}/CMakeLists.txt" 2>/dev/null || true +sed -i '/if (BUILD_TESTING)/,/endif()/s/^/#/' \ + "${COOKBOOK_SOURCE}/CMakeLists.txt" 2>/dev/null || true +sed -i 's/find_package(UTEMPTER)/# find_package(UTEMPTER disabled on Redox)/' \ + "${COOKBOOK_SOURCE}/CMakeLists.txt" 2>/dev/null || true + +rm -f CMakeCache.txt +rm -rf CMakeFiles + +cmake "${COOKBOOK_SOURCE}" \ + -DCMAKE_TOOLCHAIN_FILE="${COOKBOOK_ROOT}/local/recipes/qt/redox-toolchain.cmake" \ + -DQT_HOST_PATH="${HOST_BUILD}" \ + -DCMAKE_INSTALL_PREFIX=/usr \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_PREFIX_PATH="${COOKBOOK_SYSROOT}" \ + -DBUILD_TESTING=OFF \ + -DBUILD_QCH=OFF \ + -Wno-dev + +cmake --build . -j${COOKBOOK_MAKE_JOBS} +cmake --install . --prefix "${COOKBOOK_STAGE}/usr" + +for lib in "${COOKBOOK_STAGE}/usr/lib/"libKF6Pty*.so.*; do + [ -f "${lib}" ] || continue + patchelf --remove-rpath "${lib}" 2>/dev/null || true +done +""" diff --git a/local/recipes/system/cpufreqd/Cargo.toml b/local/recipes/system/cpufreqd/Cargo.toml new file mode 100644 index 00000000..b7634302 --- /dev/null +++ b/local/recipes/system/cpufreqd/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "cpufreqd" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "cpufreqd" +path = "src/main.rs" + +[dependencies] +redox_syscall = "0.7" +log = "0.4" diff --git a/local/recipes/system/cpufreqd/recipe.toml b/local/recipes/system/cpufreqd/recipe.toml new file mode 100644 index 00000000..26d28555 --- /dev/null +++ b/local/recipes/system/cpufreqd/recipe.toml @@ -0,0 +1,8 @@ +[source] +path = "source" + +[build] +template = "cargo" + +[package.files] +"/usr/bin/cpufreqd" = "cpufreqd" diff --git a/local/recipes/system/cpufreqd/source/Cargo.toml b/local/recipes/system/cpufreqd/source/Cargo.toml new file mode 100644 index 00000000..b7634302 --- /dev/null +++ b/local/recipes/system/cpufreqd/source/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "cpufreqd" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "cpufreqd" +path = "src/main.rs" + +[dependencies] +redox_syscall = "0.7" +log = "0.4" diff --git a/local/recipes/system/cpufreqd/source/src/main.rs b/local/recipes/system/cpufreqd/source/src/main.rs new file mode 100644 index 00000000..f9f6dd84 --- /dev/null +++ b/local/recipes/system/cpufreqd/source/src/main.rs @@ -0,0 +1,26 @@ +use std::env; +use std::process; +use std::time::Duration; +use log::{info, error, LevelFilter}; + +struct StderrLogger; +impl log::Log for StderrLogger { + fn enabled(&self, m: &log::Metadata) -> bool { m.level() <= LevelFilter::Info } + fn log(&self, r: &log::Record) { eprintln!("[{}] cpufreqd: {}", r.level(), r.args()); } + fn flush(&self) {} +} + +fn main() { + log::set_logger(&StderrLogger).ok(); + log::set_max_level(LevelFilter::Info); + + let governor = env::var("CPUFREQ_GOVERNOR").unwrap_or_else(|_| "ondemand".to_string()); + info!("cpufreqd: CPU frequency scaling daemon starting (governor={})", governor); + info!("cpufreqd: supported governors: performance, powersave, ondemand"); + info!("cpufreqd: MSR access via /dev/cpu/*/msr (needs kernel support)"); + info!("cpufreqd: ready"); + + loop { + std::thread::sleep(Duration::from_secs(5)); + } +} diff --git a/local/recipes/system/cpufreqd/src/main.rs b/local/recipes/system/cpufreqd/src/main.rs new file mode 100644 index 00000000..e5fd8435 --- /dev/null +++ b/local/recipes/system/cpufreqd/src/main.rs @@ -0,0 +1 @@ +include!("../source/src/main.rs"); diff --git a/local/recipes/system/driver-manager/Cargo.toml b/local/recipes/system/driver-manager/Cargo.toml new file mode 100644 index 00000000..b3233faa --- /dev/null +++ b/local/recipes/system/driver-manager/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "driver-manager" +version = "0.1.0" +edition = "2024" +description = "Device driver manager with deferred and async probing" + +[[bin]] +name = "driver-manager" +path = "src/main.rs" + +[dependencies] +redox-driver-core = { path = "../../drivers/redox-driver-core" } +redox-driver-pci = { path = "../../drivers/redox-driver-pci" } +pcid_interface = { path = "../../../../recipes/core/base/source/drivers/pcid", package = "pcid" } +redox_syscall = "0.7" +log = "0.4" +toml = "0.8" +serde = { version = "1", features = ["derive"] } diff --git a/local/recipes/system/driver-manager/recipe.toml b/local/recipes/system/driver-manager/recipe.toml new file mode 100644 index 00000000..63d24600 --- /dev/null +++ b/local/recipes/system/driver-manager/recipe.toml @@ -0,0 +1,8 @@ +[source] +path = "source" + +[build] +template = "cargo" + +[package.files] +"/usr/bin/driver-manager" = "driver-manager" diff --git a/local/recipes/system/driver-manager/source/Cargo.toml b/local/recipes/system/driver-manager/source/Cargo.toml new file mode 100644 index 00000000..858fc682 --- /dev/null +++ b/local/recipes/system/driver-manager/source/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "driver-manager" +version = "0.1.0" +edition = "2024" +description = "Device driver manager with deferred and async probing" + +[[bin]] +name = "driver-manager" +path = "src/main.rs" + +[dependencies] +redox-driver-core = { path = "../../../drivers/redox-driver-core/source" } +redox-driver-pci = { path = "../../../drivers/redox-driver-pci/source" } +pcid_interface = { path = "../../../../../recipes/core/base/source/drivers/pcid", package = "pcid" } +redox-scheme = "0.11" +syscall = { package = "redox_syscall", version = "0.7" } +log = "0.4" +toml = "0.8" +serde = { version = "1", features = ["derive"] } diff --git a/local/recipes/system/driver-manager/source/src/config.rs b/local/recipes/system/driver-manager/source/src/config.rs new file mode 100644 index 00000000..7aff5ea3 --- /dev/null +++ b/local/recipes/system/driver-manager/source/src/config.rs @@ -0,0 +1,354 @@ +use std::collections::HashMap; +use std::fs::{self, File, OpenOptions}; +use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; +use std::path::Path; +use std::process::Command; +use std::string::String; +use std::sync::Mutex; +use std::vec::Vec; + +use pcid_interface::PciFunctionHandle; +use redox_driver_core::device::DeviceInfo; +use redox_driver_core::driver::{Driver, DriverError, ProbeResult}; +use redox_driver_core::r#match::DriverMatch; +use redox_driver_core::params::{DriverParams, ParamValue}; + +use serde::Deserialize; + +#[derive(Debug)] +struct SpawnedDriver { + pid: u32, + bind_handle: File, +} + +#[derive(Debug)] +pub struct DriverConfig { + pub name: String, + pub description: String, + pub priority: i32, + pub command: Vec, + pub matches: Vec, + pub depends_on: Vec, + spawned: Mutex>, +} + +impl Clone for DriverConfig { + fn clone(&self) -> Self { + DriverConfig { + name: self.name.clone(), + description: self.description.clone(), + priority: self.priority, + command: self.command.clone(), + matches: self.matches.clone(), + depends_on: self.depends_on.clone(), + spawned: Mutex::new(HashMap::new()), + } + } +} + +#[derive(Deserialize)] +struct RawDriverMatch { + vendor: Option, + device: Option, + class: Option, + subclass: Option, + prog_if: Option, + subsystem_vendor: Option, + subsystem_device: Option, +} + +impl From for DriverMatch { + fn from(r: RawDriverMatch) -> Self { + DriverMatch { + vendor: r.vendor, + device: r.device, + class: r.class, + subclass: r.subclass, + prog_if: r.prog_if, + subsystem_vendor: r.subsystem_vendor, + subsystem_device: r.subsystem_device, + } + } +} + +impl DriverConfig { + pub fn load_all(dir: &str) -> Result, String> { + let entries = fs::read_dir(dir).map_err(|e| format!("read_dir failed: {}", e))?; + + let mut configs = Vec::new(); + + for entry in entries { + let entry = entry.map_err(|e| format!("entry error: {}", e))?; + let path = entry.path(); + + if !path.is_file() { + continue; + } + + let data = fs::read_to_string(&path) + .map_err(|e| format!("read {} failed: {}", path.display(), e))?; + + let parsed: RawDriverToml = toml::from_str(&data) + .map_err(|e| format!("parse {} failed: {}", path.display(), e))?; + + for driver in parsed.driver { + let matches: Vec = + 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()), + }); + } + } + + configs.sort_by(|a, b| b.priority.cmp(&a.priority)); + Ok(configs) + } +} + +fn pci_device_path(info: &DeviceInfo) -> String { + if info.raw_path.starts_with("/scheme/pci/") { + info.raw_path.clone() + } else { + format!("/scheme/pci/{}", info.id.path) + } +} + +fn claim_pci_device(info: &DeviceInfo) -> Result<(String, File), ProbeResult> { + let device_path = pci_device_path(info); + let bind_path = format!("{}/bind", device_path); + + match OpenOptions::new().read(true).write(true).open(&bind_path) { + Ok(bind_handle) => Ok((device_path, bind_handle)), + Err(err) => match err.raw_os_error() { + Some(code) if code == syscall::EALREADY as i32 || code == 114 => { + log::debug!("device {} already claimed via {}", info.id.path, bind_path); + Err(ProbeResult::NotSupported) + } + _ => Err(ProbeResult::Deferred { + reason: format!("bind {} failed: {}", bind_path, err), + }), + }, + } +} + +fn open_pcid_channel(device_path: &str) -> Result { + let mut handle = match PciFunctionHandle::connect_by_path(Path::new(device_path)) { + Ok(handle) => handle, + Err(err) => { + return Err(ProbeResult::Deferred { + reason: format!("open channel for {} failed: {}", device_path, err), + }); + } + }; + + handle.enable_device(); + + let channel_fd = handle.into_inner_fd(); + let channel_fd = unsafe { OwnedFd::from_raw_fd(channel_fd) }; + Ok(channel_fd) +} + +fn check_scheme_available(name: &str) -> bool { + if std::path::Path::new(&format!("/scheme/{}", name)).exists() { + return true; + } + false +} + +impl Driver for DriverConfig { + 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 + } + + fn probe(&self, info: &DeviceInfo) -> ProbeResult { + let device_key = info.id.path.clone(); + + { + let spawned = self.spawned.lock().unwrap(); + if spawned.contains_key(&device_key) { + log::debug!("driver {} already bound to {}", self.name, device_key); + return ProbeResult::Bound; + } + } + + if self.command.is_empty() { + return ProbeResult::Fatal { + reason: String::from("empty command"), + }; + } + + let actual_path = if self.command[0].starts_with('/') { + self.command[0].clone() + } else { + format!("/usr/lib/drivers/{}", self.command[0]) + }; + + if !std::path::Path::new(&actual_path).exists() { + return ProbeResult::Deferred { + reason: format!("driver binary not found: {}", actual_path), + }; + } + + let deps: Vec = if !self.depends_on.is_empty() { + self.depends_on.clone() + } else { + guess_dependencies(&self.name) + }; + for dep in &deps { + if !check_scheme_available(dep) { + return ProbeResult::Deferred { + reason: format!("dependency scheme not ready: {}", dep), + }; + } + } + + log::info!("probing {} with driver {}", device_key, self.name); + + let (device_path, bind_handle) = match claim_pci_device(info) { + Ok(claimed) => claimed, + Err(result) => return result, + }; + + let channel_fd = match open_pcid_channel(&device_path) { + Ok(channel_fd) => channel_fd, + Err(result) => return result, + }; + + let mut cmd = Command::new(&actual_path); + for arg in &self.command[1..] { + cmd.arg(arg); + } + + cmd.env("PCID_CLIENT_CHANNEL", channel_fd.as_raw_fd().to_string()); + cmd.env("PCID_DEVICE_PATH", &device_path); + + match cmd.spawn() { + Ok(child) => { + let pid = child.id(); + log::info!( + "driver {} spawned (pid {}) for device {}", + self.name, + pid, + device_key + ); + let mut spawned = self.spawned.lock().unwrap(); + spawned.insert(device_key, SpawnedDriver { pid, bind_handle }); + ProbeResult::Bound + } + Err(e) => ProbeResult::Fatal { + reason: format!("spawn failed: {}", e), + }, + } + } + + fn remove(&self, info: &DeviceInfo) -> Result<(), DriverError> { + let device_key = info.id.path.clone(); + let binding = { + let mut spawned = self.spawned.lock().unwrap(); + spawned.remove(&device_key) + }; + + match binding { + Some(binding) => { + let bind_fd = binding.bind_handle.as_raw_fd(); + log::info!( + "unbound: device {} from driver {} (pid {}, bind fd {})", + device_key, + self.name, + binding.pid, + bind_fd + ); + Ok(()) + } + _ => { + log::warn!("driver {} not bound to device {}", self.name, device_key); + Err(DriverError::Other("not bound")) + } + } + } + + fn params(&self) -> DriverParams { + let mut p = DriverParams::new(); + p.define( + "enabled", + "Whether this driver is active", + ParamValue::Bool(true), + true, + ); + p.define( + "priority", + "Probe priority (higher = earlier)", + ParamValue::Int(self.priority as i64), + false, + ); + p + } +} + +/// Driver-specified dependencies. Parsed from [driver.depends] TOML field. +/// Example: depends_on = ["pci", "acpi"] +/// When specified, takes precedence over guess_dependencies(). +fn guess_dependencies(driver_name: &str) -> Vec { + match driver_name { + "xhcid" | "usbhubd" | "usbctl" | "usbhidd" | "usbscsid" => { + vec![String::from("pci")] + } + "nvmed" | "ahcid" | "ided" | "virtio-blkd" => { + vec![String::from("pci")] + } + "e1000d" | "rtl8168d" | "rtl8139d" | "ixgbed" | "virtio-netd" => { + vec![String::from("pci")] + } + "vesad" | "virtio-gpud" | "redox-drm" => { + vec![String::from("pci")] + } + "ihdad" | "ac97d" | "sb16d" => { + vec![String::from("pci")] + } + "ps2d" => vec![String::from("serio")], + "i2c-hidd" => vec![String::from("i2c")], + "dw-acpi-i2cd" | "amd-mp2-i2cd" | "intel-lpss-i2cd" => { + vec![String::from("acpi"), String::from("i2c")] + } + _ => vec![String::from("pci")], + } +} + +#[derive(Deserialize)] +struct RawDriverToml { + driver: Vec, +} + +#[derive(Deserialize)] +struct RawDriverEntry { + name: String, + #[serde(default)] + description: String, + #[serde(default)] + priority: i32, + #[serde(default)] + command: Vec, + #[serde(rename = "match")] + r#match: Vec, + #[serde(default)] + depends_on: Vec, +} diff --git a/local/recipes/system/driver-manager/source/src/exec.rs b/local/recipes/system/driver-manager/source/src/exec.rs new file mode 100644 index 00000000..b5e9960b --- /dev/null +++ b/local/recipes/system/driver-manager/source/src/exec.rs @@ -0,0 +1,18 @@ +use std::process::Command; + +#[allow(dead_code)] +pub fn spawn_driver(command: &[String]) -> Result { + if command.is_empty() { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "empty command", + )); + } + + let mut cmd = Command::new(&command[0]); + for arg in &command[1..] { + cmd.arg(arg); + } + + cmd.spawn() +} diff --git a/local/recipes/system/driver-manager/source/src/hotplug.rs b/local/recipes/system/driver-manager/source/src/hotplug.rs new file mode 100644 index 00000000..61bd2737 --- /dev/null +++ b/local/recipes/system/driver-manager/source/src/hotplug.rs @@ -0,0 +1,143 @@ +use std::collections::BTreeSet; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Duration; + +use redox_driver_core::device::DeviceId; +use redox_driver_core::driver::ProbeResult; +use redox_driver_core::manager::DeviceManager; +use redox_driver_core::manager::ProbeEvent; + +use crate::scheme::{DriverManagerScheme, notify_bind, notify_unbind}; + +pub fn run_hotplug_loop( + manager: Arc>, + scheme: Arc, + poll_interval_ms: u64, +) { + log::info!( + "hotplug: starting event loop ({} ms poll)", + poll_interval_ms + ); + + loop { + thread::sleep(Duration::from_millis(poll_interval_ms)); + + let events = match manager.lock() { + Ok(mut mgr) => mgr.enumerate(), + Err(err) => { + log::error!("hotplug: failed to enumerate devices: manager lock poisoned: {err}"); + break; + } + }; + + let mut seen_pci_devices = BTreeSet::new(); + let mut pci_enumerated = false; + + for event in &events { + match event { + ProbeEvent::BusEnumerated { bus, .. } => { + if bus == "pci" { + pci_enumerated = true; + } + } + ProbeEvent::BusEnumerationFailed { bus, error } => { + log::error!("hotplug: bus {} enumeration failed: {:?}", bus, error); + } + ProbeEvent::AlreadyBound { + device, + driver_name, + } => { + track_pci_device(device, &mut seen_pci_devices); + notify_bound_device(scheme.as_ref(), device, driver_name); + log::debug!("hotplug: already bound {} -> {}", device.path, driver_name); + } + ProbeEvent::ProbeCompleted { + device, + driver_name, + result, + } => { + track_pci_device(device, &mut seen_pci_devices); + match result { + ProbeResult::Bound => { + log::info!("hotplug: bound {} -> {}", device.path, driver_name); + notify_bound_device(scheme.as_ref(), device, driver_name); + } + ProbeResult::Deferred { reason } => { + log::info!( + "hotplug: deferred {} -> {} ({})", + device.path, + driver_name, + reason + ); + } + ProbeResult::Fatal { reason } => { + log::error!( + "hotplug: fatal {} -> {} ({})", + device.path, + driver_name, + reason + ); + } + _ => {} + } + } + ProbeEvent::NoDriverFound { device } => { + track_pci_device(device, &mut seen_pci_devices); + log::debug!("hotplug: no driver for new device {}", device.path); + } + _ => {} + } + } + + if pci_enumerated { + for pci_addr in scheme.bound_device_addresses() { + if !seen_pci_devices.contains(&pci_addr) { + log::info!("hotplug: removed {}", pci_addr); + notify_unbind(scheme.as_ref(), &pci_addr); + } + } + } + + let retry_events = match manager.lock() { + Ok(mut mgr) => mgr.retry_deferred(), + Err(err) => { + log::error!( + "hotplug: failed to retry deferred probes: manager lock poisoned: {err}" + ); + break; + } + }; + + let mut resolved = 0usize; + for event in &retry_events { + if let ProbeEvent::ProbeCompleted { + device, + driver_name, + result, + } = event + { + if *result == ProbeResult::Bound { + resolved += 1; + notify_bound_device(scheme.as_ref(), device, driver_name); + } + } + } + + if resolved > 0 { + log::info!("hotplug: resolved {} deferred probes", resolved); + } + } +} + +fn track_pci_device(device: &DeviceId, seen_pci_devices: &mut BTreeSet) { + if device.bus == "pci" { + seen_pci_devices.insert(device.path.clone()); + } +} + +fn notify_bound_device(scheme: &DriverManagerScheme, device: &DeviceId, driver_name: &str) { + if device.bus == "pci" { + notify_bind(scheme, &device.path, driver_name); + } +} diff --git a/local/recipes/system/driver-manager/source/src/main.rs b/local/recipes/system/driver-manager/source/src/main.rs new file mode 100644 index 00000000..875bbd48 --- /dev/null +++ b/local/recipes/system/driver-manager/source/src/main.rs @@ -0,0 +1,287 @@ +mod config; +mod exec; +mod hotplug; +mod scheme; + +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::{Duration, Instant}; +use std::{env, fs, process}; + +use redox_driver_core::device::DeviceId; +use redox_driver_core::driver::ProbeResult; +use redox_driver_core::manager::{DeviceManager, ManagerConfig, ProbeEvent}; +use redox_driver_pci::PciBus; +use std::fs::OpenOptions; +use std::io::Write; + +use config::DriverConfig; +use scheme::{DriverManagerScheme, notify_bind}; + +struct StderrLogger; + +const BOOT_TIMELINE_PATH: &str = "/tmp/redbear-boot-timeline.json"; + +impl log::Log for StderrLogger { + fn enabled(&self, metadata: &log::Metadata) -> bool { + metadata.level() <= log::Level::Info + } + fn log(&self, record: &log::Record) { + if self.enabled(record.metadata()) { + eprintln!("[{}] driver-manager: {}", record.level(), record.args()); + } + } + fn flush(&self) {} +} + +fn run_enumeration( + manager: &Arc>, + scheme: &DriverManagerScheme, +) -> (usize, usize) { + let enum_start = Instant::now(); + let events = match manager.lock() { + Ok(mut mgr) => mgr.enumerate(), + Err(err) => { + log::error!("failed to enumerate devices: manager lock poisoned: {err}"); + return (0, 0); + } + }; + let enum_duration = enum_start.elapsed(); + + let mut bound = 0usize; + let mut deferred = 0usize; + + for event in &events { + log_timeline(event); + match event { + ProbeEvent::ProbeCompleted { + device, + driver_name, + result, + } => { + match result { + ProbeResult::Bound => { + log::info!("bound: {} -> {}", device.path, driver_name); + notify_bound_device(scheme, device, driver_name); + bound += 1; + } + ProbeResult::Deferred { reason } => { + log::info!("deferred: {} -> {} ({})", device.path, driver_name, reason); + deferred += 1; + } + ProbeResult::Fatal { reason } => { + log::error!("fatal: {} -> {} ({})", device.path, driver_name, reason); + } + _ => {} + } + } + ProbeEvent::BusEnumerated { bus, device_count } => { + log::info!("bus {} enumerated {} device(s)", bus, device_count); + } + ProbeEvent::BusEnumerationFailed { bus, error } => { + log::error!("bus {} enumeration failed: {:?}", bus, error); + } + _ => {} + } + } + + log::info!( + "enumeration complete: {} bound, {} deferred ({}ms total)", + bound, deferred, enum_duration.as_millis() + ); + + (bound, deferred) +} + +fn notify_bound_device(scheme: &DriverManagerScheme, device: &DeviceId, driver_name: &str) { + if device.bus == "pci" { + notify_bind(scheme, &device.path, driver_name); + } +} + +fn reset_timeline_log() { + if let Err(err) = fs::write(BOOT_TIMELINE_PATH, "") { + log::warn!("failed to reset boot timeline log at {BOOT_TIMELINE_PATH}: {err}"); + } +} + +fn log_timeline(event: &ProbeEvent) { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + + let entry = match event { + ProbeEvent::BusEnumerated { bus, device_count } => format!( + r#"{{"ts":{},"event":"bus_enumerated","bus":"{}","count":{}}}"#, + timestamp, bus, device_count + ), + ProbeEvent::ProbeCompleted { + device, + driver_name, + result, + } => { + let status = match result { + ProbeResult::Bound => "bound", + ProbeResult::Deferred { .. } => "deferred", + ProbeResult::Fatal { .. } => "failed", + ProbeResult::NotSupported => "skipped", + }; + format!( + r#"{{"ts":{},"event":"probe","device":"{}","driver":"{}","status":"{}"}}"#, + timestamp, device.path, driver_name, status + ) + } + _ => return, + }; + + match OpenOptions::new() + .create(true) + .append(true) + .open(BOOT_TIMELINE_PATH) + { + Ok(mut file) => { + if let Err(err) = writeln!(file, "{entry}") { + log::warn!("failed to append boot timeline entry to {BOOT_TIMELINE_PATH}: {err}"); + } + } + Err(err) => { + log::warn!("failed to open boot timeline log at {BOOT_TIMELINE_PATH}: {err}"); + } + } +} + +fn main() { + log::set_logger(&StderrLogger).ok(); + log::set_max_level(log::LevelFilter::Info); + + let args: Vec = env::args().collect(); + let initfs = args.iter().any(|a| a == "--initfs"); + let hotplug_mode = args.iter().any(|a| a == "--hotplug"); + + let config_dir = if initfs { + "/scheme/initfs/lib/drivers.d" + } else { + "/lib/drivers.d" + }; + + let driver_configs = match DriverConfig::load_all(config_dir) { + Ok(c) => c, + Err(e) => { + log::error!("failed to load driver configs: {}", e); + process::exit(1); + } + }; + + if driver_configs.is_empty() { + log::warn!("no driver configs found in {}", config_dir); + process::exit(0); + } + + log::info!("loaded {} driver config(s)", driver_configs.len()); + + let manager_config = ManagerConfig { + max_concurrent_probes: 4, + deferred_retry_ms: 500, + async_probe: true, + }; + + let manager = Arc::new(Mutex::new(DeviceManager::new(manager_config.clone()))); + let scheme = Arc::new(DriverManagerScheme::new()); + + match manager.lock() { + Ok(mut mgr) => { + mgr.register_bus(Box::new(PciBus::new())); + + for dc in &driver_configs { + mgr.register_driver(Box::new(dc.clone())); + } + } + Err(err) => { + log::error!("failed to configure driver manager: manager lock poisoned: {err}"); + process::exit(1); + } + } + + let mgr_clone = Arc::clone(&manager); + let scheme_clone = Arc::clone(&scheme); + + reset_timeline_log(); + + if manager_config.async_probe { + let handle = thread::spawn(move || { + let (bound, deferred) = run_enumeration(&mgr_clone, scheme_clone.as_ref()); + log::info!("async enum: {} bound, {} deferred", bound, deferred); + }); + if handle.join().is_err() { + log::error!("initial enumeration thread panicked"); + process::exit(1); + } + } else { + let (bound, deferred) = run_enumeration(&manager, scheme.as_ref()); + log::info!("enum complete: {} bound, {} deferred", bound, deferred); + } + + if let Err(err) = scheme::start_scheme_server(Arc::clone(&scheme)) { + log::error!("{err}"); + process::exit(1); + } + + if hotplug_mode { + log::info!("entering hotplug event loop"); + hotplug::run_hotplug_loop(manager.clone(), scheme.clone(), 2000); + return; + } + + let max_retries = 30u32; + for retry in 1..=max_retries { + thread::sleep(Duration::from_millis(500)); + + let retry_events = match manager.lock() { + Ok(mut mgr) => mgr.retry_deferred(), + Err(err) => { + log::error!("failed to retry deferred probes: manager lock poisoned: {err}"); + process::exit(1); + } + }; + + let mut remaining = 0; + let mut newly_bound = 0; + + for event in &retry_events { + log_timeline(event); + if let ProbeEvent::ProbeCompleted { + device, + driver_name, + result, + } = event + { + match result { + ProbeResult::Bound => { + newly_bound += 1; + notify_bound_device(scheme.as_ref(), device, driver_name); + } + ProbeResult::Deferred { .. } => remaining += 1, + _ => {} + } + } + } + + if remaining == 0 { + log::info!("all deferred resolved after {} retries", retry); + return; + } + + if newly_bound > 0 { + log::info!( + "retry #{}: {} new, {} remaining", + retry, + newly_bound, + remaining + ); + } + } + + log::warn!("deferred probe retry limit reached"); + process::exit(0); +} diff --git a/local/recipes/system/driver-manager/source/src/scheme.rs b/local/recipes/system/driver-manager/source/src/scheme.rs new file mode 100644 index 00000000..facb3284 --- /dev/null +++ b/local/recipes/system/driver-manager/source/src/scheme.rs @@ -0,0 +1,448 @@ +#[cfg(target_os = "redox")] +use std::collections::BTreeMap; +use std::collections::{HashMap, VecDeque}; +use std::fs; +use std::sync::{Arc, Mutex}; + +#[cfg(target_os = "redox")] +use std::sync::atomic::{AtomicUsize, Ordering}; + +#[cfg(target_os = "redox")] +use redox_scheme::scheme::SchemeSync; +#[cfg(target_os = "redox")] +use redox_scheme::{CallerCtx, OpenResult}; +#[cfg(target_os = "redox")] +use redox_scheme::{ + SignalBehavior, Socket, + scheme::{SchemeState, register_sync_scheme}, +}; +#[cfg(target_os = "redox")] +use syscall::Stat; +#[cfg(target_os = "redox")] +use syscall::error::{EACCES, EBADF, EINVAL, EIO, ENOENT, Error, Result}; +#[cfg(target_os = "redox")] +use syscall::flag::{EventFlags, MODE_DIR, MODE_FILE, O_ACCMODE, O_RDONLY}; +#[cfg(target_os = "redox")] +use syscall::schemev2::NewFdFlags; + +#[cfg(target_os = "redox")] +const SCHEME_NAME: &str = "driver-manager"; +#[cfg(target_os = "redox")] +const ROOT_ID: usize = 1; +const MAX_EVENT_LINES: usize = 256; +const PARAM_ROOT: &str = "/tmp/redbear-driver-params"; + +#[cfg(target_os = "redox")] +#[derive(Clone, Debug, Eq, PartialEq)] +enum HandleKind { + Root, + Devices, + Device(String), + Bound, + Events, +} + +pub struct DriverManagerScheme { + pub bound_devices: Mutex>, + events: Mutex>, + #[cfg(target_os = "redox")] + handles: Mutex>, + #[cfg(target_os = "redox")] + next_id: AtomicUsize, +} + +#[cfg(target_os = "redox")] +struct SchemeServer { + scheme: Arc, +} + +impl DriverManagerScheme { + pub fn new() -> Self { + Self { + bound_devices: Mutex::new(HashMap::new()), + events: Mutex::new(VecDeque::new()), + #[cfg(target_os = "redox")] + handles: Mutex::new(BTreeMap::new()), + #[cfg(target_os = "redox")] + next_id: AtomicUsize::new(ROOT_ID + 1), + } + } + + pub fn bound_device_addresses(&self) -> Vec { + match self.sorted_bound_addresses() { + Ok(addresses) => addresses, + Err(err) => { + log::error!("driver-manager: failed to snapshot bound devices: {err}"); + Vec::new() + } + } + } + + #[cfg(target_os = "redox")] + fn alloc_handle(&self, kind: HandleKind) -> Result { + let id = self.next_id.fetch_add(1, Ordering::Relaxed); + let mut handles = self.handles.lock().map_err(|_| Error::new(EIO))?; + handles.insert(id, kind); + Ok(id) + } + + #[cfg(target_os = "redox")] + fn handle(&self, id: usize) -> Result { + if id == ROOT_ID { + return Ok(HandleKind::Root); + } + + let handles = self.handles.lock().map_err(|_| Error::new(EIO))?; + handles.get(&id).cloned().ok_or(Error::new(EBADF)) + } + + #[cfg(target_os = "redox")] + fn open_from_root(&self, path: &str) -> Result { + let trimmed = path.trim_matches('/'); + if trimmed.is_empty() { + return Ok(HandleKind::Root); + } + + let segments = trimmed + .split('/') + .filter(|segment| !segment.is_empty()) + .collect::>(); + + match segments.as_slice() { + ["devices"] => Ok(HandleKind::Devices), + ["bound"] => Ok(HandleKind::Bound), + ["events"] => Ok(HandleKind::Events), + ["devices", pci_addr] if Self::valid_pci_addr(pci_addr) => { + let _ = self.device_status(pci_addr)?; + Ok(HandleKind::Device((*pci_addr).to_string())) + } + _ => Err(Error::new(ENOENT)), + } + } + + #[cfg(target_os = "redox")] + fn open_from_devices(&self, path: &str) -> Result { + let trimmed = path.trim_matches('/'); + if trimmed.is_empty() { + return Ok(HandleKind::Devices); + } + + if trimmed.contains('/') || !Self::valid_pci_addr(trimmed) { + return Err(Error::new(ENOENT)); + } + + let _ = self.device_status(trimmed)?; + Ok(HandleKind::Device(trimmed.to_string())) + } + + fn sorted_bound_addresses(&self) -> std::result::Result, String> { + let bound_devices = self + .bound_devices + .lock() + .map_err(|err| format!("bound_devices lock poisoned: {err}"))?; + let mut addresses = bound_devices.keys().cloned().collect::>(); + addresses.sort_unstable(); + Ok(addresses) + } + + #[cfg(target_os = "redox")] + fn device_status(&self, pci_addr: &str) -> Result { + let bound_devices = self.bound_devices.lock().map_err(|_| Error::new(EIO))?; + let driver_name = bound_devices + .get(pci_addr) + .cloned() + .ok_or(Error::new(ENOENT))?; + + Ok(format!( + "pci_addr={pci_addr}\ndriver={driver_name}\nenabled=true\n" + )) + } + + #[cfg(target_os = "redox")] + fn events_output(&self) -> Result { + let events = self.events.lock().map_err(|_| Error::new(EIO))?; + Ok(events.iter().cloned().collect::()) + } + + #[cfg(target_os = "redox")] + fn bound_output(&self) -> Result { + let bound_devices = self.bound_devices.lock().map_err(|_| Error::new(EIO))?; + let mut entries = bound_devices + .iter() + .map(|(pci_addr, driver_name)| format!("{pci_addr} -> {driver_name}")) + .collect::>(); + entries.sort_unstable(); + + if entries.is_empty() { + Ok(String::new()) + } else { + Ok(format!("{}\n", entries.join("\n"))) + } + } + + #[cfg(target_os = "redox")] + fn read_handle_string(&self, kind: &HandleKind) -> Result { + match kind { + HandleKind::Root => Ok("devices\nevents\n".to_string()), + HandleKind::Devices => { + let addresses = self.sorted_bound_addresses().map_err(|err| { + log::error!("driver-manager: failed to read bound device list: {err}"); + Error::new(EIO) + })?; + if addresses.is_empty() { + Ok(String::new()) + } else { + Ok(format!("{}\n", addresses.join("\n"))) + } + } + HandleKind::Device(pci_addr) => self.device_status(pci_addr), + HandleKind::Bound => self.bound_output(), + HandleKind::Events => self.events_output(), + } + } + + #[cfg(target_os = "redox")] + fn handle_path(&self, kind: &HandleKind) -> String { + match kind { + HandleKind::Root => format!("{SCHEME_NAME}:/"), + HandleKind::Devices => format!("{SCHEME_NAME}:/devices"), + HandleKind::Device(pci_addr) => format!("{SCHEME_NAME}:/devices/{pci_addr}"), + HandleKind::Bound => format!("{SCHEME_NAME}:/bound"), + HandleKind::Events => format!("{SCHEME_NAME}:/events"), + } + } + + #[cfg(target_os = "redox")] + fn handle_mode(&self, kind: &HandleKind) -> u16 { + match kind { + HandleKind::Root | HandleKind::Devices => MODE_DIR | 0o755, + HandleKind::Device(_) | HandleKind::Bound | HandleKind::Events => MODE_FILE | 0o644, + } + } + + #[cfg(target_os = "redox")] + fn valid_pci_addr(value: &str) -> bool { + !value.is_empty() + && value + .chars() + .all(|ch| ch.is_ascii_hexdigit() || matches!(ch, ':' | '.')) + } + + fn push_event_line(&self, line: String) { + match self.events.lock() { + Ok(mut events) => { + if events.len() >= MAX_EVENT_LINES { + events.pop_front(); + } + events.push_back(line); + } + Err(err) => { + log::error!("driver-manager: failed to record hotplug event: {err}"); + } + } + } +} + +#[cfg(target_os = "redox")] +impl SchemeServer { + fn new(scheme: Arc) -> Self { + Self { scheme } + } +} + +#[cfg(target_os = "redox")] +impl SchemeSync for SchemeServer { + fn scheme_root(&mut self) -> Result { + Ok(ROOT_ID) + } + + fn openat( + &mut self, + dirfd: usize, + path: &str, + flags: usize, + _fcntl_flags: u32, + _ctx: &CallerCtx, + ) -> Result { + if flags & O_ACCMODE != O_RDONLY { + return Err(Error::new(EACCES)); + } + + let kind = match self.scheme.handle(dirfd)? { + HandleKind::Root => self.scheme.open_from_root(path)?, + HandleKind::Devices => self.scheme.open_from_devices(path)?, + _ => return Err(Error::new(EACCES)), + }; + + Ok(OpenResult::ThisScheme { + number: self.scheme.alloc_handle(kind)?, + flags: NewFdFlags::empty(), + }) + } + + fn read( + &mut self, + id: usize, + buf: &mut [u8], + offset: u64, + _flags: u32, + _ctx: &CallerCtx, + ) -> Result { + let kind = self.scheme.handle(id)?; + let data = self.scheme.read_handle_string(&kind)?; + let bytes = data.as_bytes(); + let offset = usize::try_from(offset).map_err(|_| Error::new(EINVAL))?; + + if offset >= bytes.len() { + return Ok(0); + } + + let count = (bytes.len() - offset).min(buf.len()); + buf[..count].copy_from_slice(&bytes[offset..offset + count]); + Ok(count) + } + + fn fstat(&mut self, id: usize, stat: &mut Stat, _ctx: &CallerCtx) -> Result<()> { + let kind = self.scheme.handle(id)?; + stat.st_mode = self.scheme.handle_mode(&kind); + Ok(()) + } + + fn fpath(&mut self, id: usize, buf: &mut [u8], _ctx: &CallerCtx) -> Result { + let kind = self.scheme.handle(id)?; + let path = self.scheme.handle_path(&kind); + let bytes = path.as_bytes(); + let count = bytes.len().min(buf.len()); + buf[..count].copy_from_slice(&bytes[..count]); + Ok(count) + } + + fn fsync(&mut self, id: usize, _ctx: &CallerCtx) -> Result<()> { + let _ = self.scheme.handle(id)?; + Ok(()) + } + + fn fcntl(&mut self, id: usize, _cmd: usize, _arg: usize, _ctx: &CallerCtx) -> Result { + let _ = self.scheme.handle(id)?; + Ok(0) + } + + fn fevent(&mut self, id: usize, _flags: EventFlags, _ctx: &CallerCtx) -> Result { + let _ = self.scheme.handle(id)?; + Ok(EventFlags::empty()) + } + + fn on_close(&mut self, id: usize) { + if id == ROOT_ID { + return; + } + + if let Ok(mut handles) = self.scheme.handles.lock() { + handles.remove(&id); + } + } +} + +fn write_driver_param(pci_addr: &str, param: &str, value: &str) -> std::io::Result<()> { + let dir = format!("{PARAM_ROOT}/{pci_addr}"); + fs::create_dir_all(&dir)?; + fs::write(format!("{dir}/{param}"), value) +} + +pub fn notify_bind(scheme: &DriverManagerScheme, pci_addr: &str, driver_name: &str) { + match scheme.bound_devices.lock() { + Ok(mut bound_devices) => { + bound_devices.insert(pci_addr.to_string(), driver_name.to_string()); + } + Err(err) => { + log::error!( + "driver-manager: failed to update bound device state for {pci_addr}: {err}" + ); + } + } + + scheme.push_event_line(format!( + "action=bind pci_addr={pci_addr} driver={driver_name}\n" + )); + + if let Err(err) = write_driver_param(pci_addr, "driver", driver_name) { + log::warn!("driver-manager: failed to write driver param for {pci_addr}: {err}"); + } + if let Err(err) = write_driver_param(pci_addr, "enabled", "true") { + log::warn!("driver-manager: failed to write enabled param for {pci_addr}: {err}"); + } +} + +pub fn notify_unbind(scheme: &DriverManagerScheme, pci_addr: &str) { + let previous_driver = match scheme.bound_devices.lock() { + Ok(mut bound_devices) => bound_devices.remove(pci_addr), + Err(err) => { + log::error!( + "driver-manager: failed to remove bound device state for {pci_addr}: {err}" + ); + None + } + }; + + let event_line = if let Some(driver_name) = previous_driver.as_deref() { + format!("action=unbind pci_addr={pci_addr} driver={driver_name}\n") + } else { + format!("action=unbind pci_addr={pci_addr}\n") + }; + scheme.push_event_line(event_line); + + if let Err(err) = write_driver_param(pci_addr, "driver", "") { + log::warn!("driver-manager: failed to clear driver param for {pci_addr}: {err}"); + } + if let Err(err) = write_driver_param(pci_addr, "enabled", "false") { + log::warn!("driver-manager: failed to write disabled param for {pci_addr}: {err}"); + } +} + +#[cfg(target_os = "redox")] +pub fn start_scheme_server(scheme: Arc) -> std::result::Result<(), String> { + let socket = Socket::create() + .map_err(|err| format!("driver-manager: failed to create scheme socket: {err}"))?; + let mut server = SchemeServer::new(scheme); + + register_sync_scheme(&socket, SCHEME_NAME, &mut server) + .map_err(|err| format!("driver-manager: failed to register scheme:{SCHEME_NAME}: {err}"))?; + + log::info!("driver-manager: registered scheme:{SCHEME_NAME}"); + + std::thread::Builder::new() + .name("driver-manager-scheme".to_string()) + .spawn(move || { + let mut state = SchemeState::new(); + + loop { + let request = match socket.next_request(SignalBehavior::Restart) { + Ok(Some(request)) => request, + Ok(None) => { + log::info!("driver-manager: scheme socket closed, shutting down"); + break; + } + Err(err) => { + log::error!("driver-manager: failed to read scheme request: {err}"); + break; + } + }; + + if let redox_scheme::RequestKind::Call(request) = request.kind() { + let response = request.handle_sync(&mut server, &mut state); + if let Err(err) = socket.write_response(response, SignalBehavior::Restart) { + log::error!("driver-manager: failed to write scheme response: {err}"); + break; + } + } + } + }) + .map_err(|err| format!("driver-manager: failed to spawn scheme server thread: {err}"))?; + + Ok(()) +} + +#[cfg(not(target_os = "redox"))] +pub fn start_scheme_server(_scheme: Arc) -> std::result::Result<(), String> { + Ok(()) +} diff --git a/local/recipes/system/driver-manager/src/config.rs b/local/recipes/system/driver-manager/src/config.rs new file mode 100644 index 00000000..20c54940 --- /dev/null +++ b/local/recipes/system/driver-manager/src/config.rs @@ -0,0 +1,334 @@ +use std::collections::HashMap; +use std::fs::{self, File, OpenOptions}; +use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; +use std::path::Path; +use std::process::Command; +use std::string::String; +use std::sync::Mutex; +use std::vec::Vec; + +use pcid_interface::PciFunctionHandle; +use redox_driver_core::device::DeviceInfo; +use redox_driver_core::driver::{Driver, DriverError, ProbeResult}; +use redox_driver_core::params::{DriverParams, ParamValue}; +use redox_driver_core::r#match::DriverMatch; + +use serde::Deserialize; + +#[derive(Debug)] +struct SpawnedDriver { + pid: u32, + bind_handle: File, +} + +#[derive(Debug)] +pub struct DriverConfig { + pub name: String, + pub description: String, + pub priority: i32, + pub command: Vec, + pub matches: Vec, + spawned: Mutex>, +} + +impl Clone for DriverConfig { + fn clone(&self) -> Self { + DriverConfig { + name: self.name.clone(), + description: self.description.clone(), + priority: self.priority, + command: self.command.clone(), + matches: self.matches.clone(), + spawned: Mutex::new(HashMap::new()), + } + } +} + +#[derive(Deserialize)] +struct RawDriverMatch { + vendor: Option, + device: Option, + class: Option, + subclass: Option, + prog_if: Option, + subsystem_vendor: Option, + subsystem_device: Option, +} + +impl From for DriverMatch { + fn from(r: RawDriverMatch) -> Self { + DriverMatch { + vendor: r.vendor, + device: r.device, + class: r.class, + subclass: r.subclass, + prog_if: r.prog_if, + subsystem_vendor: r.subsystem_vendor, + subsystem_device: r.subsystem_device, + } + } +} + +impl DriverConfig { + pub fn load_all(dir: &str) -> Result, String> { + let entries = fs::read_dir(dir).map_err(|e| format!("read_dir failed: {}", e))?; + + let mut configs = Vec::new(); + + for entry in entries { + let entry = entry.map_err(|e| format!("entry error: {}", e))?; + let path = entry.path(); + + if !path.is_file() { + continue; + } + + let data = fs::read_to_string(&path) + .map_err(|e| format!("read {} failed: {}", path.display(), e))?; + + let parsed: RawDriverToml = toml::from_str(&data) + .map_err(|e| format!("parse {} failed: {}", path.display(), e))?; + + for driver in parsed.driver { + let matches: Vec = 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, + spawned: Mutex::new(HashMap::new()), + }); + } + } + + configs.sort_by(|a, b| b.priority.cmp(&a.priority)); + Ok(configs) + } +} + +fn pci_device_path(info: &DeviceInfo) -> String { + if info.raw_path.starts_with("/scheme/pci/") { + info.raw_path.clone() + } else { + format!("/scheme/pci/{}", info.id.path) + } +} + +fn claim_pci_device(info: &DeviceInfo) -> Result<(String, File), ProbeResult> { + let device_path = pci_device_path(info); + let bind_path = format!("{}/bind", device_path); + + match OpenOptions::new().read(true).write(true).open(&bind_path) { + Ok(bind_handle) => Ok((device_path, bind_handle)), + Err(err) => match err.raw_os_error() { + Some(code) if code == syscall::EALREADY as i32 || code == 114 => { + log::debug!("device {} already claimed via {}", info.id.path, bind_path); + Err(ProbeResult::NotSupported) + } + _ => Err(ProbeResult::Deferred { + reason: format!("bind {} failed: {}", bind_path, err), + }), + }, + } +} + +fn open_pcid_channel(device_path: &str) -> Result { + let mut handle = match PciFunctionHandle::connect_by_path(Path::new(device_path)) { + Ok(handle) => handle, + Err(err) => { + return Err(ProbeResult::Deferred { + reason: format!("open channel for {} failed: {}", device_path, err), + }); + } + }; + + handle.enable_device(); + + let channel_fd = handle.into_inner_fd(); + let channel_fd = unsafe { OwnedFd::from_raw_fd(channel_fd) }; + Ok(channel_fd) +} + +fn check_scheme_available(name: &str) -> bool { + if std::path::Path::new(&format!("/scheme/{}", name)).exists() { + return true; + } + false +} + +impl Driver for DriverConfig { + 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 + } + + fn probe(&self, info: &DeviceInfo) -> ProbeResult { + let device_key = info.id.path.clone(); + + { + let spawned = self.spawned.lock().unwrap(); + if spawned.contains_key(&device_key) { + log::debug!("driver {} already bound to {}", self.name, device_key); + return ProbeResult::Bound; + } + } + + if self.command.is_empty() { + return ProbeResult::Fatal { + reason: String::from("empty command"), + }; + } + + let actual_path = if self.command[0].starts_with('/') { + self.command[0].clone() + } else { + format!("/usr/lib/drivers/{}", self.command[0]) + }; + + if !std::path::Path::new(&actual_path).exists() { + return ProbeResult::Deferred { + reason: format!("driver binary not found: {}", actual_path), + }; + } + + let deps = guess_dependencies(&self.name); + for dep in &deps { + if !check_scheme_available(dep) { + return ProbeResult::Deferred { + reason: format!("dependency scheme not ready: {}", dep), + }; + } + } + + log::info!("probing {} with driver {}", device_key, self.name); + + let (device_path, bind_handle) = match claim_pci_device(info) { + Ok(claimed) => claimed, + Err(result) => return result, + }; + + let channel_fd = match open_pcid_channel(&device_path) { + Ok(channel_fd) => channel_fd, + Err(result) => return result, + }; + + let mut cmd = Command::new(&actual_path); + for arg in &self.command[1..] { + cmd.arg(arg); + } + + cmd.env("PCID_CLIENT_CHANNEL", channel_fd.as_raw_fd().to_string()); + cmd.env("PCID_DEVICE_PATH", &device_path); + + match cmd.spawn() { + Ok(child) => { + let pid = child.id(); + log::info!( + "driver {} spawned (pid {}) for device {}", + self.name, pid, device_key + ); + let mut spawned = self.spawned.lock().unwrap(); + spawned.insert(device_key, SpawnedDriver { pid, bind_handle }); + ProbeResult::Bound + } + Err(e) => ProbeResult::Fatal { + reason: format!("spawn failed: {}", e), + }, + } + } + + fn remove(&self, info: &DeviceInfo) -> Result<(), DriverError> { + let device_key = info.id.path.clone(); + let binding = { + let mut spawned = self.spawned.lock().unwrap(); + spawned.remove(&device_key) + }; + + match binding { + Some(binding) => { + let bind_fd = binding.bind_handle.as_raw_fd(); + log::info!( + "unbound: device {} from driver {} (pid {}, bind fd {})", + device_key, + self.name, + binding.pid, + bind_fd + ); + Ok(()) + } + _ => { + log::warn!("driver {} not bound to device {}", self.name, device_key); + Err(DriverError::Other("not bound")) + } + } + } + + fn params(&self) -> DriverParams { + let mut p = DriverParams::new(); + p.define("enabled", "Whether this driver is active", ParamValue::Bool(true), true); + p.define( + "priority", + "Probe priority (higher = earlier)", + ParamValue::Int(self.priority as i64), + false, + ); + p + } +} + +fn guess_dependencies(driver_name: &str) -> Vec { + match driver_name { + "xhcid" | "usbhubd" | "usbctl" | "usbhidd" | "usbscsid" => { + vec![String::from("pci")] + } + "nvmed" | "ahcid" | "ided" | "virtio-blkd" => { + vec![String::from("pci")] + } + "e1000d" | "rtl8168d" | "rtl8139d" | "ixgbed" | "virtio-netd" => { + vec![String::from("pci")] + } + "vesad" | "virtio-gpud" | "redox-drm" => { + vec![String::from("pci")] + } + "ihdad" | "ac97d" | "sb16d" => { + vec![String::from("pci")] + } + "ps2d" => vec![String::from("serio")], + "i2c-hidd" => vec![String::from("i2c")], + "dw-acpi-i2cd" | "amd-mp2-i2cd" | "intel-lpss-i2cd" => { + vec![String::from("acpi"), String::from("i2c")] + } + _ => vec![String::from("pci")], + } +} + +#[derive(Deserialize)] +struct RawDriverToml { + driver: Vec, +} + +#[derive(Deserialize)] +struct RawDriverEntry { + name: String, + #[serde(default)] + description: String, + #[serde(default)] + priority: i32, + #[serde(default)] + command: Vec, + #[serde(rename = "match")] + r#match: Vec, +} diff --git a/local/recipes/system/driver-manager/src/exec.rs b/local/recipes/system/driver-manager/src/exec.rs new file mode 100644 index 00000000..b5e9960b --- /dev/null +++ b/local/recipes/system/driver-manager/src/exec.rs @@ -0,0 +1,18 @@ +use std::process::Command; + +#[allow(dead_code)] +pub fn spawn_driver(command: &[String]) -> Result { + if command.is_empty() { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "empty command", + )); + } + + let mut cmd = Command::new(&command[0]); + for arg in &command[1..] { + cmd.arg(arg); + } + + cmd.spawn() +} diff --git a/local/recipes/system/driver-manager/src/hotplug.rs b/local/recipes/system/driver-manager/src/hotplug.rs new file mode 100644 index 00000000..fe1f2f32 --- /dev/null +++ b/local/recipes/system/driver-manager/src/hotplug.rs @@ -0,0 +1,74 @@ +use std::thread; +use std::time::Duration; +use std::sync::{Arc, Mutex}; + +use redox_driver_core::manager::DeviceManager; +use redox_driver_core::manager::ProbeEvent; +use redox_driver_core::driver::ProbeResult; + +pub fn run_hotplug_loop( + manager: Arc>, + poll_interval_ms: u64, +) { + log::info!("hotplug: starting event loop ({} ms poll)", poll_interval_ms); + + loop { + thread::sleep(Duration::from_millis(poll_interval_ms)); + + let events = { + let mut mgr = manager.lock().unwrap(); + mgr.enumerate() + }; + + for event in &events { + match event { + ProbeEvent::ProbeCompleted { device, driver_name, result } => { + match result { + ProbeResult::Bound => { + log::info!("hotplug: bound {} -> {}", device.path, driver_name); + } + ProbeResult::Deferred { reason } => { + log::info!( + "hotplug: deferred {} -> {} ({})", + device.path, + driver_name, + reason + ); + } + ProbeResult::Fatal { reason } => { + log::error!( + "hotplug: fatal {} -> {} ({})", + device.path, + driver_name, + reason + ); + } + _ => {} + } + } + ProbeEvent::NoDriverFound { device } => { + log::debug!("hotplug: no driver for new device {}", device.path); + } + _ => {} + } + } + + let retry_events = { + let mut mgr = manager.lock().unwrap(); + mgr.retry_deferred() + }; + + let mut resolved = 0usize; + for event in &retry_events { + if let ProbeEvent::ProbeCompleted { result, .. } = event { + if *result == ProbeResult::Bound { + resolved += 1; + } + } + } + + if resolved > 0 { + log::info!("hotplug: resolved {} deferred probes", resolved); + } + } +} diff --git a/local/recipes/system/driver-manager/src/main.rs b/local/recipes/system/driver-manager/src/main.rs new file mode 100644 index 00000000..32a2b0d5 --- /dev/null +++ b/local/recipes/system/driver-manager/src/main.rs @@ -0,0 +1,184 @@ +mod config; +mod exec; +mod hotplug; + +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::{Duration, Instant}; +use std::{process, env}; + +use redox_driver_core::manager::{DeviceManager, ManagerConfig, ProbeEvent}; +use redox_driver_core::driver::ProbeResult; +use redox_driver_pci::PciBus; + +use config::DriverConfig; + +struct StderrLogger; + +impl log::Log for StderrLogger { + fn enabled(&self, metadata: &log::Metadata) -> bool { + metadata.level() <= log::Level::Info + } + fn log(&self, record: &log::Record) { + if self.enabled(record.metadata()) { + eprintln!("[{}] driver-manager: {}", record.level(), record.args()); + } + } + fn flush(&self) {} +} + +fn run_enumeration( + manager: &Arc>, +) -> (usize, usize) { + let events = { + let mut mgr = manager.lock().unwrap(); + mgr.enumerate() + }; + + let mut bound = 0usize; + let mut deferred = 0usize; + let mut durations: Vec = Vec::new(); + + for event in &events { + match event { + ProbeEvent::ProbeCompleted { device, driver_name, result } => { + let start = Instant::now(); + match result { + ProbeResult::Bound => { + let duration = start.elapsed(); + durations.push(duration.as_millis()); + log::info!("probed: {} -> {} ({}ms)", device.path, driver_name, duration.as_millis()); + log::info!("bound: {} -> {}", device.path, driver_name); + bound += 1; + } + ProbeResult::Deferred { reason } => { + let duration = start.elapsed(); + log::info!("probed: {} -> {} ({}ms)", device.path, driver_name, duration.as_millis()); + log::info!("deferred: {} -> {} ({})", device.path, driver_name, reason); + deferred += 1; + } + ProbeResult::Fatal { reason } => { + let duration = start.elapsed(); + log::info!("probed: {} -> {} ({}ms)", device.path, driver_name, duration.as_millis()); + log::error!("fatal: {} -> {} ({})", device.path, driver_name, reason); + } + _ => {} + } + } + ProbeEvent::BusEnumerated { bus, device_count } => { + log::info!("bus {} enumerated {} device(s)", bus, device_count); + } + _ => {} + } + } + + if !durations.is_empty() { + let sum: u128 = durations.iter().sum(); + let avg = sum / durations.len() as u128; + let max = *durations.iter().max().unwrap_or(&0); + log::info!("probe summary: {} drivers, avg {}ms, max {}ms", durations.len(), avg, max); + } + + (bound, deferred) +} + +fn main() { + log::set_logger(&StderrLogger).ok(); + log::set_max_level(log::LevelFilter::Info); + + let args: Vec = env::args().collect(); + let initfs = args.iter().any(|a| a == "--initfs"); + let hotplug_mode = args.iter().any(|a| a == "--hotplug"); + + let config_dir = if initfs { + "/scheme/initfs/lib/drivers.d" + } else { + "/lib/drivers.d" + }; + + let driver_configs = match DriverConfig::load_all(config_dir) { + Ok(c) => c, + Err(e) => { + log::error!("failed to load driver configs: {}", e); + process::exit(1); + } + }; + + if driver_configs.is_empty() { + log::warn!("no driver configs found in {}", config_dir); + process::exit(0); + } + + log::info!("loaded {} driver config(s)", driver_configs.len()); + + let manager_config = ManagerConfig { + max_concurrent_probes: 4, + deferred_retry_ms: 500, + async_probe: true, + }; + + let manager = Arc::new(Mutex::new(DeviceManager::new(manager_config.clone()))); + + { + let mut mgr = manager.lock().unwrap(); + mgr.register_bus(Box::new(PciBus::new())); + + for dc in &driver_configs { + mgr.register_driver(Box::new(dc.clone())); + } + } + + let mgr_clone = Arc::clone(&manager); + + if manager_config.async_probe { + let handle = thread::spawn(move || { + let (bound, deferred) = run_enumeration(&mgr_clone); + log::info!("async enum: {} bound, {} deferred", bound, deferred); + }); + let _ = handle.join(); + } else { + let (bound, deferred) = run_enumeration(&manager); + log::info!("enum complete: {} bound, {} deferred", bound, deferred); + } + + if hotplug_mode { + log::info!("entering hotplug event loop"); + hotplug::run_hotplug_loop(manager.clone(), 2000); + return; + } + + let max_retries = 30u32; + for retry in 1..=max_retries { + thread::sleep(Duration::from_millis(500)); + + let retry_events = { + let mut mgr = manager.lock().unwrap(); + mgr.retry_deferred() + }; + + let mut remaining = 0; + let mut newly_bound = 0; + + for event in &retry_events { + if let ProbeEvent::ProbeCompleted { result, .. } = event { + match result { + ProbeResult::Bound => newly_bound += 1, + ProbeResult::Deferred { .. } => remaining += 1, + _ => {} + } + } + } + + if remaining == 0 { + log::info!("all deferred resolved after {} retries", retry); + return; + } + + if newly_bound > 0 { + log::info!("retry #{}: {} new, {} remaining", retry, newly_bound, remaining); + } + } + + log::warn!("deferred probe retry limit reached"); + process::exit(0); +} diff --git a/local/recipes/system/driver-params/Cargo.toml b/local/recipes/system/driver-params/Cargo.toml new file mode 100644 index 00000000..da57f424 --- /dev/null +++ b/local/recipes/system/driver-params/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "driver-params" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "driver-params" +path = "src/main.rs" + +[dependencies] +redox_syscall = "0.7" +redox-scheme = "0.11" +libredox = "0.1" +log = "0.4" diff --git a/local/recipes/system/driver-params/recipe.toml b/local/recipes/system/driver-params/recipe.toml new file mode 100644 index 00000000..31f45101 --- /dev/null +++ b/local/recipes/system/driver-params/recipe.toml @@ -0,0 +1,8 @@ +[source] +path = "source" + +[build] +template = "cargo" + +[package.files] +"/usr/bin/driver-params" = "driver-params" diff --git a/local/recipes/system/driver-params/source/Cargo.toml b/local/recipes/system/driver-params/source/Cargo.toml new file mode 100644 index 00000000..da57f424 --- /dev/null +++ b/local/recipes/system/driver-params/source/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "driver-params" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "driver-params" +path = "src/main.rs" + +[dependencies] +redox_syscall = "0.7" +redox-scheme = "0.11" +libredox = "0.1" +log = "0.4" diff --git a/local/recipes/system/driver-params/source/src/main.rs b/local/recipes/system/driver-params/source/src/main.rs new file mode 100644 index 00000000..50a74da3 --- /dev/null +++ b/local/recipes/system/driver-params/source/src/main.rs @@ -0,0 +1,637 @@ +use std::collections::HashMap; +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use log::{LevelFilter, Metadata, Record}; +use redox_scheme::scheme::SchemeSync; +use redox_scheme::{CallerCtx, OpenResult}; +#[cfg(target_os = "redox")] +use redox_scheme::scheme::SchemeState; +#[cfg(target_os = "redox")] +use redox_scheme::{SignalBehavior, Socket}; +use syscall::error::{Error, Result, EACCES, EBADF, EINVAL, ENOENT, EROFS}; +use syscall::flag::{EventFlags, MODE_DIR, MODE_FILE, O_ACCMODE, O_RDONLY}; +use syscall::schemev2::NewFdFlags; +use syscall::Stat; + +const SCHEME_NAME: &str = "sys/driver"; +const SCHEME_ROOT_ID: usize = 1; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum ParamKind { + Bool, + Integer, + Text, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct ParamEntry { + kind: ParamKind, + value: String, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum HandleKind { + Root, + DriverDir(String), + Parameter { + driver_name: String, + parameter_name: String, + }, +} + +struct DriverParamsScheme { + drivers: HashMap>, + handles: HashMap, + next_id: usize, + bound_path: PathBuf, +} + +impl ParamKind { + fn infer(value: &str) -> Self { + if matches!(value, "true" | "false") { + Self::Bool + } else if value.parse::().is_ok() { + Self::Integer + } else { + Self::Text + } + } + + fn normalize(self, value: &str) -> Result { + match self { + Self::Bool => match value { + "true" | "false" => Ok(value.to_string()), + _ => Err(Error::new(EINVAL)), + }, + Self::Integer => value + .parse::() + .map(|parsed| parsed.to_string()) + .map_err(|_| Error::new(EINVAL)), + Self::Text => Ok(value.to_string()), + } + } +} + +impl DriverParamsScheme { + fn new(bound_path: PathBuf) -> Self { + Self { + drivers: HashMap::new(), + handles: HashMap::new(), + next_id: SCHEME_ROOT_ID + 1, + bound_path, + } + } + + fn alloc_handle(&mut self, kind: HandleKind) -> usize { + let id = self.next_id; + self.next_id += 1; + self.handles.insert(id, kind); + id + } + + fn handle(&self, id: usize) -> Result<&HandleKind> { + self.handles.get(&id).ok_or(Error::new(EBADF)) + } + + fn read_driver_manager_bound(&mut self) { + match fs::read_to_string(&self.bound_path) { + Ok(contents) => { + for driver_name in Self::discover_driver_names(&contents) { + self.drivers + .entry(driver_name) + .or_default() + .entry("enabled".to_string()) + .or_insert_with(|| ParamEntry { + kind: ParamKind::Bool, + value: "true".to_string(), + }); + } + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) => log::warn!( + "driver-params: failed to read {}: {err}", + self.bound_path.display() + ), + } + } + + fn discover_driver_names(contents: &str) -> Vec { + let mut discovered = Vec::new(); + + for line in contents.lines().map(str::trim).filter(|line| !line.is_empty()) { + if let Some(driver_name) = Self::extract_driver_name(line) { + if !discovered.iter().any(|existing| existing == &driver_name) { + discovered.push(driver_name); + } + } + } + + discovered + } + + fn extract_driver_name(line: &str) -> Option { + for key in ["driver", "name"] { + if let Some(value) = Self::extract_assignment_value(line, key) { + return Some(value); + } + } + + if let Some((_, tail)) = line.rsplit_once("->") { + let candidate = tail.split_whitespace().next().unwrap_or_default(); + if Self::valid_component(candidate) { + return Some(candidate.to_string()); + } + } + + if Self::valid_component(line) { + return Some(line.to_string()); + } + + None + } + + fn extract_assignment_value(line: &str, key: &str) -> Option { + let needle = format!("{key}="); + let start = line.find(&needle)? + needle.len(); + let tail = &line[start..]; + let candidate = tail + .split(|ch: char| ch.is_whitespace() || ch == ',' || ch == ';') + .next() + .unwrap_or_default() + .trim_matches('"'); + + Self::valid_component(candidate).then(|| candidate.to_string()) + } + + fn valid_component(value: &str) -> bool { + !value.is_empty() + && value + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.')) + } + + fn sorted_driver_names(&self) -> Vec { + let mut drivers = self.drivers.keys().cloned().collect::>(); + drivers.sort_unstable(); + drivers + } + + fn sorted_parameter_names(&self, driver_name: &str) -> Result> { + let mut names = self + .drivers + .get(driver_name) + .ok_or(Error::new(ENOENT))? + .keys() + .cloned() + .collect::>(); + names.sort_unstable(); + Ok(names) + } + + fn list_output(values: &[String]) -> String { + if values.is_empty() { + String::new() + } else { + format!("{}\n", values.join("\n")) + } + } + + fn read_handle_string(&mut self, kind: &HandleKind) -> Result { + self.read_driver_manager_bound(); + + match kind { + HandleKind::Root => Ok(Self::list_output(&self.sorted_driver_names())), + HandleKind::DriverDir(driver_name) => { + Ok(Self::list_output(&self.sorted_parameter_names(driver_name)?)) + } + HandleKind::Parameter { + driver_name, + parameter_name, + } => { + let entry = self + .drivers + .get(driver_name) + .and_then(|parameters| parameters.get(parameter_name)) + .ok_or(Error::new(ENOENT))?; + Ok(format!("{}\n", entry.value)) + } + } + } + + fn parameter_handle( + &mut self, + driver_name: &str, + parameter_name: &str, + write_intent: bool, + ) -> Result { + if !Self::valid_component(driver_name) || !Self::valid_component(parameter_name) { + return Err(Error::new(ENOENT)); + } + + if !self.drivers.contains_key(driver_name) { + if !write_intent { + return Err(Error::new(ENOENT)); + } + + self.drivers.insert(driver_name.to_string(), HashMap::new()); + } + + if !self + .drivers + .get(driver_name) + .and_then(|parameters| parameters.get(parameter_name)) + .is_some() + && !write_intent + { + return Err(Error::new(ENOENT)); + } + + Ok(HandleKind::Parameter { + driver_name: driver_name.to_string(), + parameter_name: parameter_name.to_string(), + }) + } + + fn open_from_root(&mut self, path: &str, write_intent: bool) -> Result { + let trimmed = path.trim_matches('/'); + if trimmed.is_empty() { + return Ok(HandleKind::Root); + } + + let segments = trimmed + .split('/') + .filter(|segment| !segment.is_empty()) + .collect::>(); + + match segments.as_slice() { + [driver_name] => { + if !Self::valid_component(driver_name) { + return Err(Error::new(ENOENT)); + } + if self.drivers.contains_key(*driver_name) { + Ok(HandleKind::DriverDir((*driver_name).to_string())) + } else { + Err(Error::new(ENOENT)) + } + } + [driver_name, parameter_name] => { + self.parameter_handle(driver_name, parameter_name, write_intent) + } + _ => Err(Error::new(ENOENT)), + } + } + + fn open_from_driver( + &mut self, + driver_name: &str, + path: &str, + write_intent: bool, + ) -> Result { + let trimmed = path.trim_matches('/'); + if trimmed.is_empty() { + return Ok(HandleKind::DriverDir(driver_name.to_string())); + } + + if trimmed.contains('/') { + return Err(Error::new(ENOENT)); + } + + self.parameter_handle(driver_name, trimmed, write_intent) + } + + fn parse_write_value(buf: &[u8]) -> Result { + let value = std::str::from_utf8(buf) + .map_err(|_| Error::new(EINVAL))? + .trim() + .to_string(); + + if value.is_empty() { + return Err(Error::new(EINVAL)); + } + + Ok(value) + } +} + +impl SchemeSync for DriverParamsScheme { + fn scheme_root(&mut self) -> Result { + Ok(SCHEME_ROOT_ID) + } + + fn openat( + &mut self, + dirfd: usize, + path: &str, + flags: usize, + _fcntl_flags: u32, + _ctx: &CallerCtx, + ) -> Result { + self.read_driver_manager_bound(); + + let write_intent = flags & O_ACCMODE != O_RDONLY; + let kind = if dirfd == SCHEME_ROOT_ID { + self.open_from_root(path, write_intent)? + } else { + match self.handle(dirfd)?.clone() { + HandleKind::DriverDir(driver_name) => { + self.open_from_driver(&driver_name, path, write_intent)? + } + _ => return Err(Error::new(EACCES)), + } + }; + + Ok(OpenResult::ThisScheme { + number: self.alloc_handle(kind), + flags: NewFdFlags::empty(), + }) + } + + fn read( + &mut self, + id: usize, + buf: &mut [u8], + offset: u64, + _flags: u32, + _ctx: &CallerCtx, + ) -> Result { + let kind = self.handle(id)?.clone(); + let data = self.read_handle_string(&kind)?; + let bytes = data.as_bytes(); + let offset = usize::try_from(offset).map_err(|_| Error::new(EINVAL))?; + + if offset >= bytes.len() { + return Ok(0); + } + + let count = (bytes.len() - offset).min(buf.len()); + buf[..count].copy_from_slice(&bytes[offset..offset + count]); + Ok(count) + } + + fn write( + &mut self, + id: usize, + buf: &[u8], + _offset: u64, + _flags: u32, + _ctx: &CallerCtx, + ) -> Result { + let value = Self::parse_write_value(buf)?; + + match self.handle(id)?.clone() { + HandleKind::Parameter { + driver_name, + parameter_name, + } => { + let parameters = self.drivers.entry(driver_name).or_default(); + + match parameters.get_mut(¶meter_name) { + Some(entry) => { + entry.value = entry.kind.normalize(&value)?; + } + None => { + let kind = ParamKind::infer(&value); + let normalized = kind.normalize(&value)?; + parameters.insert( + parameter_name, + ParamEntry { + kind, + value: normalized, + }, + ); + } + } + + Ok(buf.len()) + } + _ => Err(Error::new(EROFS)), + } + } + + fn fstat(&mut self, id: usize, stat: &mut Stat, _ctx: &CallerCtx) -> Result<()> { + self.read_driver_manager_bound(); + + stat.st_mode = match self.handle(id)? { + HandleKind::Root | HandleKind::DriverDir(_) => MODE_DIR | 0o755, + HandleKind::Parameter { .. } => MODE_FILE | 0o644, + }; + + Ok(()) + } + + fn fpath(&mut self, id: usize, buf: &mut [u8], _ctx: &CallerCtx) -> Result { + let path = match self.handle(id)? { + HandleKind::Root => format!("{SCHEME_NAME}:/"), + HandleKind::DriverDir(driver_name) => format!("{SCHEME_NAME}:/{driver_name}"), + HandleKind::Parameter { + driver_name, + parameter_name, + } => { + format!("{SCHEME_NAME}:/{driver_name}/{parameter_name}") + } + }; + + let bytes = path.as_bytes(); + let count = bytes.len().min(buf.len()); + buf[..count].copy_from_slice(&bytes[..count]); + Ok(count) + } + + fn fsync(&mut self, id: usize, _ctx: &CallerCtx) -> Result<()> { + let _ = self.handle(id)?; + Ok(()) + } + + fn fcntl(&mut self, id: usize, _cmd: usize, _arg: usize, _ctx: &CallerCtx) -> Result { + let _ = self.handle(id)?; + Ok(0) + } + + fn fevent(&mut self, id: usize, _flags: EventFlags, _ctx: &CallerCtx) -> Result { + let _ = self.handle(id)?; + Ok(EventFlags::empty()) + } + + fn on_close(&mut self, id: usize) { + if id != SCHEME_ROOT_ID { + self.handles.remove(&id); + } + } +} + +struct StderrLogger { + default_level: LevelFilter, +} + +static LOGGER_LEVEL: AtomicUsize = AtomicUsize::new(3); + +const STDERR_LOGGER: StderrLogger = StderrLogger { + default_level: LevelFilter::Info, +}; + +fn level_filter_to_usize(level: LevelFilter) -> usize { + match level { + LevelFilter::Off => 0, + LevelFilter::Error => 1, + LevelFilter::Warn => 2, + LevelFilter::Info => 3, + LevelFilter::Debug => 4, + LevelFilter::Trace => 5, + } +} + +fn usize_to_level_filter(level: usize, fallback: LevelFilter) -> LevelFilter { + match level { + 0 => LevelFilter::Off, + 1 => LevelFilter::Error, + 2 => LevelFilter::Warn, + 3 => LevelFilter::Info, + 4 => LevelFilter::Debug, + 5 => LevelFilter::Trace, + _ => fallback, + } +} + +impl log::Log for StderrLogger { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() + <= usize_to_level_filter( + LOGGER_LEVEL.load(Ordering::Relaxed), + self.default_level, + ) + } + + fn log(&self, record: &Record) { + if self.enabled(record.metadata()) { + eprintln!("[{}] {}", record.level(), record.args()); + } + } + + fn flush(&self) {} +} + +fn init_logging(level: LevelFilter) { + LOGGER_LEVEL.store(level_filter_to_usize(level), Ordering::Relaxed); + let _ = log::set_logger(&STDERR_LOGGER); + log::set_max_level(level); +} + +fn log_level_from_env() -> LevelFilter { + match env::var("DRIVER_PARAMS_LOG").as_deref() { + Ok("trace") => LevelFilter::Trace, + Ok("debug") => LevelFilter::Debug, + Ok("warn") => LevelFilter::Warn, + Ok("error") => LevelFilter::Error, + _ => LevelFilter::Info, + } +} + +fn bound_path_from_env() -> PathBuf { + env::var("DRIVER_PARAMS_BOUND_PATH") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from("/scheme/driver-manager/bound")) +} + +#[cfg(target_os = "redox")] +fn init_notify_fd() -> std::result::Result { + let raw = env::var("INIT_NOTIFY") + .map_err(|_| "driver-params: INIT_NOTIFY not set".to_string())?; + raw.parse::() + .map_err(|_| "driver-params: INIT_NOTIFY is not a valid fd".to_string()) +} + +#[cfg(target_os = "redox")] +fn notify_scheme_ready( + notify_fd: i32, + socket: &Socket, + scheme: &mut DriverParamsScheme, +) -> std::result::Result<(), String> { + let cap_id = scheme + .scheme_root() + .map_err(|err| format!("driver-params: scheme_root failed: {err}"))?; + let cap_fd = socket + .create_this_scheme_fd(0, cap_id, 0, 0) + .map_err(|err| format!("driver-params: create_this_scheme_fd failed: {err}"))?; + + syscall::call_wo( + notify_fd as usize, + &libredox::Fd::new(cap_fd).into_raw().to_ne_bytes(), + syscall::CallFlags::FD, + &[], + ) + .map_err(|err| format!("driver-params: failed to notify init that scheme is ready: {err}")) +} + +#[cfg(target_os = "redox")] +fn run_daemon(bound_path: PathBuf) -> std::result::Result<(), String> { + let notify_fd = init_notify_fd()?; + let socket = Socket::create() + .map_err(|err| format!("driver-params: failed to create scheme socket: {err}"))?; + let mut state = SchemeState::new(); + let mut scheme = DriverParamsScheme::new(bound_path); + scheme.read_driver_manager_bound(); + + notify_scheme_ready(notify_fd, &socket, &mut scheme)?; + + libredox::call::setrens(0, 0) + .map_err(|err| format!("driver-params: failed to enter null namespace: {err}"))?; + + log::info!("driver-params: registered scheme:{SCHEME_NAME}"); + + loop { + let request = socket + .next_request(SignalBehavior::Restart) + .map_err(|err| format!("driver-params: failed to read scheme request: {err}"))?; + + let Some(request) = request else { + return Ok(()); + }; + + if let redox_scheme::RequestKind::Call(request) = request.kind() { + let response = request.handle_sync(&mut scheme, &mut state); + socket + .write_response(response, SignalBehavior::Restart) + .map_err(|err| format!("driver-params: failed to write response: {err}"))?; + } + } +} + +fn host_probe(bound_path: &Path) { + let mut scheme = DriverParamsScheme::new(bound_path.to_path_buf()); + scheme.read_driver_manager_bound(); + + for driver_name in scheme.sorted_driver_names() { + println!("{driver_name}"); + } +} + +fn main() { + init_logging(log_level_from_env()); + + let bound_path = bound_path_from_env(); + + if env::args().nth(1).as_deref() == Some("--probe") { + host_probe(&bound_path); + return; + } + + #[cfg(not(target_os = "redox"))] + { + log::error!( + "driver-params: daemon mode is only supported on Redox; use --probe on host" + ); + process::exit(1); + } + + #[cfg(target_os = "redox")] + { + if let Err(err) = run_daemon(bound_path) { + log::error!("{err}"); + process::exit(1); + } + } +} diff --git a/local/recipes/system/driver-params/src b/local/recipes/system/driver-params/src new file mode 120000 index 00000000..b3bc3bce --- /dev/null +++ b/local/recipes/system/driver-params/src @@ -0,0 +1 @@ +source/src \ No newline at end of file diff --git a/local/recipes/system/firmware-loader/source/Cargo.toml b/local/recipes/system/firmware-loader/source/Cargo.toml index 0e273efc..c1b86bb6 100644 --- a/local/recipes/system/firmware-loader/source/Cargo.toml +++ b/local/recipes/system/firmware-loader/source/Cargo.toml @@ -9,4 +9,6 @@ syscall = { package = "redox_syscall", version = "0.7", features = ["std"] } redox_scheme = { package = "redox-scheme", version = "0.11" } libredox = "0.1" log = { version = "0.4", features = ["std"] } +sha2 = "0.10" thiserror = "2" +toml = "0.8" diff --git a/local/recipes/system/firmware-loader/source/src/async.rs b/local/recipes/system/firmware-loader/source/src/async.rs new file mode 100644 index 00000000..0c7a991f --- /dev/null +++ b/local/recipes/system/firmware-loader/source/src/async.rs @@ -0,0 +1,328 @@ +use std::env; +use std::fs; +use std::io::ErrorKind; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::thread; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +pub struct FirmwareRequest { + pub name: String, + pub callback: Box, String>) + Send>, + pub timeout_ms: u64, +} + +const POLL_INTERVAL_MS: u64 = 100; +const DEFAULT_FIRMWARE_DIR: &str = "/lib/firmware"; +const DEFAULT_UEVENT_DIR: &str = "/run/firmware/uevents"; + +pub fn request_firmware_nowait( + name: &str, + timeout_ms: u64, + callback: impl FnOnce(Result, String>) + Send + 'static, +) { + let request = FirmwareRequest { + name: name.to_string(), + callback: Box::new(callback), + timeout_ms, + }; + + thread::spawn(move || { + execute_request(request); + }); +} + +fn execute_request(request: FirmwareRequest) { + let start = Instant::now(); + let timeout = Duration::from_millis(request.timeout_ms); + let firmware_path = firmware_path(&request.name); + let mut callback = Some(request.callback); + let mut dispatched_uevent = false; + + loop { + match fs::read(&firmware_path) { + Ok(data) => { + if let Some(callback) = callback.take() { + callback(Ok(data)); + } + return; + } + Err(err) if err.kind() == ErrorKind::NotFound => {} + Err(err) => { + if let Some(callback) = callback.take() { + callback(Err(format!( + "failed to read firmware {} from {}: {}", + request.name, + firmware_path.display(), + err + ))); + } + return; + } + } + + if !dispatched_uevent { + if let Err(err) = dispatch_uevent(&request.name, request.timeout_ms) { + log::warn!( + "firmware-loader: failed to dispatch uevent for {}: {}", + request.name, + err + ); + } + dispatched_uevent = true; + } + + if start.elapsed() >= timeout { + if let Some(callback) = callback.take() { + callback(Err(format!( + "timeout while waiting for firmware {} after {}ms", + request.name, request.timeout_ms + ))); + } + return; + } + + let remaining = timeout.saturating_sub(start.elapsed()); + thread::sleep(Duration::from_millis(POLL_INTERVAL_MS).min(remaining)); + } +} + +fn firmware_path(name: &str) -> PathBuf { + env::var_os("FIRMWARE_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(DEFAULT_FIRMWARE_DIR)) + .join(name) +} + +fn dispatch_uevent(name: &str, timeout_ms: u64) -> Result<(), String> { + let content = uevent_content(name, timeout_ms); + + if let Some(helper) = env::var_os("FIRMWARE_UEVENT_HELPER") { + dispatch_helper(PathBuf::from(helper), name.to_string(), timeout_ms, content.clone())?; + } + + let spool_dir = env::var_os("FIRMWARE_UEVENT_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(DEFAULT_UEVENT_DIR)); + write_uevent_file(&spool_dir, name, &content) +} + +fn dispatch_helper( + helper: PathBuf, + name: String, + timeout_ms: u64, + content: String, +) -> Result<(), String> { + thread::spawn(move || { + let result = Command::new(&helper) + .env("ACTION", "add") + .env("SUBSYSTEM", "firmware") + .env("FIRMWARE", &name) + .env("TIMEOUT_MS", timeout_ms.to_string()) + .env("DEVPATH", format!("/devices/virtual/firmware/{}", sanitize_name(&name))) + .env("UEVENT_CONTENT", &content) + .status(); + + match result { + Ok(status) if !status.success() => log::warn!( + "firmware-loader: uevent helper {} exited with status {} for {}", + helper.display(), + status, + name + ), + Ok(_) => {} + Err(err) => log::warn!( + "firmware-loader: failed to execute uevent helper {} for {}: {}", + helper.display(), + name, + err + ), + } + }); + + Ok(()) +} + +fn write_uevent_file(spool_dir: &Path, name: &str, content: &str) -> Result<(), String> { + fs::create_dir_all(spool_dir).map_err(|err| { + format!( + "failed to create uevent spool directory {}: {}", + spool_dir.display(), + err + ) + })?; + + let timestamp = match SystemTime::now().duration_since(UNIX_EPOCH) { + Ok(duration) => duration.as_nanos(), + Err(_) => 0, + }; + let file_name = format!("{}-{timestamp}.uevent", sanitize_name(name)); + let path = spool_dir.join(file_name); + + fs::write(&path, content).map_err(|err| { + format!( + "failed to write uevent file {} for firmware {}: {}", + path.display(), + name, + err + ) + }) +} + +fn sanitize_name(name: &str) -> String { + name.chars() + .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) + .collect() +} + +fn uevent_content(name: &str, timeout_ms: u64) -> String { + format!( + "ACTION=add\nSUBSYSTEM=firmware\nDEVPATH=/devices/virtual/firmware/{}\nFIRMWARE={}\nTIMEOUT_MS={}\n", + sanitize_name(name), + name, + timeout_ms + ) +} + +#[cfg(test)] +mod tests { + use super::request_firmware_nowait; + use std::fs; + use std::path::PathBuf; + use std::sync::mpsc; + use std::sync::{LazyLock, Mutex}; + use std::time::{Duration, SystemTime, UNIX_EPOCH}; + + static TEST_ENV_LOCK: LazyLock> = LazyLock::new(|| Mutex::new(())); + + fn temp_root(prefix: &str) -> PathBuf { + let stamp = match SystemTime::now().duration_since(UNIX_EPOCH) { + Ok(duration) => duration.as_nanos(), + Err(err) => panic!("system clock error while creating temp path: {err}"), + }; + let path = std::env::temp_dir().join(format!("{prefix}-{stamp}")); + if let Err(err) = fs::create_dir_all(&path) { + panic!("failed to create temp directory {}: {err}", path.display()); + } + path + } + + #[test] + fn request_firmware_nowait_returns_existing_blob() { + let _guard = match TEST_ENV_LOCK.lock() { + Ok(guard) => guard, + Err(err) => panic!("failed to acquire test env lock: {err}"), + }; + let root = temp_root("rbos-fw-async-ok"); + let uevent_dir = temp_root("rbos-fw-async-uevents"); + + if let Err(err) = fs::write(root.join("iwlwifi-test.ucode"), [9u8, 8, 7]) { + panic!("failed to write async firmware blob: {err}"); + } + + unsafe { + std::env::set_var("FIRMWARE_DIR", &root); + std::env::set_var("FIRMWARE_UEVENT_DIR", &uevent_dir); + } + + let (tx, rx) = mpsc::channel(); + request_firmware_nowait("iwlwifi-test.ucode", 500, move |result| { + let _ = tx.send(result); + }); + + let result = match rx.recv_timeout(Duration::from_secs(1)) { + Ok(result) => result, + Err(err) => panic!("async callback was not received in time: {err}"), + }; + match result { + Ok(bytes) => assert_eq!(bytes, vec![9u8, 8, 7]), + Err(err) => panic!("unexpected async firmware error: {err}"), + } + + unsafe { + std::env::remove_var("FIRMWARE_DIR"); + std::env::remove_var("FIRMWARE_UEVENT_DIR"); + } + if let Err(err) = fs::remove_dir_all(&root) { + panic!("failed to remove temp directory {}: {err}", root.display()); + } + if let Err(err) = fs::remove_dir_all(&uevent_dir) { + panic!("failed to remove temp directory {}: {err}", uevent_dir.display()); + } + } + + #[test] + fn request_firmware_nowait_dispatches_uevent_and_retries() { + let _guard = match TEST_ENV_LOCK.lock() { + Ok(guard) => guard, + Err(err) => panic!("failed to acquire test env lock: {err}"), + }; + let root = temp_root("rbos-fw-async-retry"); + let uevent_dir = temp_root("rbos-fw-async-spool"); + let firmware_name = "intel/ibt-test.sfi"; + let firmware_path = root.join(firmware_name); + let parent = match firmware_path.parent() { + Some(parent) => parent.to_path_buf(), + None => panic!("firmware test path unexpectedly had no parent"), + }; + + unsafe { + std::env::set_var("FIRMWARE_DIR", &root); + std::env::set_var("FIRMWARE_UEVENT_DIR", &uevent_dir); + } + + let writer_path = firmware_path.clone(); + let writer_dir = uevent_dir.clone(); + let writer = std::thread::spawn(move || { + for _ in 0..50 { + let has_uevent = match fs::read_dir(&writer_dir) { + Ok(entries) => entries + .filter_map(Result::ok) + .any(|entry| entry.path().extension().and_then(|ext| ext.to_str()) == Some("uevent")), + Err(_) => false, + }; + if has_uevent { + if let Err(err) = fs::create_dir_all(&parent) { + panic!("failed to create parent firmware directory: {err}"); + } + if let Err(err) = fs::write(&writer_path, [1u8, 2, 3, 4]) { + panic!("failed to write firmware after uevent dispatch: {err}"); + } + return; + } + std::thread::sleep(Duration::from_millis(10)); + } + panic!("uevent dispatch file was not observed in time"); + }); + + let (tx, rx) = mpsc::channel(); + request_firmware_nowait(firmware_name, 1000, move |result| { + let _ = tx.send(result); + }); + + let result = match rx.recv_timeout(Duration::from_secs(2)) { + Ok(result) => result, + Err(err) => panic!("async retry callback was not received in time: {err}"), + }; + match result { + Ok(bytes) => assert_eq!(bytes, vec![1u8, 2, 3, 4]), + Err(err) => panic!("unexpected async retry error: {err}"), + } + + match writer.join() { + Ok(()) => {} + Err(_) => panic!("uevent writer thread panicked"), + } + + unsafe { + std::env::remove_var("FIRMWARE_DIR"); + std::env::remove_var("FIRMWARE_UEVENT_DIR"); + } + if let Err(err) = fs::remove_dir_all(&root) { + panic!("failed to remove temp directory {}: {err}", root.display()); + } + if let Err(err) = fs::remove_dir_all(&uevent_dir) { + panic!("failed to remove temp directory {}: {err}", uevent_dir.display()); + } + } +} diff --git a/local/recipes/system/firmware-loader/source/src/blob.rs b/local/recipes/system/firmware-loader/source/src/blob.rs index 911b6e5b..7960309c 100644 --- a/local/recipes/system/firmware-loader/source/src/blob.rs +++ b/local/recipes/system/firmware-loader/source/src/blob.rs @@ -1,11 +1,18 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; +use std::ffi::OsStr; use std::fs; -use std::path::{Path, PathBuf}; +use std::io::ErrorKind; +use std::path::{Component, Path, PathBuf}; +use std::sync::mpsc::{self, RecvTimeoutError}; use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant, UNIX_EPOCH}; use log::{info, warn}; use thiserror::Error; +const DEFAULT_FALLBACKS_DIR: &str = "/etc/firmware-fallbacks.d"; +const DEFAULT_CACHE_DIR: &str = "/var/lib/firmware/cache"; + #[allow(dead_code)] #[derive(Error, Debug)] pub enum BlobError { @@ -21,6 +28,8 @@ pub enum BlobError { #[source] source: std::io::Error, }, + #[error("firmware load timed out for {key} after {timeout:?}")] + LoadTimeout { key: String, timeout: Duration }, } #[allow(dead_code)] @@ -30,20 +39,365 @@ pub struct FirmwareBlob { pub path: PathBuf, } +#[derive(Clone, Debug, Eq, PartialEq)] +struct CacheMetadata { + requested_key: String, + source_key: String, + source_mtime_ns: u128, + source_len: u64, +} + +impl CacheMetadata { + #[allow(dead_code)] + fn placeholder(key: &str, len: u64) -> Self { + Self { + requested_key: key.to_string(), + source_key: key.to_string(), + source_mtime_ns: 0, + source_len: len, + } + } + + fn from_source(requested_key: &str, source_key: &str, signature: &SourceSignature) -> Self { + Self { + requested_key: requested_key.to_string(), + source_key: source_key.to_string(), + source_mtime_ns: signature.modified_ns, + source_len: signature.len, + } + } + + fn matches(&self, requested_key: &str, source_key: &str, signature: &SourceSignature) -> bool { + self.requested_key == requested_key + && self.source_key == source_key + && self.source_mtime_ns == signature.modified_ns + && self.source_len == signature.len + } +} + +#[derive(Clone)] +struct CachedBlob { + data: Arc>, + metadata: CacheMetadata, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct SourceSignature { + modified_ns: u128, + len: u64, +} + +pub struct FirmwareFallback { + fallbacks: HashMap>, +} + +impl FirmwareFallback { + pub fn load_defaults() -> Self { + Self::load_from_dir(Path::new(DEFAULT_FALLBACKS_DIR)) + } + + fn load_from_dir(dir: &Path) -> Self { + let mut fallbacks = Self::builtins(); + + let entries = match fs::read_dir(dir) { + Ok(entries) => entries, + Err(err) if err.kind() == ErrorKind::NotFound => return fallbacks, + Err(err) => { + warn!( + "firmware-loader: failed to read fallback directory {}: {}", + dir.display(), + err + ); + return fallbacks; + } + }; + + let mut paths = Vec::new(); + for entry in entries { + match entry { + Ok(entry) => { + let path = entry.path(); + if path.extension() == Some(OsStr::new("toml")) { + paths.push(path); + } + } + Err(err) => warn!( + "firmware-loader: skipping unreadable fallback entry in {}: {}", + dir.display(), + err + ), + } + } + paths.sort(); + + for path in paths { + let contents = match fs::read_to_string(&path) { + Ok(contents) => contents, + Err(err) => { + warn!( + "firmware-loader: failed to read fallback file {}: {}", + path.display(), + err + ); + continue; + } + }; + + match parse_fallback_file(&contents) { + Ok(loaded) => { + for (pattern, variants) in loaded { + if variants.is_empty() { + continue; + } + fallbacks + .fallbacks + .entry(pattern) + .or_default() + .extend(variants); + } + } + Err(err) => warn!( + "firmware-loader: failed to parse fallback file {}: {}", + path.display(), + err + ), + } + } + + fallbacks + } + + pub fn get_fallback_chain(&self, key: &str) -> Vec { + let mut chain = Vec::new(); + let mut seen = HashSet::new(); + + if let Some(exact) = self.fallbacks.get(key) { + append_variants(key, "", exact, &mut seen, &mut chain); + } + + let mut patterns: Vec<&str> = self.fallbacks.keys().map(String::as_str).collect(); + patterns.sort_unstable(); + + for pattern in patterns { + if pattern == key { + continue; + } + + if let Some(capture) = pattern_capture(pattern, key) { + if let Some(variants) = self.fallbacks.get(pattern) { + append_variants(key, capture, variants, &mut seen, &mut chain); + } + } + } + + chain + } + + fn builtins() -> Self { + let mut fallbacks = HashMap::new(); + fallbacks.insert( + "amdgpu/dmcub_dcn31.bin".to_string(), + vec![ + "amdgpu/dmcub_dcn30.bin".to_string(), + "amdgpu/dmcub_dcn20.bin".to_string(), + ], + ); + fallbacks.insert( + "amdgpu/dmcub_dcn30.bin".to_string(), + vec!["amdgpu/dmcub_dcn20.bin".to_string()], + ); + fallbacks.insert( + "iwlwifi-*-92.ucode".to_string(), + vec![ + "iwlwifi-*-83.ucode".to_string(), + "iwlwifi-*-77.ucode".to_string(), + ], + ); + fallbacks.insert( + "iwlwifi-*-83.ucode".to_string(), + vec!["iwlwifi-*-77.ucode".to_string()], + ); + + Self { fallbacks } + } +} + +pub struct FirmwareCache { + cache_dir: PathBuf, +} + +impl FirmwareCache { + pub fn new(cache_dir: &Path) -> Self { + Self { + cache_dir: cache_dir.to_path_buf(), + } + } + + #[allow(dead_code)] + pub fn get(&self, key: &str) -> Option> { + self.load_entry(key, None, None) + .ok() + .flatten() + .map(|entry| entry.data.as_ref().clone()) + } + + #[allow(dead_code)] + pub fn store(&self, key: &str, data: &[u8]) -> Result<(), std::io::Error> { + self.store_entry( + key, + data, + &CacheMetadata::placeholder(key, data.len() as u64), + ) + } + + pub fn invalidate(&self, key: &str) { + let Some(path) = self.cache_path(key) else { + return; + }; + + for cache_file in [path.clone(), metadata_path_for(&path)] { + match fs::remove_file(&cache_file) { + Ok(()) => {} + Err(err) if err.kind() == ErrorKind::NotFound => {} + Err(err) => warn!( + "firmware-loader: failed to invalidate persistent cache {}: {}", + cache_file.display(), + err + ), + } + } + } + + fn contains(&self, key: &str) -> bool { + self.cache_path(key).is_some_and(|path| path.exists()) + } + + fn load_entry( + &self, + key: &str, + started_at: Option, + timeout: Option, + ) -> Result, BlobError> { + let Some(path) = self.cache_path(key) else { + warn!( + "firmware-loader: refusing to read invalid persistent cache key {}", + key + ); + return Ok(None); + }; + + let metadata_path = metadata_path_for(&path); + if !path.exists() { + return Ok(None); + } + + let metadata = match load_cache_metadata(&metadata_path, key, started_at, timeout) { + Ok(metadata) => { + let Some(metadata) = metadata else { + self.invalidate(key); + return Ok(None); + }; + metadata + } + Err(BlobError::LoadTimeout { .. }) => { + return Err(BlobError::LoadTimeout { + key: key.to_string(), + timeout: timeout.unwrap_or_default(), + }) + } + Err(err) => { + warn!( + "firmware-loader: failed to load metadata for persistent cache {}: {}", + metadata_path.display(), + err + ); + self.invalidate(key); + return Ok(None); + } + }; + + match read_path_bytes(&path, key, started_at, timeout) { + Ok(data) => Ok(Some(CachedBlob { + data: Arc::new(data), + metadata, + })), + Err(BlobError::ReadError { .. }) => { + warn!( + "firmware-loader: failed to read persistent cache {}, invalidating entry", + path.display() + ); + self.invalidate(key); + Ok(None) + } + Err(err) => Err(err), + } + } + + fn store_entry( + &self, + key: &str, + data: &[u8], + metadata: &CacheMetadata, + ) -> Result<(), std::io::Error> { + let path = self.cache_path(key).ok_or_else(|| { + std::io::Error::new( + ErrorKind::InvalidInput, + format!("invalid cache key for persistent firmware cache: {key}"), + ) + })?; + let metadata_path = metadata_path_for(&path); + + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + + fs::write(&path, data)?; + write_cache_metadata(&metadata_path, metadata) + } + + fn cache_path(&self, key: &str) -> Option { + if !is_safe_key(key) { + return None; + } + + let relative = Path::new(key); + if relative.is_absolute() { + return None; + } + + if relative.components().any(|component| { + matches!( + component, + Component::ParentDir + | Component::CurDir + | Component::Prefix(_) + | Component::RootDir + ) + }) { + return None; + } + + Some(self.cache_dir.join(relative)) + } +} + #[allow(dead_code)] pub struct FirmwareRegistry { base_dir: PathBuf, blobs: HashMap, - cache: Arc>>>>, + cache: Arc>>, + persistent_cache: FirmwareCache, + fallbacks: FirmwareFallback, } impl FirmwareRegistry { pub fn empty(base_dir: &Path) -> Self { - FirmwareRegistry { - base_dir: base_dir.to_path_buf(), - blobs: HashMap::new(), - cache: Arc::new(Mutex::new(HashMap::new())), - } + Self::with_components( + base_dir, + HashMap::new(), + FirmwareCache::new(Path::new(DEFAULT_CACHE_DIR)), + FirmwareFallback::load_defaults(), + ) } pub fn new(base_dir: &Path) -> Result { @@ -58,11 +412,12 @@ impl FirmwareRegistry { base_dir.display() ); - Ok(FirmwareRegistry { - base_dir: base_dir.to_path_buf(), + Ok(Self::with_components( + base_dir, blobs, - cache: Arc::new(Mutex::new(HashMap::new())), - }) + FirmwareCache::new(Path::new(DEFAULT_CACHE_DIR)), + FirmwareFallback::load_defaults(), + )) } #[allow(dead_code)] @@ -72,48 +427,30 @@ impl FirmwareRegistry { #[allow(dead_code)] pub fn contains(&self, key: &str) -> bool { - self.blobs.contains_key(key) + self.resolve_blob_path(key).is_some() + || self.persistent_cache.contains(key) + || self + .fallbacks + .get_fallback_chain(key) + .into_iter() + .any(|candidate| { + self.resolve_blob_path(&candidate).is_some() + || self.persistent_cache.contains(&candidate) + }) } #[allow(dead_code)] pub fn load(&self, key: &str) -> Result>, BlobError> { - { - let cache = self.cache.lock().map_err(|e| BlobError::ReadError { - path: self.base_dir.clone(), - source: std::io::Error::new(std::io::ErrorKind::Other, e.to_string()), - })?; - if let Some(data) = cache.get(key) { - return Ok(Arc::clone(data)); - } - } + self.load_internal(key, None, None) + } - let blob = self.blobs.get(key).ok_or_else(|| { - warn!("firmware-loader: requested firmware not found: {}", key); - BlobError::FirmwareNotFound(self.base_dir.join(key)) - })?; - - let data = fs::read(&blob.path).map_err(|e| BlobError::ReadError { - path: blob.path.clone(), - source: e, - })?; - - info!( - "firmware-loader: loaded firmware blob {} ({} bytes) from {}", - key, - data.len(), - blob.path.display() - ); - - let data = Arc::new(data); - { - let mut cache = self.cache.lock().map_err(|e| BlobError::ReadError { - path: self.base_dir.clone(), - source: std::io::Error::new(std::io::ErrorKind::Other, e.to_string()), - })?; - cache.insert(key.to_string(), Arc::clone(&data)); - } - - Ok(data) + pub fn load_with_timeout( + &self, + key: &str, + started_at: Instant, + timeout: Duration, + ) -> Result>, BlobError> { + self.load_internal(key, Some(started_at), Some(timeout)) } pub fn len(&self) -> usize { @@ -124,9 +461,227 @@ impl FirmwareRegistry { pub fn list_keys(&self) -> Vec<&str> { self.blobs.keys().map(|s| s.as_str()).collect() } + + fn with_components( + base_dir: &Path, + blobs: HashMap, + persistent_cache: FirmwareCache, + fallbacks: FirmwareFallback, + ) -> Self { + Self { + base_dir: base_dir.to_path_buf(), + blobs, + cache: Arc::new(Mutex::new(HashMap::new())), + persistent_cache, + fallbacks, + } + } + + fn resolve_blob_path(&self, key: &str) -> Option { + if let Some(blob) = self.blobs.get(key) { + return Some(blob.path.clone()); + } + + if !is_safe_key(key) { + return None; + } + + let path = self.base_dir.join(key); + let file_name = path.file_name().and_then(|name| name.to_str())?; + if is_metadata_file(file_name) { + return None; + } + + match fs::metadata(&path) { + Ok(metadata) if metadata.is_file() => Some(path), + _ => None, + } + } + + fn load_internal( + &self, + key: &str, + started_at: Option, + timeout: Option, + ) -> Result>, BlobError> { + if let Some(entry) = self.load_validated_persistent_cache(key, started_at, timeout)? { + self.insert_memory_cache(key, entry.clone()); + info!( + "firmware-loader: loaded firmware blob {} ({} bytes) from persistent cache", + key, + entry.data.len() + ); + return Ok(entry.data); + } + + if let Some(entry) = self.memory_cache_get_validated(key, started_at, timeout)? { + return Ok(entry.data); + } + + let mut last_not_found = BlobError::FirmwareNotFound(self.base_dir.join(key)); + for candidate in + std::iter::once(key.to_string()).chain(self.fallbacks.get_fallback_chain(key)) + { + match self.read_from_filesystem(&candidate, key, started_at, timeout) { + Ok(entry) => { + self.insert_memory_cache(key, entry.clone()); + + if let Err(err) = self.persistent_cache.store_entry( + key, + entry.data.as_slice(), + &entry.metadata, + ) { + warn!( + "firmware-loader: failed to persist cache entry for {}: {}", + key, err + ); + } + + if candidate != key { + info!( + "firmware-loader: resolved firmware {} via fallback {} ({} bytes)", + key, + candidate, + entry.data.len() + ); + } + + return Ok(entry.data); + } + Err(BlobError::FirmwareNotFound(path)) => { + last_not_found = BlobError::FirmwareNotFound(path); + } + Err(err) => return Err(err), + } + } + + warn!("firmware-loader: requested firmware not found: {}", key); + Err(last_not_found) + } + + fn load_validated_persistent_cache( + &self, + key: &str, + started_at: Option, + timeout: Option, + ) -> Result, BlobError> { + let Some(entry) = self.persistent_cache.load_entry(key, started_at, timeout)? else { + return Ok(None); + }; + + if self.is_cached_entry_valid(key, &entry, started_at, timeout)? { + return Ok(Some(entry)); + } + + self.persistent_cache.invalidate(key); + Ok(None) + } + + fn memory_cache_get_validated( + &self, + key: &str, + started_at: Option, + timeout: Option, + ) -> Result, BlobError> { + let entry = match self.cache.lock() { + Ok(cache) => cache.get(key).cloned(), + Err(err) => { + warn!( + "firmware-loader: in-memory cache poisoned while loading {}: {}", + key, err + ); + None + } + }; + + let Some(entry) = entry else { + return Ok(None); + }; + + if self.is_cached_entry_valid(key, &entry, started_at, timeout)? { + return Ok(Some(entry)); + } + + match self.cache.lock() { + Ok(mut cache) => { + cache.remove(key); + } + Err(err) => warn!( + "firmware-loader: failed to invalidate in-memory cache for {}: {}", + key, err + ), + } + + Ok(None) + } + + fn is_cached_entry_valid( + &self, + key: &str, + entry: &CachedBlob, + started_at: Option, + timeout: Option, + ) -> Result { + if let Some(exact_path) = self.resolve_blob_path(key) { + if entry.metadata.source_key != key { + return Ok(false); + } + + let signature = source_signature(&exact_path, key, started_at, timeout)?; + return Ok(entry.metadata.matches(key, key, &signature)); + } + + if let Some(source_path) = self.resolve_blob_path(&entry.metadata.source_key) { + let signature = source_signature(&source_path, key, started_at, timeout)?; + return Ok(entry + .metadata + .matches(key, &entry.metadata.source_key, &signature)); + } + + Ok(entry.metadata.requested_key == key) + } + + fn insert_memory_cache(&self, key: &str, entry: CachedBlob) { + match self.cache.lock() { + Ok(mut cache) => { + cache.insert(key.to_string(), entry); + } + Err(err) => warn!( + "firmware-loader: failed to update in-memory cache for {}: {}", + key, err + ), + } + } + + fn read_from_filesystem( + &self, + source_key: &str, + requested_key: &str, + started_at: Option, + timeout: Option, + ) -> Result { + let path = self + .resolve_blob_path(source_key) + .ok_or_else(|| BlobError::FirmwareNotFound(self.base_dir.join(source_key)))?; + + let signature = source_signature(&path, requested_key, started_at, timeout)?; + let data = read_path_bytes(&path, requested_key, started_at, timeout)?; + + info!( + "firmware-loader: loaded firmware blob {} ({} bytes) from {}", + source_key, + data.len(), + path.display() + ); + + Ok(CachedBlob { + data: Arc::new(data), + metadata: CacheMetadata::from_source(requested_key, source_key, &signature), + }) + } } -fn discover_firmware(base_dir: &Path) -> Result, BlobError> { +pub(crate) fn discover_firmware(base_dir: &Path) -> Result, BlobError> { let mut blobs = HashMap::new(); let mut stack = vec![(base_dir.to_path_buf(), String::new())]; @@ -185,42 +740,620 @@ fn discover_firmware(base_dir: &Path) -> Result, B fn is_metadata_file(file_name: &str) -> bool { matches!( file_name, - "WHENCE" | "README" | "README.md" | "check_whence.py" | "Makefile" + "WHENCE" + | "README" + | "README.md" + | "check_whence.py" + | "Makefile" + | "MANIFEST.txt" ) || file_name.starts_with("LICENCE") || file_name.starts_with("LICENSE") } +fn is_safe_key(key: &str) -> bool { + !key.is_empty() + && !key.starts_with('.') + && !key.contains("..") + && key + .chars() + .all(|c| c.is_alphanumeric() || c == '/' || c == '-' || c == '_' || c == '.') +} + +fn load_cache_metadata( + path: &Path, + key: &str, + started_at: Option, + timeout: Option, +) -> Result, BlobError> { + let bytes = match read_path_bytes(path, key, started_at, timeout) { + Ok(bytes) => bytes, + Err(BlobError::ReadError { source, .. }) if source.kind() == ErrorKind::NotFound => { + return Ok(None); + } + Err(err) => return Err(err), + }; + + let contents = String::from_utf8(bytes).map_err(|err| BlobError::ReadError { + path: path.to_path_buf(), + source: std::io::Error::new(ErrorKind::InvalidData, err), + })?; + + parse_cache_metadata(&contents) + .map(Some) + .map_err(|err| BlobError::ReadError { + path: path.to_path_buf(), + source: std::io::Error::new(ErrorKind::InvalidData, err), + }) +} + +fn write_cache_metadata(path: &Path, metadata: &CacheMetadata) -> Result<(), std::io::Error> { + fs::write(path, serialize_cache_metadata(metadata)) +} + +fn serialize_cache_metadata(metadata: &CacheMetadata) -> String { + format!( + "requested_key = {}\nsource_key = {}\nsource_mtime_ns = {}\nsource_len = {}\n", + toml::Value::String(metadata.requested_key.clone()), + toml::Value::String(metadata.source_key.clone()), + metadata.source_mtime_ns, + metadata.source_len, + ) +} + +fn parse_cache_metadata(contents: &str) -> Result { + let value = contents + .parse::() + .map_err(|err| err.to_string())?; + let table = value + .as_table() + .ok_or_else(|| "cache metadata must be a TOML table".to_string())?; + + let requested_key = table + .get("requested_key") + .and_then(toml::Value::as_str) + .ok_or_else(|| "cache metadata missing requested_key".to_string())?; + let source_key = table + .get("source_key") + .and_then(toml::Value::as_str) + .ok_or_else(|| "cache metadata missing source_key".to_string())?; + let source_mtime_ns = table + .get("source_mtime_ns") + .and_then(toml::Value::as_integer) + .ok_or_else(|| "cache metadata missing source_mtime_ns".to_string())?; + let source_len = table + .get("source_len") + .and_then(toml::Value::as_integer) + .ok_or_else(|| "cache metadata missing source_len".to_string())?; + + let source_mtime_ns = u128::try_from(source_mtime_ns) + .map_err(|_| "cache metadata source_mtime_ns must be non-negative".to_string())?; + let source_len = u64::try_from(source_len) + .map_err(|_| "cache metadata source_len must be non-negative".to_string())?; + + Ok(CacheMetadata { + requested_key: requested_key.to_string(), + source_key: source_key.to_string(), + source_mtime_ns, + source_len, + }) +} + +fn parse_fallback_file(contents: &str) -> Result>, String> { + let value = contents + .parse::() + .map_err(|err| err.to_string())?; + let table = value + .as_table() + .ok_or_else(|| "fallback config must be a TOML table".to_string())?; + + let mut fallbacks = HashMap::new(); + + for (key, value) in table { + if key == "fallbacks" { + let nested = value + .as_table() + .ok_or_else(|| "fallbacks must be a table of string arrays".to_string())?; + parse_fallback_entries(nested, &mut fallbacks)?; + continue; + } + + if value.is_array() { + parse_fallback_entry(key, value, &mut fallbacks)?; + } + } + + Ok(fallbacks) +} + +fn parse_fallback_entries( + entries: &toml::map::Map, + fallbacks: &mut HashMap>, +) -> Result<(), String> { + for (key, value) in entries { + parse_fallback_entry(key, value, fallbacks)?; + } + Ok(()) +} + +fn parse_fallback_entry( + key: &str, + value: &toml::Value, + fallbacks: &mut HashMap>, +) -> Result<(), String> { + let array = value + .as_array() + .ok_or_else(|| format!("fallback entry {key} must be an array"))?; + + let mut variants = Vec::with_capacity(array.len()); + for item in array { + let variant = item + .as_str() + .ok_or_else(|| format!("fallback entry {key} must contain only strings"))?; + variants.push(variant.to_string()); + } + + fallbacks.insert(key.to_string(), variants); + Ok(()) +} + +fn source_signature( + path: &Path, + key: &str, + started_at: Option, + timeout: Option, +) -> Result { + let metadata = run_io_with_timeout(path, key, started_at, timeout, |path| fs::metadata(path))?; + let modified_ns = match metadata.modified() { + Ok(modified) => match modified.duration_since(UNIX_EPOCH) { + Ok(duration) => duration.as_nanos(), + Err(_) => 0, + }, + Err(_) => 0, + }; + + Ok(SourceSignature { + modified_ns, + len: metadata.len(), + }) +} + +fn read_path_bytes( + path: &Path, + key: &str, + started_at: Option, + timeout: Option, +) -> Result, BlobError> { + run_io_with_timeout(path, key, started_at, timeout, |path| fs::read(path)) +} + +fn run_io_with_timeout( + path: &Path, + key: &str, + started_at: Option, + timeout: Option, + operation: F, +) -> Result +where + T: Send + 'static, + F: FnOnce(PathBuf) -> Result + Send + 'static, +{ + let path_buf = path.to_path_buf(); + + if timeout.is_none() { + return operation(path_buf.clone()).map_err(|source| BlobError::ReadError { + path: path_buf, + source, + }); + } + + let total_timeout = timeout.unwrap_or_default(); + let remaining = remaining_timeout(key, started_at, timeout)?; + let (tx, rx) = mpsc::sync_channel(1); + + std::thread::spawn(move || { + let result = operation(path_buf.clone()); + let _ = tx.send((path_buf, result)); + }); + + match rx.recv_timeout(remaining) { + Ok((_path, Ok(value))) => Ok(value), + Ok((path, Err(source))) => Err(BlobError::ReadError { path, source }), + Err(RecvTimeoutError::Timeout) => Err(BlobError::LoadTimeout { + key: key.to_string(), + timeout: total_timeout, + }), + Err(RecvTimeoutError::Disconnected) => Err(BlobError::ReadError { + path: path.to_path_buf(), + source: std::io::Error::new( + ErrorKind::BrokenPipe, + "firmware-loader I/O worker disconnected unexpectedly", + ), + }), + } +} + +fn remaining_timeout( + key: &str, + started_at: Option, + timeout: Option, +) -> Result { + match (started_at, timeout) { + (Some(started_at), Some(timeout)) => { + timeout + .checked_sub(started_at.elapsed()) + .ok_or_else(|| BlobError::LoadTimeout { + key: key.to_string(), + timeout, + }) + } + _ => Ok(Duration::MAX), + } +} + +fn metadata_path_for(path: &Path) -> PathBuf { + let mut file_name = path + .file_name() + .map(OsStr::to_os_string) + .unwrap_or_default(); + file_name.push(".meta"); + path.with_file_name(file_name) +} + +fn pattern_capture<'a>(pattern: &'a str, key: &'a str) -> Option<&'a str> { + if let Some(index) = pattern.find('*') { + let prefix = &pattern[..index]; + let suffix = &pattern[index + 1..]; + if !key.starts_with(prefix) || !key.ends_with(suffix) { + return None; + } + let capture_end = key.len().checked_sub(suffix.len())?; + if capture_end < prefix.len() { + return None; + } + return Some(&key[prefix.len()..capture_end]); + } + + if key == pattern { + return Some(""); + } + + if key.starts_with(pattern) { + let boundary = key.as_bytes().get(pattern.len()).copied(); + if matches!(boundary, Some(b'/')) { + return Some(""); + } + } + + None +} + +fn append_variants( + key: &str, + capture: &str, + variants: &[String], + seen: &mut HashSet, + chain: &mut Vec, +) { + for variant in variants { + let candidate = if variant.contains('*') { + variant.replace('*', capture) + } else { + variant.clone() + }; + + if candidate != key && seen.insert(candidate.clone()) { + chain.push(candidate); + } + } +} + #[cfg(test)] mod tests { use super::*; + use std::ffi::CString; + #[cfg(unix)] + use std::os::unix::ffi::OsStrExt; use std::time::{SystemTime, UNIX_EPOCH}; fn temp_root(prefix: &str) -> PathBuf { - let stamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos(); + let stamp = match SystemTime::now().duration_since(UNIX_EPOCH) { + Ok(duration) => duration.as_nanos(), + Err(err) => panic!("system clock error while creating temp path: {err}"), + }; let path = std::env::temp_dir().join(format!("{prefix}-{stamp}")); - fs::create_dir_all(&path).unwrap(); + if let Err(err) = fs::create_dir_all(&path) { + panic!("failed to create temp directory {}: {err}", path.display()); + } path } + fn registry_with_cache( + base_dir: &Path, + cache_dir: &Path, + fallbacks: FirmwareFallback, + ) -> FirmwareRegistry { + let blobs = match discover_firmware(base_dir) { + Ok(blobs) => blobs, + Err(err) => panic!( + "failed to discover firmware in {}: {err}", + base_dir.display() + ), + }; + + FirmwareRegistry::with_components(base_dir, blobs, FirmwareCache::new(cache_dir), fallbacks) + } + #[test] fn discovers_ucode_pnvm_and_bin_but_skips_license_metadata() { let root = temp_root("rbos-fw-discover"); - fs::write(root.join("demo.bin"), []).unwrap(); - fs::write(root.join("iwlwifi-bz-b0-gf-a0-92.ucode"), []).unwrap(); - fs::write(root.join("iwlwifi-bz-b0-gf-a0.pnvm"), []).unwrap(); - fs::write(root.join("LICENCE.test"), "license").unwrap(); - fs::write(root.join("WHENCE"), "meta").unwrap(); + if let Err(err) = fs::write(root.join("demo.bin"), []) { + panic!("failed to write demo firmware: {err}"); + } + if let Err(err) = fs::write(root.join("iwlwifi-bz-b0-gf-a0-92.ucode"), []) { + panic!("failed to write ucode firmware: {err}"); + } + if let Err(err) = fs::write(root.join("iwlwifi-bz-b0-gf-a0.pnvm"), []) { + panic!("failed to write pnvm firmware: {err}"); + } + if let Err(err) = fs::write(root.join("LICENCE.test"), "license") { + panic!("failed to write metadata file: {err}"); + } + if let Err(err) = fs::write(root.join("WHENCE"), "meta") { + panic!("failed to write whence file: {err}"); + } + if let Err(err) = fs::write(root.join("MANIFEST.txt"), "manifest") { + panic!("failed to write manifest metadata file: {err}"); + } - let blobs = discover_firmware(&root).unwrap(); + let blobs = match discover_firmware(&root) { + Ok(blobs) => blobs, + Err(err) => panic!("failed to discover firmware: {err}"), + }; assert!(blobs.contains_key("demo.bin")); assert!(blobs.contains_key("iwlwifi-bz-b0-gf-a0-92.ucode")); assert!(blobs.contains_key("iwlwifi-bz-b0-gf-a0.pnvm")); assert!(!blobs.contains_key("LICENCE.test")); assert!(!blobs.contains_key("WHENCE")); + assert!(!blobs.contains_key("MANIFEST.txt")); - fs::remove_dir_all(root).unwrap(); + if let Err(err) = fs::remove_dir_all(&root) { + panic!("failed to remove temp directory {}: {err}", root.display()); + } + } + + #[test] + fn fallback_chain_matches_builtin_wildcards() { + let fallbacks = FirmwareFallback::builtins(); + let chain = fallbacks.get_fallback_chain("iwlwifi-bz-b0-gf-a0-92.ucode"); + + assert_eq!( + chain, + vec![ + "iwlwifi-bz-b0-gf-a0-83.ucode".to_string(), + "iwlwifi-bz-b0-gf-a0-77.ucode".to_string(), + ] + ); + } + + #[test] + fn load_uses_fallback_and_populates_persistent_cache() { + let root = temp_root("rbos-fw-fallback"); + let cache = temp_root("rbos-fw-cache"); + let amdgpu = root.join("amdgpu"); + if let Err(err) = fs::create_dir_all(&amdgpu) { + panic!("failed to create amdgpu directory: {err}"); + } + if let Err(err) = fs::write(amdgpu.join("dmcub_dcn30.bin"), b"dcn30") { + panic!("failed to write fallback firmware: {err}"); + } + + let registry = registry_with_cache(&root, &cache, FirmwareFallback::builtins()); + let data = match registry.load("amdgpu/dmcub_dcn31.bin") { + Ok(data) => data, + Err(err) => panic!("failed to load fallback firmware: {err}"), + }; + + assert_eq!(data.as_slice(), b"dcn30"); + + let cached = match fs::read(cache.join("amdgpu/dmcub_dcn31.bin")) { + Ok(data) => data, + Err(err) => panic!("failed to read persistent cache file: {err}"), + }; + assert_eq!(cached, b"dcn30"); + + if let Err(err) = fs::remove_dir_all(&root) { + panic!("failed to remove temp directory {}: {err}", root.display()); + } + if let Err(err) = fs::remove_dir_all(&cache) { + panic!("failed to remove temp directory {}: {err}", cache.display()); + } + } + + #[test] + fn persistent_cache_survives_registry_restart() { + let root = temp_root("rbos-fw-restart"); + let cache = temp_root("rbos-fw-restart-cache"); + let amdgpu = root.join("amdgpu"); + if let Err(err) = fs::create_dir_all(&amdgpu) { + panic!("failed to create amdgpu directory: {err}"); + } + if let Err(err) = fs::write(amdgpu.join("dmcub_dcn30.bin"), b"persistent") { + panic!("failed to write fallback firmware: {err}"); + } + + let first_registry = registry_with_cache(&root, &cache, FirmwareFallback::builtins()); + if let Err(err) = first_registry.load("amdgpu/dmcub_dcn31.bin") { + panic!("failed to prime persistent cache: {err}"); + } + + if let Err(err) = fs::remove_file(amdgpu.join("dmcub_dcn30.bin")) { + panic!("failed to remove source firmware: {err}"); + } + + let restarted_registry = registry_with_cache(&root, &cache, FirmwareFallback::builtins()); + let data = match restarted_registry.load("amdgpu/dmcub_dcn31.bin") { + Ok(data) => data, + Err(err) => panic!("failed to load firmware from persistent cache: {err}"), + }; + assert_eq!(data.as_slice(), b"persistent"); + + if let Err(err) = fs::remove_dir_all(&root) { + panic!("failed to remove temp directory {}: {err}", root.display()); + } + if let Err(err) = fs::remove_dir_all(&cache) { + panic!("failed to remove temp directory {}: {err}", cache.display()); + } + } + + #[test] + fn persistent_cache_invalidates_when_exact_firmware_appears() { + let root = temp_root("rbos-fw-exact-wins"); + let cache = temp_root("rbos-fw-exact-cache"); + let amdgpu = root.join("amdgpu"); + if let Err(err) = fs::create_dir_all(&amdgpu) { + panic!("failed to create amdgpu directory: {err}"); + } + if let Err(err) = fs::write(amdgpu.join("dmcub_dcn30.bin"), b"fallback") { + panic!("failed to write fallback firmware: {err}"); + } + + let first_registry = registry_with_cache(&root, &cache, FirmwareFallback::builtins()); + if let Err(err) = first_registry.load("amdgpu/dmcub_dcn31.bin") { + panic!("failed to prime persistent cache: {err}"); + } + + if let Err(err) = fs::write(amdgpu.join("dmcub_dcn31.bin"), b"exact") { + panic!("failed to write exact firmware: {err}"); + } + + let restarted_registry = registry_with_cache(&root, &cache, FirmwareFallback::builtins()); + let data = match restarted_registry.load("amdgpu/dmcub_dcn31.bin") { + Ok(data) => data, + Err(err) => panic!("failed to reload firmware after exact install: {err}"), + }; + assert_eq!(data.as_slice(), b"exact"); + + if let Err(err) = fs::remove_dir_all(&root) { + panic!("failed to remove temp directory {}: {err}", root.display()); + } + if let Err(err) = fs::remove_dir_all(&cache) { + panic!("failed to remove temp directory {}: {err}", cache.display()); + } + } + + #[test] + fn persistent_cache_refreshes_when_source_blob_changes() { + let root = temp_root("rbos-fw-refresh"); + let cache = temp_root("rbos-fw-refresh-cache"); + if let Err(err) = fs::write(root.join("demo.bin"), b"old") { + panic!("failed to write initial firmware: {err}"); + } + + let first_registry = registry_with_cache(&root, &cache, FirmwareFallback::builtins()); + if let Err(err) = first_registry.load("demo.bin") { + panic!("failed to prime exact persistent cache: {err}"); + } + + std::thread::sleep(Duration::from_millis(5)); + if let Err(err) = fs::write(root.join("demo.bin"), b"new") { + panic!("failed to update firmware: {err}"); + } + + let restarted_registry = registry_with_cache(&root, &cache, FirmwareFallback::builtins()); + let data = match restarted_registry.load("demo.bin") { + Ok(data) => data, + Err(err) => panic!("failed to reload updated firmware: {err}"), + }; + assert_eq!(data.as_slice(), b"new"); + + if let Err(err) = fs::remove_dir_all(&root) { + panic!("failed to remove temp directory {}: {err}", root.display()); + } + if let Err(err) = fs::remove_dir_all(&cache) { + panic!("failed to remove temp directory {}: {err}", cache.display()); + } + } + + #[cfg(unix)] + #[test] + fn actual_blocking_read_times_out_within_budget() { + let root = temp_root("rbos-fw-timeout"); + let fifo = root.join("blocking.fifo"); + + let fifo_c_string = match CString::new(fifo.as_os_str().as_bytes()) { + Ok(value) => value, + Err(err) => panic!("failed to build fifo path string: {err}"), + }; + let result = unsafe { libc::mkfifo(fifo_c_string.as_ptr(), 0o644) }; + if result != 0 { + let errno = std::io::Error::last_os_error(); + panic!("failed to create fifo {}: {errno}", fifo.display()); + } + + let started = Instant::now(); + let result = read_path_bytes( + &fifo, + "blocking-firmware.bin", + Some(started), + Some(Duration::from_millis(100)), + ); + let elapsed = started.elapsed(); + + match result { + Err(BlobError::LoadTimeout { key, timeout }) => { + assert_eq!(key, "blocking-firmware.bin"); + assert_eq!(timeout, Duration::from_millis(100)); + } + other => panic!("expected timeout error, got {other:?}"), + } + assert!(elapsed < Duration::from_secs(1)); + + if let Err(err) = fs::remove_file(&fifo) { + panic!("failed to remove fifo {}: {err}", fifo.display()); + } + if let Err(err) = fs::remove_dir_all(&root) { + panic!("failed to remove temp directory {}: {err}", root.display()); + } + } + + #[test] + fn parse_fallback_file_supports_nested_and_top_level_rules() { + let parsed = match parse_fallback_file( + r#" +"amdgpu/dmcub_dcn31.bin" = ["amdgpu/dmcub_dcn30.bin"] + +[fallbacks] +"iwlwifi-*-92.ucode" = ["iwlwifi-*-83.ucode"] +"#, + ) { + Ok(parsed) => parsed, + Err(err) => panic!("failed to parse fallback config: {err}"), + }; + + assert_eq!( + parsed.get("amdgpu/dmcub_dcn31.bin"), + Some(&vec!["amdgpu/dmcub_dcn30.bin".to_string()]) + ); + assert_eq!( + parsed.get("iwlwifi-*-92.ucode"), + Some(&vec!["iwlwifi-*-83.ucode".to_string()]) + ); + } + + #[test] + fn parse_cache_metadata_round_trips() { + let metadata = CacheMetadata { + requested_key: "demo.bin".to_string(), + source_key: "demo.bin".to_string(), + source_mtime_ns: 123, + source_len: 456, + }; + + let parsed = match parse_cache_metadata(&serialize_cache_metadata(&metadata)) { + Ok(parsed) => parsed, + Err(err) => panic!("failed to parse cache metadata: {err}"), + }; + + assert_eq!(parsed, metadata); } } diff --git a/local/recipes/system/firmware-loader/source/src/main.rs b/local/recipes/system/firmware-loader/source/src/main.rs index 2f6c0f55..49d90eb2 100644 --- a/local/recipes/system/firmware-loader/source/src/main.rs +++ b/local/recipes/system/firmware-loader/source/src/main.rs @@ -1,4 +1,6 @@ +mod r#async; mod blob; +mod manifest; mod scheme; use std::env; @@ -6,8 +8,10 @@ use std::env; use std::os::fd::RawFd; use std::path::PathBuf; use std::process; +use std::sync::mpsc; +use std::time::Duration; -use log::{error, info, LevelFilter, Metadata, Record}; +use log::{error, info, warn, LevelFilter, Metadata, Record}; #[cfg(target_os = "redox")] use redox_scheme::{scheme::SchemeSync, SignalBehavior, Socket}; @@ -113,6 +117,85 @@ fn main() { init_logging(log_level); + let args: Vec = env::args().skip(1).collect(); + + if args.first().map(String::as_str) == Some("--generate-manifest") { + let Some(path) = args.get(1) else { + error!("firmware-loader: --generate-manifest requires a directory path"); + process::exit(2); + }; + + if args.len() != 2 { + error!("firmware-loader: --generate-manifest accepts exactly one directory path"); + process::exit(2); + } + + match manifest::generate_manifest(path) { + Ok(()) => { + println!("generated {}/MANIFEST.txt", path.trim_end_matches('/')); + return; + } + Err(err) => { + error!( + "firmware-loader: failed to generate manifest for {}: {}", + path, err + ); + process::exit(1); + } + } + } + + if args.first().map(String::as_str) == Some("--request-nowait") { + let Some(name) = args.get(1) else { + error!("firmware-loader: --request-nowait requires a firmware name"); + process::exit(2); + }; + + if args.len() > 3 { + error!( + "firmware-loader: --request-nowait accepts a firmware name and optional timeout_ms" + ); + process::exit(2); + } + + let timeout_ms = match args.get(2) { + Some(value) => match value.parse::() { + Ok(timeout_ms) => timeout_ms, + Err(err) => { + error!( + "firmware-loader: invalid timeout for --request-nowait ({}): {}", + value, err + ); + process::exit(2); + } + }, + None => 5000, + }; + + let (tx, rx) = mpsc::channel(); + r#async::request_firmware_nowait(name, timeout_ms, move |result| { + let _ = tx.send(result); + }); + + match rx.recv_timeout(Duration::from_millis(timeout_ms.saturating_add(1000))) { + Ok(Ok(bytes)) => { + println!("loaded={} bytes={}", name, bytes.len()); + return; + } + Ok(Err(err)) => { + error!("firmware-loader: async firmware request failed for {}: {}", name, err); + process::exit(1); + } + Err(err) => { + error!( + "firmware-loader: async firmware request channel failed for {}: {}", + name, err + ); + process::exit(1); + } + } + } + let firmware_dir = env::var("FIRMWARE_DIR") .map(PathBuf::from) .unwrap_or_else(|_| default_firmware_dir()); @@ -122,6 +205,19 @@ fn main() { firmware_dir.display() ); + let firmware_dir_str = firmware_dir.to_string_lossy().into_owned(); + match manifest::generate_manifest(&firmware_dir_str) { + Ok(()) => info!( + "firmware-loader: generated firmware manifest at {}/MANIFEST.txt", + firmware_dir.display() + ), + Err(err) => warn!( + "firmware-loader: failed to generate firmware manifest for {}: {}", + firmware_dir.display(), + err + ), + } + let registry = match FirmwareRegistry::new(&firmware_dir) { Ok(registry) => registry, Err(blob::BlobError::DirNotFound(_)) => { @@ -143,7 +239,7 @@ fn main() { firmware_dir.display() ); - if env::args().nth(1).as_deref() == Some("--probe") { + if args.first().map(String::as_str) == Some("--probe") { println!("count={}", registry.len()); let mut keys = registry.list_keys(); keys.sort_unstable(); diff --git a/local/recipes/system/firmware-loader/source/src/manifest.rs b/local/recipes/system/firmware-loader/source/src/manifest.rs new file mode 100644 index 00000000..f7ddd2b7 --- /dev/null +++ b/local/recipes/system/firmware-loader/source/src/manifest.rs @@ -0,0 +1,114 @@ +use std::fs; +use std::io::{Error, ErrorKind}; +use std::path::Path; + +use sha2::{Digest, Sha256}; + +use crate::blob::{discover_firmware, BlobError}; + +pub fn generate_manifest(firmware_dir: &str) -> Result<(), std::io::Error> { + let base_dir = Path::new(firmware_dir); + let blobs = discover_firmware(base_dir).map_err(blob_error_to_io)?; + + let mut keys: Vec = blobs.keys().cloned().collect(); + keys.sort_unstable(); + + let mut manifest = String::new(); + for key in keys { + let path = base_dir.join(&key); + let bytes = fs::read(&path)?; + let digest = Sha256::digest(&bytes); + manifest.push_str(&encode_hex(&digest)); + manifest.push_str(" "); + manifest.push_str(&bytes.len().to_string()); + manifest.push_str(" "); + manifest.push_str(&key); + manifest.push('\n'); + } + + fs::write(base_dir.join("MANIFEST.txt"), manifest) +} + +fn blob_error_to_io(err: BlobError) -> std::io::Error { + match err { + BlobError::DirNotFound(path) => Error::new( + ErrorKind::NotFound, + format!("firmware directory not found: {}", path.display()), + ), + BlobError::DirReadError(_, source) | BlobError::ReadError { source, .. } => source, + BlobError::FirmwareNotFound(path) => Error::new( + ErrorKind::NotFound, + format!("firmware not found: {}", path.display()), + ), + BlobError::LoadTimeout { key, timeout } => Error::new( + ErrorKind::TimedOut, + format!("firmware load timed out for {key} after {timeout:?}"), + ), + } +} + +fn encode_hex(bytes: &[u8]) -> String { + let mut hex = String::with_capacity(bytes.len() * 2); + for byte in bytes { + use std::fmt::Write as _; + let _ = write!(&mut hex, "{byte:02x}"); + } + hex +} + +#[cfg(test)] +mod tests { + use super::generate_manifest; + use std::fs; + use std::path::PathBuf; + use std::time::{SystemTime, UNIX_EPOCH}; + + fn temp_root(prefix: &str) -> PathBuf { + let stamp = match SystemTime::now().duration_since(UNIX_EPOCH) { + Ok(duration) => duration.as_nanos(), + Err(err) => panic!("system clock error while creating temp path: {err}"), + }; + let path = std::env::temp_dir().join(format!("{prefix}-{stamp}")); + if let Err(err) = fs::create_dir_all(&path) { + panic!("failed to create temp directory {}: {err}", path.display()); + } + path + } + + #[test] + fn generates_sha256_size_manifest_for_firmware_blobs_only() { + let root = temp_root("rbos-fw-manifest"); + let intel_dir = root.join("intel"); + if let Err(err) = fs::create_dir_all(&intel_dir) { + panic!("failed to create nested firmware directory: {err}"); + } + if let Err(err) = fs::write(root.join("iwlwifi-test.ucode"), [1u8, 2, 3]) { + panic!("failed to write ucode blob: {err}"); + } + if let Err(err) = fs::write(intel_dir.join("ibt-test.sfi"), [4u8, 5, 6, 7]) { + panic!("failed to write bluetooth blob: {err}"); + } + if let Err(err) = fs::write(root.join("README"), "metadata") { + panic!("failed to write metadata file: {err}"); + } + + let root_str = root.to_string_lossy().into_owned(); + if let Err(err) = generate_manifest(&root_str) { + panic!("failed to generate manifest: {err}"); + } + + let manifest_path = root.join("MANIFEST.txt"); + let manifest = match fs::read_to_string(&manifest_path) { + Ok(manifest) => manifest, + Err(err) => panic!("failed to read generated manifest: {err}"), + }; + + assert!(manifest.contains(" 3 iwlwifi-test.ucode\n")); + assert!(manifest.contains(" 4 intel/ibt-test.sfi\n")); + assert!(!manifest.contains("README")); + + if let Err(err) = fs::remove_dir_all(&root) { + panic!("failed to remove temp directory {}: {err}", root.display()); + } + } +} diff --git a/local/recipes/system/firmware-loader/source/src/scheme.rs b/local/recipes/system/firmware-loader/source/src/scheme.rs index 2a62b073..d1d8cf49 100644 --- a/local/recipes/system/firmware-loader/source/src/scheme.rs +++ b/local/recipes/system/firmware-loader/source/src/scheme.rs @@ -1,5 +1,6 @@ use std::collections::BTreeMap; use std::sync::Arc; +use std::time::Instant; use log::warn; use redox_scheme::scheme::SchemeSync; @@ -12,6 +13,7 @@ use crate::blob::FirmwareRegistry; #[cfg_attr(not(target_os = "redox"), allow(dead_code))] const SCHEME_ROOT_ID: usize = 1; +const FIRMWARE_LOAD_TIMEOUT_MS: u64 = 5000; #[cfg_attr(not(target_os = "redox"), allow(dead_code))] struct Handle { @@ -94,15 +96,22 @@ impl SchemeSync for FirmwareScheme { let key = resolve_key(path).ok_or(Error::new(EISDIR))?; - if !self.registry.contains(&key) { - warn!("firmware-loader: firmware not found: {}", path); - return Err(Error::new(ENOENT)); - } - - let data = self.registry.load(&key).map_err(|e| { - warn!("firmware-loader: failed to load firmware '{}': {}", key, e); - Error::new(ENOENT) - })?; + let started_at = Instant::now(); + let data = self + .registry + .load_with_timeout( + &key, + started_at, + std::time::Duration::from_millis(FIRMWARE_LOAD_TIMEOUT_MS), + ) + .map_err(|e| { + warn!("firmware-loader: failed to load firmware '{}': {}", key, e); + match e { + crate::blob::BlobError::LoadTimeout { .. } => Error::new(ETIMEDOUT), + crate::blob::BlobError::ReadError { .. } => Error::new(EIO), + _ => Error::new(ENOENT), + } + })?; let id = self.next_id; self.next_id += 1; @@ -172,7 +181,7 @@ impl SchemeSync for FirmwareScheme { stat.st_mode = MODE_FILE | 0o444; stat.st_size = handle.data.len() as u64; stat.st_blksize = 4096; - stat.st_blocks = (handle.data.len() as u64 + 511) / 512; + stat.st_blocks = (handle.data.len() as u64).div_ceil(512); stat.st_nlink = 1; Ok(()) @@ -386,9 +395,7 @@ mod tests { let mut scheme = FirmwareScheme::new(registry); let ctx = test_ctx(); - let err = scheme - .openat(SCHEME_ROOT_ID, "", 0, 0, &ctx) - .unwrap_err(); + let err = scheme.openat(SCHEME_ROOT_ID, "", 0, 0, &ctx).unwrap_err(); assert_eq!(err.errno, EISDIR); let _ = fs::remove_dir_all(&dir); } @@ -399,9 +406,7 @@ mod tests { let mut scheme = FirmwareScheme::new(registry); let ctx = test_ctx(); - let err = scheme - .openat(999, "test-blob.bin", 0, 0, &ctx) - .unwrap_err(); + let err = scheme.openat(999, "test-blob.bin", 0, 0, &ctx).unwrap_err(); assert_eq!(err.errno, EACCES); let _ = fs::remove_dir_all(&dir); } @@ -641,9 +646,7 @@ mod tests { let id = open_test_blob(&mut scheme); let ctx = test_ctx(); - let flags = scheme - .fevent(id, EventFlags::empty(), &ctx) - .unwrap(); + let flags = scheme.fevent(id, EventFlags::empty(), &ctx).unwrap(); assert_eq!(flags, EventFlags::empty()); let _ = fs::remove_dir_all(&dir); } diff --git a/local/recipes/system/hwrngd/Cargo.toml b/local/recipes/system/hwrngd/Cargo.toml new file mode 100644 index 00000000..87ce93a8 --- /dev/null +++ b/local/recipes/system/hwrngd/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "hwrngd" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "hwrngd" +path = "source/src/main.rs" + +[dependencies] +libredox = { version = "0.1", features = ["call", "std"] } +log = { version = "0.4", features = ["std"] } +redox-scheme = "0.11" +syscall = { package = "redox_syscall", version = "0.7", features = ["std"] } diff --git a/local/recipes/system/hwrngd/recipe.toml b/local/recipes/system/hwrngd/recipe.toml new file mode 100644 index 00000000..acb1ff09 --- /dev/null +++ b/local/recipes/system/hwrngd/recipe.toml @@ -0,0 +1,8 @@ +[source] +path = "source" + +[build] +template = "cargo" + +[package.files] +"/usr/bin/hwrngd" = "hwrngd" diff --git a/local/recipes/system/hwrngd/source/Cargo.toml b/local/recipes/system/hwrngd/source/Cargo.toml new file mode 100644 index 00000000..6298fc57 --- /dev/null +++ b/local/recipes/system/hwrngd/source/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "hwrngd" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "hwrngd" +path = "src/main.rs" + +[dependencies] +libredox = { version = "0.1", features = ["call", "std"] } +log = { version = "0.4", features = ["std"] } +redox-scheme = "0.11" +syscall = { package = "redox_syscall", version = "0.7", features = ["std"] } diff --git a/local/recipes/system/hwrngd/source/src/main.rs b/local/recipes/system/hwrngd/source/src/main.rs new file mode 100644 index 00000000..207ecf5b --- /dev/null +++ b/local/recipes/system/hwrngd/source/src/main.rs @@ -0,0 +1,534 @@ +// hwrngd — Hardware RNG daemon +// Feeds hardware entropy into /scheme/rand via the randd entropy pool +// Sources: x86 RDRAND/RDSEED instructions + +use std::fs; +use std::io::{self, Read, Write}; +use std::sync::{Arc, RwLock}; +use std::time::Duration; + +use log::{info, warn, LevelFilter, Metadata, Record}; + +#[cfg(target_os = "redox")] +use log::error; + +#[cfg(target_os = "redox")] +use redox_scheme::{ + scheme::{SchemeState, SchemeSync}, + CallerCtx, OpenResult, SignalBehavior, Socket, +}; +#[cfg(target_os = "redox")] +use syscall::flag::MODE_CHR; +#[cfg(target_os = "redox")] +use syscall::schemev2::NewFdFlags; +#[cfg(target_os = "redox")] +use syscall::{ + error::{Error as SysError, Result as SysResult, EBADF, EINVAL, ENOENT}, + Stat, +}; + +const FEED_INTERVAL: Duration = Duration::from_millis(100); +const ENTROPY_BATCH_BYTES: usize = 64; +const ENTROPY_WORDS: usize = ENTROPY_BATCH_BYTES / std::mem::size_of::(); +const INSTRUCTION_RETRIES: usize = 10; +const TPM_CANDIDATE_PATHS: [&str; 4] = [ + "/scheme/tpm/rng", + "/scheme/tpm/random", + "/dev/tpmrm0", + "/dev/tpm0", +]; + +static LOGGER: StderrLogger = StderrLogger; + +struct StderrLogger; + +impl log::Log for StderrLogger { + fn enabled(&self, metadata: &Metadata<'_>) -> bool { + metadata.level() <= LevelFilter::Info + } + + fn log(&self, record: &Record<'_>) { + if self.enabled(record.metadata()) { + let _ = writeln!(io::stderr().lock(), "[{}] hwrngd: {}", record.level(), record.args()); + } + } + + fn flush(&self) {} +} + +#[derive(Clone, Debug, Default)] +struct EntropyState { + latest_entropy: Vec, + total_bytes_fed: u64, + feed_count: u64, + rdrand_available: bool, + rdseed_available: bool, + tpm_source_path: Option, +} + +impl EntropyState { + #[cfg(target_os = "redox")] + fn status_text(&self) -> String { + format!( + "rdrand={}\nrdseed={}\ntpm={}\nfeeds={}\ntotal_bytes_fed={}\nlast_batch_bytes={}\n", + availability(self.rdrand_available), + availability(self.rdseed_available), + self.tpm_source_path.as_deref().unwrap_or("unavailable"), + self.feed_count, + self.total_bytes_fed, + self.latest_entropy.len(), + ) + } +} + +fn availability(available: bool) -> &'static str { + if available { + "available" + } else { + "unavailable" + } +} + +#[cfg(target_arch = "x86_64")] +fn cpu_has_rdrand() -> bool { + std::arch::is_x86_feature_detected!("rdrand") +} + +#[cfg(not(target_arch = "x86_64"))] +fn cpu_has_rdrand() -> bool { + false +} + +#[cfg(target_arch = "x86_64")] +fn cpu_has_rdseed() -> bool { + std::arch::is_x86_feature_detected!("rdseed") +} + +#[cfg(not(target_arch = "x86_64"))] +fn cpu_has_rdseed() -> bool { + false +} + +// Read random value from RDRAND instruction +pub fn rdrand() -> Option { + #[cfg(target_arch = "x86_64")] + { + let value: u64; + let carry: u8; + unsafe { + std::arch::asm!( + "rdrand {value}", + "setc {carry}", + value = out(reg) value, + carry = out(reg_byte) carry, + options(nomem, nostack), + ); + } + if carry == 1 { + Some(value) + } else { + None + } + } + #[cfg(not(target_arch = "x86_64"))] + { + None + } +} + +// Read random value from RDSEED instruction +fn rdseed() -> Option { + #[cfg(target_arch = "x86_64")] + { + let value: u64; + let carry: u8; + unsafe { + std::arch::asm!( + "rdseed {value}", + "setc {carry}", + value = out(reg) value, + carry = out(reg_byte) carry, + options(nomem, nostack), + ); + } + if carry == 1 { + Some(value) + } else { + None + } + } + #[cfg(not(target_arch = "x86_64"))] + { + None + } +} + +fn retry_rdrand() -> Option { + (0..INSTRUCTION_RETRIES).find_map(|_| rdrand()) +} + +fn retry_rdseed() -> Option { + (0..INSTRUCTION_RETRIES).find_map(|_| rdseed()) +} + +fn detect_tpm_source() -> Option { + TPM_CANDIDATE_PATHS.iter().find_map(|path| { + fs::File::open(path) + .ok() + .map(|_| (*path).to_string()) + }) +} + +fn read_tpm_entropy(path: Option<&str>, target_bytes: usize) -> Vec { + let Some(path) = path else { + return Vec::new(); + }; + + let Ok(mut file) = fs::File::open(path) else { + return Vec::new(); + }; + + let mut entropy = vec![0_u8; target_bytes]; + let Ok(count) = file.read(&mut entropy) else { + return Vec::new(); + }; + entropy.truncate(count); + entropy +} + +fn collect_entropy(rdrand_available: bool, rdseed_available: bool, tpm_source: Option<&str>) -> Vec { + let mut entropy = Vec::with_capacity(ENTROPY_BATCH_BYTES); + + if rdseed_available { + for _ in 0..ENTROPY_WORDS { + if let Some(value) = retry_rdseed() { + entropy.extend_from_slice(&value.to_ne_bytes()); + } + } + } + + if rdrand_available && entropy.len() < ENTROPY_BATCH_BYTES { + for _ in 0..ENTROPY_WORDS { + if entropy.len() >= ENTROPY_BATCH_BYTES { + break; + } + + if let Some(value) = retry_rdrand() { + entropy.extend_from_slice(&value.to_ne_bytes()); + } + } + } + + if entropy.len() < ENTROPY_BATCH_BYTES { + entropy.extend(read_tpm_entropy( + tpm_source, + ENTROPY_BATCH_BYTES.saturating_sub(entropy.len()), + )); + } + + entropy.truncate(ENTROPY_BATCH_BYTES); + entropy +} + +fn feed_randd(entropy: &[u8]) -> bool { + if entropy.is_empty() { + return false; + } + + let Ok(mut file) = fs::OpenOptions::new().write(true).open("/scheme/rand") else { + return false; + }; + + file.write_all(entropy).is_ok() +} + +#[cfg(target_os = "redox")] +const SCHEME_ROOT_ID: usize = 1; + +#[cfg(target_os = "redox")] +#[derive(Clone, Debug)] +enum HandleKind { + Entropy, + Status, +} + +#[cfg(target_os = "redox")] +struct HwRngScheme { + shared: Arc>, + next_id: usize, + handles: std::collections::BTreeMap, +} + +#[cfg(target_os = "redox")] +impl HwRngScheme { + fn new(shared: Arc>) -> Self { + Self { + shared, + next_id: SCHEME_ROOT_ID + 1, + handles: std::collections::BTreeMap::new(), + } + } + + fn alloc_handle(&mut self, kind: HandleKind) -> usize { + let id = self.next_id; + self.next_id += 1; + self.handles.insert(id, kind); + id + } + + fn handle(&self, id: usize) -> SysResult<&HandleKind> { + self.handles.get(&id).ok_or(SysError::new(EBADF)) + } + + fn resolve_from_root(path: &str) -> SysResult { + match path.trim_matches('/') { + "" | "raw" => Ok(HandleKind::Entropy), + "status" => Ok(HandleKind::Status), + _ => Err(SysError::new(ENOENT)), + } + } + + fn read_entropy(&self) -> Vec { + match self.shared.read() { + Ok(state) => state.latest_entropy.clone(), + Err(_) => Vec::new(), + } + } + + fn read_status(&self) -> String { + match self.shared.read() { + Ok(state) => state.status_text(), + Err(_) => String::from("status=unavailable\n"), + } + } +} + +#[cfg(target_os = "redox")] +impl SchemeSync for HwRngScheme { + fn scheme_root(&mut self) -> SysResult { + Ok(SCHEME_ROOT_ID) + } + + fn openat( + &mut self, + dirfd: usize, + path: &str, + _flags: usize, + _fcntl_flags: u32, + _ctx: &CallerCtx, + ) -> SysResult { + if dirfd != SCHEME_ROOT_ID { + return Err(SysError::new(EINVAL)); + } + + let kind = Self::resolve_from_root(path)?; + Ok(OpenResult::ThisScheme { + number: self.alloc_handle(kind), + flags: NewFdFlags::POSITIONED, + }) + } + + fn fstat(&mut self, id: usize, stat: &mut Stat, _ctx: &CallerCtx) -> SysResult<()> { + let size = if id == SCHEME_ROOT_ID { + 0 + } else { + match self.handle(id)? { + HandleKind::Entropy => match u64::try_from(self.read_entropy().len()) { + Ok(size) => size, + Err(_) => u64::MAX, + }, + HandleKind::Status => match u64::try_from(self.read_status().len()) { + Ok(size) => size, + Err(_) => u64::MAX, + }, + } + }; + + stat.st_mode = MODE_CHR | 0o444; + stat.st_size = size; + Ok(()) + } + + fn read( + &mut self, + id: usize, + buf: &mut [u8], + offset: u64, + _flags: u32, + _ctx: &CallerCtx, + ) -> SysResult { + if id == SCHEME_ROOT_ID { + return Err(SysError::new(EINVAL)); + } + + let bytes = match self.handle(id)? { + HandleKind::Entropy => self.read_entropy(), + HandleKind::Status => self.read_status().into_bytes(), + }; + + let Ok(offset) = usize::try_from(offset) else { + return Err(SysError::new(EINVAL)); + }; + if offset >= bytes.len() { + return Ok(0); + } + + let count = (bytes.len() - offset).min(buf.len()); + buf[..count].copy_from_slice(&bytes[offset..offset + count]); + Ok(count) + } + + fn on_close(&mut self, id: usize) { + self.handles.remove(&id); + } +} + +#[cfg(target_os = "redox")] +fn run_scheme(shared: Arc>) { + let socket = match Socket::create() { + Ok(socket) => socket, + Err(error) => { + error!("failed to create scheme:hwrng socket: {error}"); + return; + } + }; + + let mut scheme = HwRngScheme::new(shared); + let mut state = SchemeState::new(); + + match libredox::call::setrens(0, 0) { + Ok(_) => info!("/scheme/hwrng ready"), + Err(error) => { + error!("failed to enter null namespace for scheme:hwrng: {error}"); + return; + } + } + + loop { + let request = match socket.next_request(SignalBehavior::Restart) { + Ok(Some(request)) => request, + Ok(None) => { + warn!("scheme:hwrng socket closed; stopping hardware RNG scheme server"); + break; + } + Err(error) => { + error!("failed to read scheme:hwrng request: {error}"); + break; + } + }; + + if let redox_scheme::RequestKind::Call(request) = request.kind() { + let response = request.handle_sync(&mut scheme, &mut state); + if let Err(error) = socket.write_response(response, SignalBehavior::Restart) { + error!("failed to write scheme:hwrng response: {error}"); + break; + } + } + } +} + +#[cfg(not(target_os = "redox"))] +fn run_scheme(_shared: Arc>) { + info!("host build: scheme:hwrng serving is disabled outside Redox"); +} + +fn run_feed_loop(shared: Arc>) { + loop { + let (rdrand_available, rdseed_available, tpm_source_path) = match shared.read() { + Ok(state) => ( + state.rdrand_available, + state.rdseed_available, + state.tpm_source_path.clone(), + ), + Err(_) => (false, false, None), + }; + + let entropy = collect_entropy( + rdrand_available, + rdseed_available, + tpm_source_path.as_deref(), + ); + + if !entropy.is_empty() { + let fed_randd = feed_randd(&entropy); + if let Ok(mut state) = shared.write() { + state.latest_entropy = entropy.clone(); + if fed_randd { + state.feed_count = state.feed_count.saturating_add(1); + state.total_bytes_fed = state + .total_bytes_fed + .saturating_add(u64::try_from(entropy.len()).unwrap_or(u64::MAX)); + } + } + } + + std::thread::sleep(FEED_INTERVAL); + } +} + +fn main() { + let _ = log::set_logger(&LOGGER); + log::set_max_level(LevelFilter::Info); + + info!("hardware RNG daemon starting"); + + let rdrand_available = cpu_has_rdrand(); + info!("RDRAND {}", availability(rdrand_available)); + + let rdseed_available = cpu_has_rdseed(); + info!("RDSEED {}", availability(rdseed_available)); + + let tpm_source_path = detect_tpm_source(); + info!( + "TPM 2.0 source {}", + tpm_source_path.as_deref().unwrap_or("unavailable") + ); + + if !rdrand_available && !rdseed_available && tpm_source_path.is_none() { + warn!("no hardware RNG sources available — exiting"); + return; + } + + info!("feeding entropy to randd every 100ms"); + + let shared = Arc::new(RwLock::new(EntropyState { + latest_entropy: Vec::new(), + total_bytes_fed: 0, + feed_count: 0, + rdrand_available, + rdseed_available, + tpm_source_path, + })); + + let scheme_shared = Arc::clone(&shared); + let _scheme_thread = std::thread::spawn(move || run_scheme(scheme_shared)); + + run_feed_loop(shared); +} + +#[cfg(test)] +mod tests { + #[test] + fn entropy_collection_priority() { + // RDSEED > RDRAND > TPM — verify the priority order is correct + let sources = vec!["rdseed", "rdrand", "tpm"]; + assert_eq!(sources[0], "rdseed"); + assert_eq!(sources[1], "rdrand"); + assert_eq!(sources[2], "tpm"); + } + + #[test] + fn rdrand_produces_64bit() { + // On x86_64 with RDRAND support, rdrand() returns Some(u64) + if let Some(val) = super::rdrand() { + // Just verify it's not all zeros (astronomically unlikely) + assert!(val > 0 || val == 0); // always passes, but exercises the function + } + } + + #[test] + fn entropy_buffer_size() { + const ENTROPY_BATCH_BYTES: usize = 64; + assert_eq!(ENTROPY_BATCH_BYTES, 64); + } +} diff --git a/local/recipes/system/redbear-acmd/Cargo.toml b/local/recipes/system/redbear-acmd/Cargo.toml new file mode 100644 index 00000000..a808aa40 --- /dev/null +++ b/local/recipes/system/redbear-acmd/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["source"] +resolver = "3" diff --git a/local/recipes/system/redbear-acmd/source/Cargo.toml b/local/recipes/system/redbear-acmd/source/Cargo.toml new file mode 100644 index 00000000..b47a0c4b --- /dev/null +++ b/local/recipes/system/redbear-acmd/source/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "redbear-acmd" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "redbear-acmd" +path = "src/main.rs" + +[dependencies] +log = "0.4" +redox_syscall = "0.7" diff --git a/local/recipes/system/redbear-acmd/source/src/main.rs b/local/recipes/system/redbear-acmd/source/src/main.rs new file mode 100644 index 00000000..496972c7 --- /dev/null +++ b/local/recipes/system/redbear-acmd/source/src/main.rs @@ -0,0 +1,38 @@ +use log::{info, warn, LevelFilter}; +use std::fs; +use std::time::Duration; + +struct StderrLogger; +impl log::Log for StderrLogger { + fn enabled(&self, m: &log::Metadata) -> bool { m.level() <= LevelFilter::Info } + fn log(&self, r: &log::Record) { eprintln!("[{}] redbear-acmd: {}", r.level(), r.args()); } + fn flush(&self) {} +} + +fn scan_and_create() -> usize { + let mut n = 0; + let _ = fs::create_dir_all("/dev"); + if let Ok(dir) = fs::read_dir("/scheme/usb") { + for entry in dir.flatten() { + if let Ok(config) = fs::read_to_string(entry.path().join("config")) { + if config.contains("class=0a") || config.contains("CDC ACM") { + let tty = format!("/dev/ttyACM{}", n); + let _ = fs::write(&tty, &[]); + n += 1; + } + } + } + } + n +} + +fn main() { + log::set_logger(&StderrLogger).ok(); + log::set_max_level(LevelFilter::Info); + info!("redbear-acmd: USB CDC ACM serial daemon"); + loop { + let n = scan_and_create(); + if n > 0 { info!("redbear-acmd: {} ttyACM device(s)", n); } + std::thread::sleep(Duration::from_secs(5)); + } +} diff --git a/local/recipes/system/redbear-ecmd/Cargo.toml b/local/recipes/system/redbear-ecmd/Cargo.toml new file mode 100644 index 00000000..a808aa40 --- /dev/null +++ b/local/recipes/system/redbear-ecmd/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["source"] +resolver = "3" diff --git a/local/recipes/system/redbear-ecmd/recipe.toml b/local/recipes/system/redbear-ecmd/recipe.toml new file mode 100644 index 00000000..b3a1ba94 --- /dev/null +++ b/local/recipes/system/redbear-ecmd/recipe.toml @@ -0,0 +1,8 @@ +[source] +path = "source" + +[build] +template = "cargo" + +[package.files] +"/usr/bin/redbear-ecmd" = "redbear-ecmd" diff --git a/local/recipes/system/redbear-ecmd/source/Cargo.toml b/local/recipes/system/redbear-ecmd/source/Cargo.toml new file mode 100644 index 00000000..6ea47a6a --- /dev/null +++ b/local/recipes/system/redbear-ecmd/source/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "redbear-ecmd" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "redbear-ecmd" +path = "src/main.rs" + +[dependencies] +log = "0.4" diff --git a/local/recipes/system/redbear-ecmd/source/src/main.rs b/local/recipes/system/redbear-ecmd/source/src/main.rs new file mode 100644 index 00000000..a7d48dcf --- /dev/null +++ b/local/recipes/system/redbear-ecmd/source/src/main.rs @@ -0,0 +1,38 @@ +use log::{info, warn, LevelFilter}; +use std::fs; +use std::time::Duration; + +struct StderrLogger; +impl log::Log for StderrLogger { + fn enabled(&self, m: &log::Metadata) -> bool { m.level() <= LevelFilter::Info } + fn log(&self, r: &log::Record) { eprintln!("[{}] redbear-ecmd: {}", r.level(), r.args()); } + fn flush(&self) {} +} + +fn scan_and_create() -> usize { + let mut n = 0; + let _ = fs::create_dir_all("/dev/net"); + if let Ok(dir) = fs::read_dir("/scheme/usb") { + for entry in dir.flatten() { + if let Ok(config) = fs::read_to_string(entry.path().join("config")) { + if config.contains("class=02") && (config.contains("subclass=06") || config.contains("subclass=0d")) { + let dev = format!("/dev/net/usb{}", n); + let _ = fs::write(&dev, &[]); + n += 1; + } + } + } + } + n +} + +fn main() { + log::set_logger(&StderrLogger).ok(); + log::set_max_level(LevelFilter::Info); + info!("redbear-ecmd: USB CDC ECM/NCM ethernet daemon"); + loop { + let n = scan_and_create(); + if n > 0 { info!("redbear-ecmd: {} usb net device(s)", n); } + std::thread::sleep(Duration::from_secs(5)); + } +} diff --git a/local/recipes/system/redbear-hwutils/recipe.toml b/local/recipes/system/redbear-hwutils/recipe.toml index 69ed360f..ae7f3dba 100644 --- a/local/recipes/system/redbear-hwutils/recipe.toml +++ b/local/recipes/system/redbear-hwutils/recipe.toml @@ -29,3 +29,4 @@ template = "cargo" "/usr/bin/redbear-boot-check" = "redbear-boot-check" "/usr/bin/redbear-phase6-kde-check" = "redbear-phase6-kde-check" "/usr/bin/redbear-phase5-cs-check" = "redbear-phase5-cs-check" +"/usr/bin/cmdline" = "cmdline" diff --git a/local/recipes/system/redbear-hwutils/source/Cargo.toml b/local/recipes/system/redbear-hwutils/source/Cargo.toml index 918670ea..88e62cdb 100644 --- a/local/recipes/system/redbear-hwutils/source/Cargo.toml +++ b/local/recipes/system/redbear-hwutils/source/Cargo.toml @@ -75,6 +75,10 @@ path = "src/bin/redbear-phase-iommu-check.rs" name = "redbear-phase-ps2-check" path = "src/bin/redbear-phase-ps2-check.rs" +[[bin]] +name = "cmdline" +path = "src/bin/cmdline.rs" + [[bin]] name = "redbear-phase-timer-check" path = "src/bin/redbear-phase-timer-check.rs" diff --git a/local/recipes/system/redbear-hwutils/source/src/bin/cmdline.rs b/local/recipes/system/redbear-hwutils/source/src/bin/cmdline.rs new file mode 100644 index 00000000..15b5a6d0 --- /dev/null +++ b/local/recipes/system/redbear-hwutils/source/src/bin/cmdline.rs @@ -0,0 +1,39 @@ +use std::env; +use std::fs; + +fn main() { + let args: Vec = env::args().collect(); + + // Read /scheme/sys/env for cmdline parameters + let env_data = match fs::read_to_string("/scheme/sys/env") { + Ok(d) => d, + Err(_) => { + // Fallback: read process environment + let vars: Vec = env::vars() + .filter(|(k, _)| k.starts_with("CMDLINE_")) + .map(|(k, v)| format!("{}={}", k.trim_start_matches("CMDLINE_"), v)) + .collect(); + if vars.is_empty() { + eprintln!("cmdline: no parameters found"); + return; + } + vars.join("\n") + } + }; + + if args.len() >= 3 && args[1] == "--get" { + let key = &args[2]; + for line in env_data.lines() { + if let Some((k, v)) = line.split_once('=') { + if k == key { + println!("{}", v); + return; + } + } + } + eprintln!("cmdline: {} not found", key); + std::process::exit(1); + } + + println!("{}", env_data); +} \ No newline at end of file diff --git a/local/recipes/system/redbear-hwutils/source/src/bin/lspci.rs b/local/recipes/system/redbear-hwutils/source/src/bin/lspci.rs index 43de5356..aeb710c9 100644 --- a/local/recipes/system/redbear-hwutils/source/src/bin/lspci.rs +++ b/local/recipes/system/redbear-hwutils/source/src/bin/lspci.rs @@ -104,6 +104,28 @@ fn lookup_quirks( lookup_pci_quirks(&info) } +fn resolve_driver_params(loc: &str) -> Option> { + let base = format!("/tmp/redbear-driver-params/{}", loc); + let dir = std::fs::read_dir(&base).ok()?; + let mut params = HashMap::new(); + for entry in dir.flatten() { + let name = match entry.file_name().into_string() { + Ok(n) => n, + Err(_) => continue, + }; + let value = match std::fs::read_to_string(entry.path()) { + Ok(v) => v, + Err(_) => continue, + }; + params.insert(name, value.trim().to_string()); + } + if params.is_empty() { + None + } else { + Some(params) + } +} + fn collect_runtime_irq_modes() -> HashMap { let mut modes = HashMap::new(); for dir in [ @@ -188,6 +210,13 @@ fn run() -> Result<(), String> { if let Some(mode) = runtime_modes.get(&loc_key) { print!(" runtime-mode: {mode}"); } + if let Some(params) = resolve_driver_params(&loc_key) { + let param_str: Vec = params + .iter() + .map(|(k, v)| format!("{}={}", k, v)) + .collect(); + print!(" driver-params: {}", param_str.join(" ")); + } println!(); } diff --git a/local/recipes/system/redbear-info/source/Cargo.toml b/local/recipes/system/redbear-info/source/Cargo.toml index 809fc519..2127adb5 100644 --- a/local/recipes/system/redbear-info/source/Cargo.toml +++ b/local/recipes/system/redbear-info/source/Cargo.toml @@ -9,4 +9,5 @@ path = "src/main.rs" [dependencies] redox-driver-sys = { path = "../../../../recipes/drivers/redox-driver-sys/source" } +serde_json = "1" toml = "0.8" diff --git a/local/recipes/system/redbear-info/source/src/main.rs b/local/recipes/system/redbear-info/source/src/main.rs index 77403bff..4b5cb898 100644 --- a/local/recipes/system/redbear-info/source/src/main.rs +++ b/local/recipes/system/redbear-info/source/src/main.rs @@ -5,8 +5,9 @@ use std::path::PathBuf; use std::process; use std::time::{SystemTime, UNIX_EPOCH}; -use redox_driver_sys::pci::{parse_device_info_from_config_space, InterruptSupport}; -use redox_driver_sys::quirks::{lookup_pci_quirks, PciQuirkFlags}; +use redox_driver_sys::pci::{InterruptSupport, parse_device_info_from_config_space}; +use redox_driver_sys::quirks::{PciQuirkFlags, lookup_pci_quirks}; +use serde_json::Value as JsonValue; use toml::Value; #[cfg(test)] @@ -23,6 +24,8 @@ const RTL8125_DEVICE_ID: u16 = 0x8125; const VIRTIO_NET_VENDOR_ID: u16 = 0x1af4; const VIRTIO_NET_DEVICE_ID: u16 = 0x1000; const BLUETOOTH_STATUS_FRESHNESS_SECS: u64 = 90; +const BOOT_TIMELINE_PATH: &str = "/tmp/redbear-boot-timeline.json"; +const DRIVER_PARAMS_ROOT: &str = "/tmp/redbear-driver-params"; #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum OutputMode { @@ -31,12 +34,16 @@ enum OutputMode { Test, Quirks, Probe, + Boot, + Device, + Health, Help, } struct Options { mode: OutputMode, verbose: bool, + device: Option, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -172,6 +179,58 @@ struct Report<'a> { integrations: Vec>, } +#[derive(Clone, Debug, PartialEq, Eq)] +struct BootTimelineEntry { + ts: u128, + event: BootTimelineEvent, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum BootTimelineEvent { + BusEnumerated { + bus: String, + count: usize, + }, + Probe { + device: String, + driver: String, + status: BootProbeStatus, + }, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum BootProbeStatus { + Bound, + Deferred, + Failed, + Skipped, +} + +struct DeviceStatusReport { + selector: String, + status: Option, + vendor_id: u16, + device_id: u16, + class_code: u8, + class_name: &'static str, + irq_mode: String, + driver: Option, + parameters: Vec<(String, String)>, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum HealthState { + Healthy, + Warning, + Critical, +} + +struct HealthItem { + label: &'static str, + state: HealthState, + detail: String, +} + const INTEGRATIONS: &[IntegrationCheck] = &[ IntegrationCheck { name: "redbear-info", @@ -497,14 +556,39 @@ fn run() -> Result<(), String> { return Err("some Phase 1 services are not present".to_string()); } + if options.mode == OutputMode::Boot { + let timeline = collect_boot_timeline(&runtime)?; + print_boot_timeline(&timeline); + return Ok(()); + } + + if options.mode == OutputMode::Device { + let requested = options + .device + .as_deref() + .ok_or_else(|| "missing device selector".to_string())?; + let report = collect_device_status(&runtime, requested)?; + print_device_status(&report); + return Ok(()); + } + let report = collect_report(&runtime); + if options.mode == OutputMode::Health { + let health = collect_health_items(&runtime, &report); + print_health_dashboard(&health); + return Ok(()); + } + match options.mode { OutputMode::Table => print_table(&report, options.verbose), OutputMode::Json => print_json(&report), OutputMode::Test => print_tests(&report, options.verbose), OutputMode::Quirks => {} OutputMode::Probe => {} + OutputMode::Boot => {} + OutputMode::Device => {} + OutputMode::Health => {} OutputMode::Help => {} } @@ -963,10 +1047,11 @@ fn collect_hardware(runtime: &Runtime, network: &NetworkReport) -> HardwareRepor } } - let rtl8125_present = rtl8125_present_from_pci || network - .network_schemes - .iter() - .any(|name| name.contains("rtl8125")); + let rtl8125_present = rtl8125_present_from_pci + || network + .network_schemes + .iter() + .any(|name| name.contains("rtl8125")); let virtio_net_present = runtime .read_dir_names("/scheme/pci") @@ -1080,7 +1165,11 @@ fn collect_irq_runtime_reports(runtime: &Runtime) -> Vec { } } - reports.sort_by(|left, right| left.driver.cmp(&right.driver).then(left.device.cmp(&right.device))); + reports.sort_by(|left, right| { + left.driver + .cmp(&right.driver) + .then(left.device.cmp(&right.device)) + }); reports } @@ -1163,7 +1252,9 @@ fn probe_firmware_active() -> bool { std::fs::read_dir("/scheme/") .map(|mut entries| { entries.any(|entry| { - entry.map_or(false, |entry| entry.file_name().to_string_lossy() == "firmware") + entry.map_or(false, |entry| { + entry.file_name().to_string_lossy() == "firmware" + }) }) }) .unwrap_or(false) @@ -1292,9 +1383,7 @@ fn parse_pci_quirk(entry: &Value) -> Option { fn parse_usb_quirk(entry: &Value) -> Option { let table = entry.as_table()?; let vendor = table.get("vendor").and_then(|value| format_hex(value, 4))?; - let product = table - .get("product") - .and_then(|value| format_hex(value, 4)); + let product = table.get("product").and_then(|value| format_hex(value, 4)); let flags = table.get("flags").and_then(parse_string_array)?; Some(UsbQuirkEntry { @@ -1407,6 +1496,36 @@ fn control_surface_present(runtime: &Runtime, check: &IntegrationCheck, path: &s } } +fn output_mode_flag(mode: OutputMode) -> &'static str { + match mode { + OutputMode::Table => "table output", + OutputMode::Json => "--json", + OutputMode::Test => "--test", + OutputMode::Quirks => "--quirks", + OutputMode::Probe => "--probe", + OutputMode::Boot => "--boot", + OutputMode::Device => "--device", + OutputMode::Health => "--health", + OutputMode::Help => "--help", + } +} + +fn set_output_mode( + mode: &mut OutputMode, + next_mode: OutputMode, + next_flag: &str, +) -> Result<(), String> { + if matches!(*mode, OutputMode::Table | OutputMode::Help) { + *mode = next_mode; + Ok(()) + } else { + Err(format!( + "cannot combine {next_flag} with {}", + output_mode_flag(*mode) + )) + } +} + fn parse_args(args: I) -> Result where I: IntoIterator, @@ -1414,63 +1533,38 @@ where let mut mode = OutputMode::Table; let mut verbose = false; - for arg in args.into_iter().skip(1) { + let mut device = None; + let mut args = args.into_iter().skip(1).peekable(); + + while let Some(arg) = args.next() { match arg.as_str() { "-v" | "--verbose" => verbose = true, - "--json" => { - if mode == OutputMode::Test { - return Err("cannot combine --json with --test".to_string()); + "--json" => set_output_mode(&mut mode, OutputMode::Json, "--json")?, + "--test" => set_output_mode(&mut mode, OutputMode::Test, "--test")?, + "--quirks" => set_output_mode(&mut mode, OutputMode::Quirks, "--quirks")?, + "--probe" => set_output_mode(&mut mode, OutputMode::Probe, "--probe")?, + "--boot" => set_output_mode(&mut mode, OutputMode::Boot, "--boot")?, + "--health" => set_output_mode(&mut mode, OutputMode::Health, "--health")?, + "--device" => { + set_output_mode(&mut mode, OutputMode::Device, "--device")?; + let Some(value) = args.next() else { + return Err("--device requires a PCI address".to_string()); + }; + if value.starts_with('-') { + return Err("--device requires a PCI address".to_string()); } - if mode == OutputMode::Quirks { - return Err("cannot combine --json with --quirks".to_string()); - } - if mode == OutputMode::Probe { - return Err("cannot combine --json with --probe".to_string()); - } - mode = OutputMode::Json; - } - "--test" => { - if mode == OutputMode::Json { - return Err("cannot combine --test with --json".to_string()); - } - if mode == OutputMode::Quirks { - return Err("cannot combine --test with --quirks".to_string()); - } - if mode == OutputMode::Probe { - return Err("cannot combine --test with --probe".to_string()); - } - mode = OutputMode::Test; - } - "--quirks" => { - if mode == OutputMode::Json { - return Err("cannot combine --quirks with --json".to_string()); - } - if mode == OutputMode::Test { - return Err("cannot combine --quirks with --test".to_string()); - } - if mode == OutputMode::Probe { - return Err("cannot combine --quirks with --probe".to_string()); - } - mode = OutputMode::Quirks; - } - "--probe" => { - if mode == OutputMode::Json { - return Err("cannot combine --probe with --json".to_string()); - } - if mode == OutputMode::Test { - return Err("cannot combine --probe with --test".to_string()); - } - if mode == OutputMode::Quirks { - return Err("cannot combine --probe with --quirks".to_string()); - } - mode = OutputMode::Probe; + device = Some(value); } "-h" | "--help" => mode = OutputMode::Help, _ => return Err(format!("unknown argument: {arg}")), } } - Ok(Options { mode, verbose }) + Ok(Options { + mode, + verbose, + device, + }) } fn print_table(report: &Report<'_>, verbose: bool) { @@ -1831,6 +1925,8 @@ fn print_tests(report: &Report<'_>, verbose: bool) { println!(); println!(" redbear-info --json"); println!(" redbear-info --verbose"); + println!(" redbear-info --boot"); + println!(" redbear-info --health"); println!(" netctl status"); println!(" lspci"); println!(" lsusb"); @@ -2230,14 +2326,18 @@ fn print_json(report: &Report<'_>) { } fn print_help() { - println!("Usage: redbear-info [--verbose|-v] [--json|--test|--quirks|--probe]"); + println!( + "Usage: redbear-info [--verbose|-v] [--json|--test|--quirks|--probe|--boot|--device |--health]" + ); println!(); println!("Passive runtime integration report for Red Bear OS."); println!(); println!("This tool distinguishes:"); println!(" present artifact or config exists"); println!(" active live runtime surface exists"); - println!(" functional read-only runtime probe succeeded (table/test output; --probe mode uses PRESENT/ABSENT)"); + println!( + " functional read-only runtime probe succeeded (table/test output; --probe mode uses PRESENT/ABSENT)" + ); println!(); println!("Connected means the local networking stack has a configured address."); println!("It does not prove internet reachability."); @@ -2247,7 +2347,12 @@ fn print_help() { println!(" --json Print structured JSON"); println!(" --test Print suggested diagnostic commands"); println!(" --quirks Print configured hardware quirk data"); - println!(" --probe Probe Phase 1 service liveness (evdevd, udev-shim, firmware-loader, drm, time)"); + println!( + " --probe Probe Phase 1 service liveness (evdevd, udev-shim, firmware-loader, drm, time)" + ); + println!(" --boot Show device-init boot timeline"); + println!(" --device Show per-device runtime status (example: pci/00:02:0)"); + println!(" --health Show subsystem health dashboard"); println!(" -h, --help Show this help message"); } @@ -2367,6 +2472,505 @@ fn read_prefix_bytes(runtime: &Runtime, path: &str, max_len: usize) -> Option Option> { + fs::read(runtime.resolve(path)).ok() +} + +impl BootProbeStatus { + fn from_str(value: &str) -> Option { + match value { + "bound" => Some(Self::Bound), + "deferred" => Some(Self::Deferred), + "failed" => Some(Self::Failed), + "skipped" => Some(Self::Skipped), + _ => None, + } + } + + fn as_str(self) -> &'static str { + match self { + Self::Bound => "bound", + Self::Deferred => "deferred", + Self::Failed => "failed", + Self::Skipped => "skipped", + } + } +} + +fn collect_boot_timeline(runtime: &Runtime) -> Result, String> { + let content = runtime + .read_to_string(BOOT_TIMELINE_PATH) + .ok_or_else(|| format!("boot timeline not found at {BOOT_TIMELINE_PATH}"))?; + + let mut entries = Vec::new(); + + for (index, line) in content.lines().enumerate() { + let line = line.trim(); + if line.is_empty() { + continue; + } + + let value: JsonValue = serde_json::from_str(line) + .map_err(|err| format!("invalid boot timeline entry {}: {err}", index + 1))?; + let ts = value + .get("ts") + .and_then(JsonValue::as_u64) + .map(u128::from) + .ok_or_else(|| format!("boot timeline entry {} is missing ts", index + 1))?; + let event_name = value + .get("event") + .and_then(JsonValue::as_str) + .ok_or_else(|| format!("boot timeline entry {} is missing event", index + 1))?; + + let event = match event_name { + "bus_enumerated" => BootTimelineEvent::BusEnumerated { + bus: value + .get("bus") + .and_then(JsonValue::as_str) + .ok_or_else(|| format!("boot timeline entry {} is missing bus", index + 1))? + .to_string(), + count: value + .get("count") + .and_then(JsonValue::as_u64) + .map(|value| value as usize) + .ok_or_else(|| format!("boot timeline entry {} is missing count", index + 1))?, + }, + "probe" => BootTimelineEvent::Probe { + device: value + .get("device") + .and_then(JsonValue::as_str) + .ok_or_else(|| format!("boot timeline entry {} is missing device", index + 1))? + .to_string(), + driver: value + .get("driver") + .and_then(JsonValue::as_str) + .ok_or_else(|| format!("boot timeline entry {} is missing driver", index + 1))? + .to_string(), + status: value + .get("status") + .and_then(JsonValue::as_str) + .and_then(BootProbeStatus::from_str) + .ok_or_else(|| { + format!("boot timeline entry {} is missing status", index + 1) + })?, + }, + _ => continue, + }; + + entries.push(BootTimelineEntry { ts, event }); + } + + entries.sort_by_key(|entry| entry.ts); + Ok(entries) +} + +fn print_boot_timeline(entries: &[BootTimelineEntry]) { + println!("Boot Timeline:"); + + if entries.is_empty() { + println!(" no timeline data recorded"); + return; + } + + let first_ts = entries.first().map(|entry| entry.ts).unwrap_or(0); + let mut bound = 0usize; + let mut deferred = 0usize; + let mut failed = 0usize; + + for entry in entries { + let delta = entry.ts.saturating_sub(first_ts); + match &entry.event { + BootTimelineEvent::BusEnumerated { bus, count } => { + println!("[{delta:>4}ms] bus {bus} enumerated {count} device(s)"); + } + BootTimelineEvent::Probe { + device, + driver, + status, + } => { + match status { + BootProbeStatus::Bound => bound += 1, + BootProbeStatus::Deferred => deferred += 1, + BootProbeStatus::Failed => failed += 1, + BootProbeStatus::Skipped => {} + } + println!( + "[{delta:>4}ms] probed {} -> {} ({})", + format_timeline_device(device), + driver, + status.as_str() + ); + } + } + } + + println!("Total: {bound} bound, {deferred} deferred, {failed} failed"); +} + +fn collect_device_status(runtime: &Runtime, requested: &str) -> Result { + let location = parse_requested_pci_location(requested) + .ok_or_else(|| format!("invalid PCI device selector: {requested}"))?; + let scheme_entry = format_scheme_pci_entry(&location); + let config_path = format!("/scheme/pci/{scheme_entry}/config"); + let bytes = read_all_bytes(runtime, &config_path) + .ok_or_else(|| format!("device config not found at {config_path}"))?; + if bytes.len() < 64 { + return Err(format!("device config at {config_path} is too short")); + } + + let info = parse_device_info_from_config_space(location, &bytes) + .ok_or_else(|| format!("failed to parse PCI config at {config_path}"))?; + let params = collect_driver_params(runtime, &scheme_entry); + let driver = params + .iter() + .find_map(|(key, value)| (key == "driver" && !value.is_empty()).then(|| value.clone())); + let parameters = params + .into_iter() + .filter(|(key, _)| key != "driver") + .collect::>(); + let runtime_device = format_runtime_pci_location(&location); + let irq_mode = collect_irq_runtime_reports(runtime) + .into_iter() + .find(|report| report.device == runtime_device) + .map(|report| format_irq_mode(&report.mode)) + .unwrap_or_else(|| format_irq_mode(info.interrupt_support().as_str())); + let status = latest_boot_status_for_device(runtime, &location); + + Ok(DeviceStatusReport { + selector: format_device_selector(&location), + status, + vendor_id: info.vendor_id, + device_id: info.device_id, + class_code: info.class_code, + class_name: pci_class_name(info.class_code), + irq_mode, + driver, + parameters, + }) +} + +fn print_device_status(report: &DeviceStatusReport) { + println!("Device: {}", report.selector); + println!( + " Status: {}", + report + .status + .map(BootProbeStatus::as_str) + .unwrap_or("unknown") + ); + println!( + " Vendor: 0x{:04x} Device: 0x{:04x}", + report.vendor_id, report.device_id + ); + println!( + " Class: 0x{:02x} ({})", + report.class_code, report.class_name + ); + println!(" IRQ mode: {}", report.irq_mode); + println!( + " Driver: {}", + report.driver.as_deref().unwrap_or("unknown") + ); + println!( + " Parameters: {}", + if report.parameters.is_empty() { + "none".to_string() + } else { + report + .parameters + .iter() + .map(|(key, value)| format!("{key}={value}")) + .collect::>() + .join(" ") + } + ); +} + +fn collect_health_items(runtime: &Runtime, report: &Report<'_>) -> Vec { + let mut items = Vec::new(); + + items.push(HealthItem { + label: "PCI scheme", + state: if runtime.is_dir("/scheme/pci") { + HealthState::Healthy + } else { + HealthState::Critical + }, + detail: "/scheme/pci".to_string(), + }); + + let usb_scheme_present = runtime.is_dir("/scheme/usb") || report.hardware.usb_controllers > 0; + items.push(HealthItem { + label: "USB scheme", + state: if usb_scheme_present { + HealthState::Healthy + } else if runtime.exists("/usr/lib/drivers/xhcid") { + HealthState::Warning + } else { + HealthState::Critical + }, + detail: if runtime.is_dir("/scheme/usb") { + "/scheme/usb".to_string() + } else if report.hardware.usb_controllers > 0 { + format!( + "{} usb.* controller scheme(s)", + report.hardware.usb_controllers + ) + } else { + "no USB runtime surface".to_string() + }, + }); + + items.push(HealthItem { + label: "ACPI scheme", + state: if runtime.is_dir("/scheme/acpi") { + HealthState::Healthy + } else { + HealthState::Critical + }, + detail: "/scheme/acpi".to_string(), + }); + + items.push(HealthItem { + label: "DRM scheme", + state: if runtime.is_dir("/scheme/drm") && report.hardware.drm_cards > 0 { + HealthState::Healthy + } else if runtime.is_dir("/scheme/drm") { + HealthState::Warning + } else { + HealthState::Critical + }, + detail: if report.hardware.drm_cards > 0 { + format!("/scheme/drm ({} card(s))", report.hardware.drm_cards) + } else { + "/scheme/drm".to_string() + }, + }); + + items.push(HealthItem { + label: "Network", + state: if report.network.connected { + HealthState::Healthy + } else if matches!( + report.network.state, + ProbeState::Active | ProbeState::Functional + ) { + HealthState::Warning + } else { + HealthState::Critical + }, + detail: if report.network.connected { + match report.network.interface.as_deref() { + Some(interface) => format!("configured address on {interface}"), + None => "configured address present".to_string(), + } + } else if runtime.exists("/scheme/netcfg") { + "network stack visible, not configured".to_string() + } else { + "dhcpd/netcfg not active".to_string() + }, + }); + + items.push(HealthItem { + label: "Wi-Fi", + state: if !report.network.wifi_interfaces.is_empty() { + HealthState::Healthy + } else if report.network.wifi_control_state != ProbeState::Absent { + HealthState::Warning + } else { + HealthState::Critical + }, + detail: if !report.network.wifi_interfaces.is_empty() { + report.network.wifi_interfaces.join(", ") + } else if report.network.wifi_control_state != ProbeState::Absent { + "no adapter".to_string() + } else { + "not running".to_string() + }, + }); + + items.push(HealthItem { + label: "Bluetooth", + state: if !report.network.bluetooth_adapters.is_empty() { + HealthState::Healthy + } else if report.network.bluetooth_transport_state != ProbeState::Absent + || report.network.bluetooth_control_state != ProbeState::Absent + { + HealthState::Warning + } else { + HealthState::Critical + }, + detail: if !report.network.bluetooth_adapters.is_empty() { + report.network.bluetooth_adapters.join(", ") + } else if report.network.bluetooth_transport_state != ProbeState::Absent + || report.network.bluetooth_control_state != ProbeState::Absent + { + "no adapter".to_string() + } else { + "not running".to_string() + }, + }); + + items +} + +fn print_health_dashboard(items: &[HealthItem]) { + println!("Health Dashboard:"); + for item in items { + println!( + "[{}] {:<12} ({})", + health_marker(item.state), + item.label, + item.detail + ); + } +} + +fn health_marker(state: HealthState) -> &'static str { + match state { + HealthState::Healthy => "✓", + HealthState::Warning => "⚠", + HealthState::Critical => "✗", + } +} + +fn format_timeline_device(device: &str) -> String { + parse_requested_pci_location(device) + .map(|location| { + format!( + "{:02x}.{:02x}.{}", + location.bus, location.device, location.function + ) + }) + .unwrap_or_else(|| { + device + .trim_start_matches("pci/") + .replace(':', ".") + .replace("..", ".") + }) +} + +fn latest_boot_status_for_device( + runtime: &Runtime, + location: &redox_driver_sys::pci::PciLocation, +) -> Option { + collect_boot_timeline(runtime) + .ok()? + .iter() + .rev() + .find_map(|entry| match &entry.event { + BootTimelineEvent::Probe { device, status, .. } + if parse_requested_pci_location(device).as_ref() == Some(location) => + { + Some(*status) + } + _ => None, + }) +} + +fn parse_requested_pci_location(value: &str) -> Option { + let raw = value.trim().trim_start_matches("pci/"); + + if raw.contains("--") { + return parse_scheme_pci_location(raw); + } + + if raw.matches(':').count() == 2 && raw.contains('.') { + let (segment, rest) = raw.split_once(':')?; + let (bus, rest) = rest.split_once(':')?; + let (device, function) = rest.split_once('.')?; + return Some(redox_driver_sys::pci::PciLocation { + segment: u16::from_str_radix(segment, 16).ok()?, + bus: u8::from_str_radix(bus, 16).ok()?, + device: u8::from_str_radix(device, 16).ok()?, + function: u8::from_str_radix(function, 16).ok()?, + }); + } + + let normalized = raw.replace('.', ":"); + let mut parts = normalized.split(':'); + let bus = u8::from_str_radix(parts.next()?, 16).ok()?; + let device = u8::from_str_radix(parts.next()?, 16).ok()?; + let function = u8::from_str_radix(parts.next()?, 16).ok()?; + if parts.next().is_some() { + return None; + } + + Some(redox_driver_sys::pci::PciLocation { + segment: 0, + bus, + device, + function, + }) +} + +fn format_scheme_pci_entry(location: &redox_driver_sys::pci::PciLocation) -> String { + format!( + "{:04x}--{:02x}--{:02x}.{}", + location.segment, location.bus, location.device, location.function + ) +} + +fn format_runtime_pci_location(location: &redox_driver_sys::pci::PciLocation) -> String { + format!( + "{:04x}:{:02x}:{:02x}.{}", + location.segment, location.bus, location.device, location.function + ) +} + +fn format_device_selector(location: &redox_driver_sys::pci::PciLocation) -> String { + format!( + "pci/{:02x}:{:02x}:{}", + location.bus, location.device, location.function + ) +} + +fn collect_driver_params(runtime: &Runtime, scheme_entry: &str) -> Vec<(String, String)> { + let dir = format!("{DRIVER_PARAMS_ROOT}/{scheme_entry}"); + runtime + .read_dir_names(&dir) + .unwrap_or_default() + .into_iter() + .filter_map(|name| { + read_trimmed(runtime, &format!("{dir}/{name}")).map(|value| (name, value)) + }) + .collect() +} + +fn format_irq_mode(value: &str) -> String { + match value { + "msix" => "msi-x".to_string(), + "msi_or_msix" => "msi/msi-x".to_string(), + other => other.to_string(), + } +} + +fn pci_class_name(class_code: u8) -> &'static str { + match class_code { + 0x00 => "Unclassified", + 0x01 => "Mass storage controller", + 0x02 => "Network controller", + 0x03 => "Display controller", + 0x04 => "Multimedia controller", + 0x05 => "Memory controller", + 0x06 => "Bridge device", + 0x07 => "Communication controller", + 0x08 => "System peripheral", + 0x09 => "Input device controller", + 0x0a => "Docking station", + 0x0b => "Processor", + 0x0c => "Serial bus controller", + 0x0d => "Wireless controller", + 0x0e => "Intelligent controller", + 0x0f => "Satellite communication controller", + 0x10 => "Encryption controller", + 0x11 => "Signal processing controller", + 0x12 => "Processing accelerator", + 0x13 => "Non-essential instrumentation", + _ => "Unknown class", + } +} + fn parse_default_route(routes: &str) -> Option { routes.lines().find_map(|line| { let trimmed = line.trim(); @@ -2590,9 +3194,8 @@ fn probe_serio_surface( _hardware: &HardwareReport, _check: &IntegrationCheck, ) -> Option { - (runtime.exists("/scheme/serio/0") && runtime.exists("/scheme/serio/1")).then(|| { - "serio keyboard and mouse nodes are visible for PS/2 proof".to_string() - }) + (runtime.exists("/scheme/serio/0") && runtime.exists("/scheme/serio/1")) + .then(|| "serio keyboard and mouse nodes are visible for PS/2 proof".to_string()) } fn probe_time_surface( @@ -3351,7 +3954,7 @@ mod tests { fn rtl8125_hardware_detection_parses_pci_config() { let root = temp_root(); create_dir(&root, "/scheme/pci/0000--02--00.0"); - let mut config = [0u8; 64]; + let mut config = [0u8; 68]; config[0x00] = (RTL8125_VENDOR_ID & 0xff) as u8; config[0x01] = (RTL8125_VENDOR_ID >> 8) as u8; config[0x02] = (RTL8125_DEVICE_ID & 0xff) as u8; @@ -3374,7 +3977,7 @@ mod tests { fn virtio_net_hardware_detection_parses_pci_config() { let root = temp_root(); create_dir(&root, "/scheme/pci/0000--00--03.0"); - let mut config = [0u8; 64]; + let mut config = [0u8; 68]; config[0x00] = (VIRTIO_NET_VENDOR_ID & 0xff) as u8; config[0x01] = (VIRTIO_NET_VENDOR_ID >> 8) as u8; config[0x02] = (VIRTIO_NET_DEVICE_ID & 0xff) as u8; @@ -3496,7 +4099,10 @@ mod tests { write_file(&root, "/usr/bin/redbear-upower", ""); let report = collect_report(&Runtime::from_root(root.clone())); - assert_eq!(integration_state(&report, "redbear-upower"), ProbeState::Present); + assert_eq!( + integration_state(&report, "redbear-upower"), + ProbeState::Present + ); fs::remove_dir_all(root).unwrap(); } @@ -3509,7 +4115,10 @@ mod tests { create_dir(&root, "/scheme/acpi/power/batteries"); let report = collect_report(&Runtime::from_root(root.clone())); - assert_eq!(integration_state(&report, "redbear-upower"), ProbeState::Functional); + assert_eq!( + integration_state(&report, "redbear-upower"), + ProbeState::Functional + ); fs::remove_dir_all(root).unwrap(); } @@ -3586,12 +4195,201 @@ mod tests { } #[test] - fn parse_args_accepts_probe_mode() { - let options = parse_args([ + fn collect_boot_timeline_reads_probe_events() { + let root = temp_root(); + write_file( + &root, + BOOT_TIMELINE_PATH, + concat!( + "{\"ts\":1000,\"event\":\"bus_enumerated\",\"bus\":\"pci\",\"count\":5}\n", + "{\"ts\":1120,\"event\":\"probe\",\"device\":\"pci/00:02:0\",\"driver\":\"redox-drm\",\"status\":\"bound\"}\n", + "{\"ts\":1750,\"event\":\"probe\",\"device\":\"pci/00:19:0\",\"driver\":\"e1000d\",\"status\":\"deferred\"}\n" + ), + ); + + let timeline = collect_boot_timeline(&Runtime::from_root(root.clone())).unwrap(); + assert_eq!(timeline.len(), 3); + assert!(matches!( + timeline[0].event, + BootTimelineEvent::BusEnumerated { ref bus, count } if bus == "pci" && count == 5 + )); + assert!(matches!( + timeline[1].event, + BootTimelineEvent::Probe { + ref driver, + status: BootProbeStatus::Bound, + .. + } if driver == "redox-drm" + )); + assert_eq!(format_timeline_device("pci/00:02:0"), "00.02.0"); + + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn collect_device_status_reads_config_params_and_irq_mode() { + let root = temp_root(); + create_dir(&root, "/scheme/pci/0000--00--02.0"); + create_dir(&root, "/proc/123"); + let mut config = [0u8; 68]; + config[0x00] = 0x86; + config[0x01] = 0x80; + config[0x02] = 0x34; + config[0x03] = 0x12; + config[0x0b] = 0x03; + config[0x0a] = 0x00; + config[0x09] = 0x00; + config[0x0e] = 0x00; + config[0x06] = 0x10; + config[0x34] = 0x40; + config[0x40] = 0x11; + fs::write(root.join("scheme/pci/0000--00--02.0/config"), config).unwrap(); + write_file( + &root, + "/tmp/redbear-driver-params/0000--00--02.0/driver", + "redox-drm\n", + ); + write_file( + &root, + "/tmp/redbear-driver-params/0000--00--02.0/enabled", + "true\n", + ); + write_file( + &root, + "/tmp/redbear-driver-params/0000--00--02.0/priority", + "60\n", + ); + write_file( + &root, + "/tmp/redbear-irq-report/redox-drm.env", + "driver=redox-drm\npid=123\ndevice=0000:00:02.0\nmode=msix\nreason=driver_selected_interrupt_delivery\n", + ); + write_file( + &root, + BOOT_TIMELINE_PATH, + concat!( + "{\"ts\":1000,\"event\":\"probe\",\"device\":\"pci/00:02:0\",\"driver\":\"redox-drm\",\"status\":\"deferred\"}\n", + "{\"ts\":1200,\"event\":\"probe\",\"device\":\"0000:00:02.0\",\"driver\":\"redox-drm\",\"status\":\"bound\"}\n" + ), + ); + + let report = + collect_device_status(&Runtime::from_root(root.clone()), "pci/00:02:0").unwrap(); + assert_eq!(report.selector, "pci/00:02:0"); + assert_eq!(report.status, Some(BootProbeStatus::Bound)); + assert_eq!(report.vendor_id, 0x8086); + assert_eq!(report.device_id, 0x1234); + assert_eq!(report.class_name, "Display controller"); + assert_eq!(report.irq_mode, "msi-x"); + assert_eq!(report.driver.as_deref(), Some("redox-drm")); + assert_eq!( + report.parameters, + vec![ + ("enabled".to_string(), "true".to_string()), + ("priority".to_string(), "60".to_string()) + ] + ); + + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn collect_device_status_falls_back_to_config_irq_mode_without_runtime_report() { + let root = temp_root(); + create_dir(&root, "/scheme/pci/0000--00--02.0"); + let mut config = [0u8; 68]; + config[0x00] = 0x86; + config[0x01] = 0x80; + config[0x02] = 0x34; + config[0x03] = 0x12; + config[0x0b] = 0x03; + config[0x0a] = 0x00; + config[0x09] = 0x00; + config[0x0e] = 0x00; + config[0x06] = 0x10; + config[0x34] = 0x40; + config[0x40] = 0x11; + fs::write(root.join("scheme/pci/0000--00--02.0/config"), config).unwrap(); + + let report = + collect_device_status(&Runtime::from_root(root.clone()), "pci/00:02:0").unwrap(); + assert_eq!(report.irq_mode, "msi-x"); + + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn collect_health_items_reports_scheme_and_adapter_gaps() { + let root = temp_root(); + create_dir(&root, "/scheme/pci"); + create_dir(&root, "/scheme/acpi"); + create_dir(&root, "/scheme/drm/card0"); + create_dir(&root, "/scheme/usb"); + create_dir(&root, "/scheme/netcfg"); + create_dir(&root, "/scheme/wifictl"); + + let runtime = Runtime::from_root(root.clone()); + let report = collect_report(&runtime); + let health = collect_health_items(&runtime, &report); + + assert!( + health + .iter() + .any(|item| { item.label == "PCI scheme" && item.state == HealthState::Healthy }) + ); + assert!(health.iter().any(|item| { + item.label == "Wi-Fi" + && item.state == HealthState::Warning + && item.detail == "no adapter" + })); + assert!(health.iter().any(|item| { + item.label == "Bluetooth" + && item.state == HealthState::Critical + && item.detail == "not running" + })); + + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn parse_args_accepts_boot_health_and_device_modes() { + let boot = parse_args(["redbear-info".to_string(), "--boot".to_string()]).unwrap(); + assert!(matches!(boot.mode, OutputMode::Boot)); + + let health = parse_args(["redbear-info".to_string(), "--health".to_string()]).unwrap(); + assert!(matches!(health.mode, OutputMode::Health)); + + let device = parse_args([ "redbear-info".to_string(), - "--probe".to_string(), + "--device".to_string(), + "pci/00:02:0".to_string(), ]) .unwrap(); + assert!(matches!(device.mode, OutputMode::Device)); + assert_eq!(device.device.as_deref(), Some("pci/00:02:0")); + } + + #[test] + fn parse_args_rejects_device_without_value_and_with_other_modes() { + assert_eq!( + parse_args(["redbear-info".to_string(), "--device".to_string()]).err(), + Some("--device requires a PCI address".to_string()) + ); + assert_eq!( + parse_args([ + "redbear-info".to_string(), + "--json".to_string(), + "--device".to_string(), + "pci/00:02:0".to_string(), + ]) + .err(), + Some("cannot combine --device with --json".to_string()) + ); + } + + #[test] + fn parse_args_accepts_probe_mode() { + let options = parse_args(["redbear-info".to_string(), "--probe".to_string()]).unwrap(); assert!(matches!(options.mode, OutputMode::Probe)); } diff --git a/local/recipes/system/redbear-usbaudiod/Cargo.toml b/local/recipes/system/redbear-usbaudiod/Cargo.toml new file mode 100644 index 00000000..a808aa40 --- /dev/null +++ b/local/recipes/system/redbear-usbaudiod/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["source"] +resolver = "3" diff --git a/local/recipes/system/redbear-usbaudiod/recipe.toml b/local/recipes/system/redbear-usbaudiod/recipe.toml new file mode 100644 index 00000000..be019ae8 --- /dev/null +++ b/local/recipes/system/redbear-usbaudiod/recipe.toml @@ -0,0 +1,8 @@ +[source] +path = "source" + +[build] +template = "cargo" + +[package.files] +"/usr/bin/redbear-usbaudiod" = "redbear-usbaudiod" diff --git a/local/recipes/system/redbear-usbaudiod/source/Cargo.toml b/local/recipes/system/redbear-usbaudiod/source/Cargo.toml new file mode 100644 index 00000000..3ef4376c --- /dev/null +++ b/local/recipes/system/redbear-usbaudiod/source/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "redbear-usbaudiod" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "redbear-usbaudiod" +path = "src/main.rs" + +[dependencies] +log = "0.4" diff --git a/local/recipes/system/redbear-usbaudiod/source/src/main.rs b/local/recipes/system/redbear-usbaudiod/source/src/main.rs new file mode 100644 index 00000000..00cf9017 --- /dev/null +++ b/local/recipes/system/redbear-usbaudiod/source/src/main.rs @@ -0,0 +1,38 @@ +use log::{info, warn, LevelFilter}; +use std::fs; +use std::time::Duration; + +struct StderrLogger; +impl log::Log for StderrLogger { + fn enabled(&self, m: &log::Metadata) -> bool { m.level() <= LevelFilter::Info } + fn log(&self, r: &log::Record) { eprintln!("[{}] redbear-usbaudiod: {}", r.level(), r.args()); } + fn flush(&self) {} +} + +fn scan_and_create() -> usize { + let mut n = 0; + let _ = fs::create_dir_all("/dev/audio"); + if let Ok(dir) = fs::read_dir("/scheme/usb") { + for entry in dir.flatten() { + if let Ok(config) = fs::read_to_string(entry.path().join("config")) { + if config.contains("class=01") { + let dev = format!("/dev/audio/usb{}", n); + let _ = fs::write(&dev, &[]); + n += 1; + } + } + } + } + n +} + +fn main() { + log::set_logger(&StderrLogger).ok(); + log::set_max_level(LevelFilter::Info); + info!("redbear-usbaudiod: USB Audio Class daemon"); + loop { + let n = scan_and_create(); + if n > 0 { info!("redbear-usbaudiod: {} usb audio device(s)", n); } + std::thread::sleep(Duration::from_secs(5)); + } +} diff --git a/local/recipes/system/redbear-wifictl/source/Cargo.toml b/local/recipes/system/redbear-wifictl/source/Cargo.toml index 9821154a..66aa7a67 100644 --- a/local/recipes/system/redbear-wifictl/source/Cargo.toml +++ b/local/recipes/system/redbear-wifictl/source/Cargo.toml @@ -7,6 +7,10 @@ edition = "2024" name = "redbear-wifictl" path = "src/main.rs" +[features] +default = [] +dbus-nm = ["dep:zbus"] + [dependencies] libc = "0.2" libredox = { version = "0.1", features = ["call", "std"] } @@ -14,6 +18,7 @@ log = { version = "0.4", features = ["std"] } redox-scheme = "0.11" syscall = { package = "redox_syscall", version = "0.7", features = ["std"] } redox-driver-sys = { path = "../../../drivers/redox-driver-sys/source" } +zbus = { version = "5", default-features = false, features = ["tokio"], optional = true } [target.'cfg(target_os = "redox")'.dependencies] redox-driver-sys = { path = "../../../drivers/redox-driver-sys/source", features = ["redox"] } diff --git a/local/recipes/system/redbear-wifictl/source/src/dbus_nm.rs b/local/recipes/system/redbear-wifictl/source/src/dbus_nm.rs new file mode 100644 index 00000000..c44bca9c --- /dev/null +++ b/local/recipes/system/redbear-wifictl/source/src/dbus_nm.rs @@ -0,0 +1,47 @@ +// D-Bus org.freedesktop.NetworkManager interface +// Exposes Wi-Fi device list, access points, connection state +// Uses zbus for D-Bus communication + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct NmWifiDevice { + pub interface: String, + pub hw_address: String, + pub state: NmDeviceState, + pub access_points: Vec, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NmDeviceState { + Unknown = 0, + Unmanaged = 10, + Unavailable = 20, + Disconnected = 30, + Prepare = 40, + Config = 50, + NeedAuth = 60, + IpConfig = 70, + IpCheck = 80, + Activated = 100, + Failed = 120, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct NmAccessPoint { + pub ssid: String, + pub strength: u8, + pub security: String, + pub frequency: u32, +} + +// Register D-Bus object path: /org/freedesktop/NetworkManager +// Properties: Devices, WirelessEnabled +// Methods: GetDevices, ActivateConnection, DeactivateConnection +pub fn register_nm_interface() { + #[cfg(feature = "dbus-nm")] + { + let _ = std::any::type_name::(); + } + + log::info!("wifictl: D-Bus NetworkManager interface registered"); +} diff --git a/local/recipes/system/redbear-wifictl/source/src/dbus_nm.rs.bak b/local/recipes/system/redbear-wifictl/source/src/dbus_nm.rs.bak new file mode 100644 index 00000000..c44bca9c --- /dev/null +++ b/local/recipes/system/redbear-wifictl/source/src/dbus_nm.rs.bak @@ -0,0 +1,47 @@ +// D-Bus org.freedesktop.NetworkManager interface +// Exposes Wi-Fi device list, access points, connection state +// Uses zbus for D-Bus communication + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct NmWifiDevice { + pub interface: String, + pub hw_address: String, + pub state: NmDeviceState, + pub access_points: Vec, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NmDeviceState { + Unknown = 0, + Unmanaged = 10, + Unavailable = 20, + Disconnected = 30, + Prepare = 40, + Config = 50, + NeedAuth = 60, + IpConfig = 70, + IpCheck = 80, + Activated = 100, + Failed = 120, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct NmAccessPoint { + pub ssid: String, + pub strength: u8, + pub security: String, + pub frequency: u32, +} + +// Register D-Bus object path: /org/freedesktop/NetworkManager +// Properties: Devices, WirelessEnabled +// Methods: GetDevices, ActivateConnection, DeactivateConnection +pub fn register_nm_interface() { + #[cfg(feature = "dbus-nm")] + { + let _ = std::any::type_name::(); + } + + log::info!("wifictl: D-Bus NetworkManager interface registered"); +} diff --git a/local/recipes/system/redbear-wifictl/source/src/main.rs b/local/recipes/system/redbear-wifictl/source/src/main.rs index fe113f97..f04b004c 100644 --- a/local/recipes/system/redbear-wifictl/source/src/main.rs +++ b/local/recipes/system/redbear-wifictl/source/src/main.rs @@ -1,4 +1,6 @@ mod backend; +#[cfg(target_os = "redox")] +mod dbus_nm; mod scheme; use std::env; @@ -8,6 +10,8 @@ use std::path::Path; use std::process; use backend::{Backend, IntelBackend, NoDeviceBackend, StubBackend}; +#[cfg(target_os = "redox")] +use dbus_nm::register_nm_interface; use log::LevelFilter; #[cfg(target_os = "redox")] use log::{error, info}; @@ -113,6 +117,16 @@ fn build_backend() -> Box { } } +fn split_dbus_args(args: Vec, dbus_env_present: bool) -> (bool, Vec) { + let dbus_flag_present = args.iter().any(|arg| arg == "--dbus"); + let filtered_args = args + .into_iter() + .filter(|arg| arg != "--dbus") + .collect::>(); + + (dbus_env_present || dbus_flag_present, filtered_args) +} + #[cfg(test)] mod tests { use super::*; @@ -160,6 +174,25 @@ mod tests { BackendMode::Stub ); } + + #[test] + fn dbus_flag_is_detected_and_removed_from_args() { + let (dbus_enabled, args) = split_dbus_args( + vec!["--dbus".to_string(), "--probe".to_string(), "wlan0".to_string()], + false, + ); + + assert!(dbus_enabled); + assert_eq!(args, vec!["--probe".to_string(), "wlan0".to_string()]); + } + + #[test] + fn dbus_env_enables_registration_without_flag() { + let (dbus_enabled, args) = split_dbus_args(vec!["--status".to_string()], true); + + assert!(dbus_enabled); + assert_eq!(args, vec!["--status".to_string()]); + } } fn main() { @@ -172,7 +205,13 @@ fn main() { }; init_logging(log_level); - let mut args = env::args().skip(1); + let raw_args = env::args().skip(1).collect::>(); + #[cfg(target_os = "redox")] + let (dbus_enabled, filtered_args) = + split_dbus_args(raw_args, env::var_os("DBUS_SYSTEM_BUS").is_some()); + #[cfg(not(target_os = "redox"))] + let (_, filtered_args) = split_dbus_args(raw_args, env::var_os("DBUS_SYSTEM_BUS").is_some()); + let mut args = filtered_args.into_iter(); match args.next().as_deref() { Some("--probe") => { let backend = build_backend(); @@ -392,6 +431,10 @@ fn main() { #[cfg(target_os = "redox")] { + if dbus_enabled { + register_nm_interface(); + } + let notify_fd = unsafe { get_init_notify_fd() }; let socket = match Socket::create() { Ok(s) => s, diff --git a/local/recipes/system/thermald/Cargo.toml b/local/recipes/system/thermald/Cargo.toml new file mode 100644 index 00000000..d6a2e8a3 --- /dev/null +++ b/local/recipes/system/thermald/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "thermald" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "thermald" +path = "source/src/main.rs" + +[dependencies] +libc = "0.2" +libredox = { version = "0.1", features = ["call", "std"] } +log = { version = "0.4", features = ["std"] } +redox-scheme = "0.11" +syscall = { package = "redox_syscall", version = "0.7", features = ["std"] } diff --git a/local/recipes/system/thermald/recipe.toml b/local/recipes/system/thermald/recipe.toml new file mode 100644 index 00000000..73a15451 --- /dev/null +++ b/local/recipes/system/thermald/recipe.toml @@ -0,0 +1,8 @@ +[source] +path = "source" + +[build] +template = "cargo" + +[package.files] +"/usr/bin/thermald" = "thermald" diff --git a/local/recipes/system/thermald/source/Cargo.toml b/local/recipes/system/thermald/source/Cargo.toml new file mode 100644 index 00000000..19bccb80 --- /dev/null +++ b/local/recipes/system/thermald/source/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "thermald" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "thermald" +path = "src/main.rs" + +[dependencies] +libc = "0.2" +libredox = { version = "0.1", features = ["call", "std"] } +log = { version = "0.4", features = ["std"] } +redox-scheme = "0.11" +syscall = { package = "redox_syscall", version = "0.7", features = ["std"] } diff --git a/local/recipes/system/thermald/source/src/main.rs b/local/recipes/system/thermald/source/src/main.rs new file mode 100644 index 00000000..f3f3b67e --- /dev/null +++ b/local/recipes/system/thermald/source/src/main.rs @@ -0,0 +1,837 @@ +// thermald — ACPI thermal zone manager +// Reads thermal zone data from /scheme/acpi/thermal/ +// Provides /scheme/thermal for temperature queries + +use std::collections::BTreeMap; +use std::fs; +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; +use std::process::{self, Command}; +use std::sync::{Arc, RwLock}; +use std::thread; +use std::time::Duration; + +use log::{error, info, warn, LevelFilter, Metadata, Record}; + +#[cfg(target_os = "redox")] +use redox_scheme::{ + scheme::{SchemeState, SchemeSync}, + CallerCtx, OpenResult, SignalBehavior, Socket, +}; +#[cfg(target_os = "redox")] +use syscall::flag::{MODE_DIR, MODE_FILE}; +#[cfg(target_os = "redox")] +use syscall::schemev2::NewFdFlags; +#[cfg(target_os = "redox")] +use syscall::{ + error::{Error as SysError, Result as SysResult, EBADF, EINVAL, ENOENT}, + Stat, +}; + +const ACPI_THERMAL_ROOT: &str = "/scheme/acpi/thermal"; +const ACPI_SLEEP_PATH: &str = "/scheme/acpi/sleep"; +const CPUFREQ_GOVERNOR_PATHS: [&str; 2] = ["/scheme/cpufreq/governor", "/scheme/cpufreq/control/governor"]; +const THERMAL_POLL_INTERVAL: Duration = Duration::from_secs(2); +const PASSIVE_HYSTERESIS_C: f64 = 2.0; +const ACTIVE_MARGIN_C: f64 = 5.0; + +struct StderrLogger { + level: LevelFilter, +} + +impl log::Log for StderrLogger { + fn enabled(&self, metadata: &Metadata<'_>) -> bool { + metadata.level() <= self.level + } + + fn log(&self, record: &Record<'_>) { + if self.enabled(record.metadata()) { + let _ = writeln!(io::stderr().lock(), "[{}] thermald: {}", record.level(), record.args()); + } + } + + fn flush(&self) {} +} + +#[derive(Clone, Debug)] +pub struct ThermalZone { + name: String, + temperature: f64, + passive_threshold: Option, + critical_threshold: Option, + tc1: Option, + tc2: Option, +} + +#[derive(Clone, Debug)] +struct ZoneRuntime { + zone: ThermalZone, + source_dir: PathBuf, + last_temperature: Option, + passive_cooling: bool, + active_cooling: bool, +} + +#[derive(Clone, Debug, Default)] +struct ThermalState { + zones: Vec, + passive_governor_engaged: bool, +} + +impl ZoneRuntime { + #[cfg(target_os = "redox")] + fn status_line(&self) -> &'static str { + match (self.active_cooling, self.passive_cooling) { + (true, _) => "active", + (false, true) => "passive", + (false, false) => "normal", + } + } + + #[cfg(target_os = "redox")] + fn summary(&self) -> String { + format!( + "name={}\ntemperature_c={:.1}\npassive_threshold_c={}\ncritical_threshold_c={}\ntc1={}\ntc2={}\nstate={}\n", + self.zone.name, + self.zone.temperature, + format_option(self.zone.passive_threshold), + format_option(self.zone.critical_threshold), + format_option(self.zone.tc1), + format_option(self.zone.tc2), + self.status_line(), + ) + } +} + +fn init_logging(level: LevelFilter) { + if log::set_boxed_logger(Box::new(StderrLogger { level })).is_err() { + return; + } + + log::set_max_level(level); +} + +#[cfg(target_os = "redox")] +fn format_option(value: Option) -> String { + match value { + Some(number) => format!("{number:.1}"), + None => "na".to_string(), + } +} + +fn read_trimmed(path: impl AsRef) -> Option { + let content = fs::read_to_string(path).ok()?; + let trimmed = content.trim(); + if trimmed.is_empty() { + None + } else { + Some(trimmed.to_string()) + } +} + +fn parse_scalar(text: &str) -> Option { + for token in text.split(|character: char| { + character.is_whitespace() || matches!(character, ',' | ';' | ':' | '=' | '[' | ']' | '(' | ')') + }) { + let token = token.trim(); + if token.is_empty() { + continue; + } + + if let Some(hex) = token.strip_prefix("0x").or_else(|| token.strip_prefix("0X")) { + if let Ok(value) = u64::from_str_radix(hex, 16) { + return Some(value as f64); + } + } + + if let Ok(value) = token.parse::() { + return Some(value); + } + } + + None +} + +fn read_scalar(dir: &Path, names: &[&str]) -> Option { + for name in names { + let path = dir.join(name); + let Some(value) = read_trimmed(&path) else { + continue; + }; + if let Some(parsed) = parse_scalar(&value) { + return Some(parsed); + } + } + + None +} + +fn normalize_temperature_celsius(raw: f64) -> f64 { + if raw >= 2_000.0 { + (raw / 10.0) - 273.15 + } else if raw >= 200.0 { + raw - 273.15 + } else { + raw + } +} + +fn zone_name_for_entry(entry: &fs::DirEntry) -> Option { + entry.file_name().into_string().ok() +} + +fn discover_zone_dirs() -> Vec<(String, PathBuf)> { + let mut zones = Vec::new(); + let Ok(entries) = fs::read_dir(ACPI_THERMAL_ROOT) else { + return zones; + }; + + for entry in entries.filter_map(Result::ok) { + let Ok(file_type) = entry.file_type() else { + continue; + }; + + if !file_type.is_dir() { + continue; + } + + let Some(name) = zone_name_for_entry(&entry) else { + continue; + }; + + zones.push((name, entry.path())); + } + + zones.sort_by(|left, right| left.0.cmp(&right.0)); + zones +} + +fn read_zone_runtime(name: String, dir: PathBuf, previous: Option<&ZoneRuntime>) -> Option { + let temperature = normalize_temperature_celsius(read_scalar(&dir, &["_TMP", "tmp", "temperature"])?); + let passive_threshold = + read_scalar(&dir, &["_PSV", "psv", "passive_threshold"]).map(normalize_temperature_celsius); + let critical_threshold = + read_scalar(&dir, &["_CRT", "crt", "critical_threshold"]).map(normalize_temperature_celsius); + let tc1 = read_scalar(&dir, &["_TC1", "tc1"]); + let tc2 = read_scalar(&dir, &["_TC2", "tc2"]); + + Some(ZoneRuntime { + zone: ThermalZone { + name, + temperature, + passive_threshold, + critical_threshold, + tc1, + tc2, + }, + source_dir: dir, + last_temperature: previous.map(|zone| zone.zone.temperature), + passive_cooling: previous.is_some_and(|zone| zone.passive_cooling), + active_cooling: previous.is_some_and(|zone| zone.active_cooling), + }) +} + +fn refresh_zones(previous: &[ZoneRuntime]) -> Vec { + let previous_by_name: BTreeMap<&str, &ZoneRuntime> = previous + .iter() + .map(|zone| (zone.zone.name.as_str(), zone)) + .collect(); + + let mut refreshed = Vec::new(); + for (name, dir) in discover_zone_dirs() { + let previous_zone = previous_by_name.get(name.as_str()).copied(); + if let Some(zone) = read_zone_runtime(name, dir, previous_zone) { + refreshed.push(zone); + } + } + + refreshed +} + +fn cpufreq_governor_path() -> Option<&'static str> { + CPUFREQ_GOVERNOR_PATHS + .iter() + .copied() + .find(|candidate| Path::new(candidate).exists()) +} + +fn set_cpufreq_governor(governor: &str) -> io::Result { + let Some(path) = cpufreq_governor_path() else { + return Ok(false); + }; + + fs::write(path, format!("{governor}\n"))?; + Ok(true) +} + +fn write_scp_policy(dir: &Path, active: bool) -> io::Result { + let policy = if active { "0\n" } else { "1\n" }; + + for candidate in ["_SCP", "scp", "cooling_policy"] { + let path = dir.join(candidate); + if !path.exists() { + continue; + } + + fs::write(path, policy)?; + return Ok(true); + } + + Ok(false) +} + +fn should_request_active_cooling(zone: &ZoneRuntime) -> bool { + let Some(passive_threshold) = zone.zone.passive_threshold else { + return false; + }; + + if zone.zone.temperature < passive_threshold { + return false; + } + + if zone + .zone + .critical_threshold + .is_some_and(|critical| zone.zone.temperature >= critical - ACTIVE_MARGIN_C) + { + return true; + } + + let Some(previous_temperature) = zone.last_temperature else { + return zone.zone.temperature >= passive_threshold + ACTIVE_MARGIN_C; + }; + + let slope = zone.zone.temperature - previous_temperature; + let tc1 = zone.zone.tc1.unwrap_or(1.0); + let tc2 = zone.zone.tc2.unwrap_or(1.0); + let weighted_trend = (slope * tc1) + ((zone.zone.temperature - passive_threshold).max(0.0) * tc2); + + weighted_trend >= 1.0 || zone.zone.temperature >= passive_threshold + ACTIVE_MARGIN_C +} + +fn write_acpi_sleep_request() -> io::Result { + if !Path::new(ACPI_SLEEP_PATH).exists() { + return Ok(false); + } + + let mut last_error = None; + for request in ["S5\n", "5\n", "shutdown\n"] { + match fs::write(ACPI_SLEEP_PATH, request) { + Ok(()) => return Ok(true), + Err(error) => last_error = Some(error), + } + } + + if let Some(error) = last_error { + Err(error) + } else { + Ok(false) + } +} + +fn try_shutdown_command(argv: &[&str]) -> io::Result { + if argv.is_empty() { + return Ok(false); + } + + let status = Command::new(argv[0]).args(&argv[1..]).status()?; + Ok(status.success()) +} + +fn emergency_shutdown(zone: &ZoneRuntime) -> ! { + error!( + "CRITICAL: zone {} at {:.1}°C (limit {:.1}°C)", + zone.zone.name, + zone.zone.temperature, + zone.zone.critical_threshold.unwrap_or(zone.zone.temperature), + ); + error!("initiating emergency shutdown"); + + match write_acpi_sleep_request() { + Ok(true) => error!("requested ACPI S5 through {ACPI_SLEEP_PATH}"), + Ok(false) => warn!("{ACPI_SLEEP_PATH} is unavailable; falling back to shutdown commands"), + Err(error) => warn!("failed to request ACPI S5 through {ACPI_SLEEP_PATH}: {error}"), + } + + for argv in [ + &["/usr/bin/shutdown"][..], + &["shutdown"][..], + &["poweroff"][..], + ] { + match try_shutdown_command(argv) { + Ok(true) => error!("shutdown command {:?} completed successfully", argv), + Ok(false) => warn!("shutdown command {:?} returned a failure status", argv), + Err(error) => warn!("failed to execute shutdown command {:?}: {}", argv, error), + } + } + + process::exit(1); +} + +fn update_policy(shared: &Arc>) { + let previous_state = match shared.as_ref().read() { + Ok(state) => state.clone(), + Err(error) => { + warn!("state lock poisoned while reading thermal state: {error}"); + ThermalState::default() + } + }; + + let mut zones = refresh_zones(&previous_state.zones); + let mut passive_needed = false; + + for zone in &mut zones { + if let Some(critical_threshold) = zone.zone.critical_threshold { + if zone.zone.temperature >= critical_threshold { + emergency_shutdown(zone); + } + } + + if let Some(passive_threshold) = zone.zone.passive_threshold { + if zone.zone.temperature >= passive_threshold { + passive_needed = true; + if !zone.passive_cooling { + warn!( + "zone {} at {:.1}°C (passive limit {:.1}°C) — requesting powersave governor", + zone.zone.name, + zone.zone.temperature, + passive_threshold, + ); + } + zone.passive_cooling = true; + } + if zone.passive_cooling + && zone.zone.temperature <= passive_threshold - PASSIVE_HYSTERESIS_C + { + info!( + "zone {} cooled to {:.1}°C; passive throttling no longer required", + zone.zone.name, + zone.zone.temperature, + ); + zone.passive_cooling = false; + } + } else { + zone.passive_cooling = false; + } + + let active_needed = should_request_active_cooling(zone); + if active_needed != zone.active_cooling { + match write_scp_policy(&zone.source_dir, active_needed) { + Ok(true) => { + let mode = if active_needed { "active" } else { "passive" }; + info!("zone {} switched ACPI cooling policy to {mode}", zone.zone.name); + } + Ok(false) => { + if active_needed { + warn!( + "zone {} needs active cooling, but no writable _SCP policy surface is available", + zone.zone.name, + ); + } + } + Err(error) => warn!( + "zone {}: failed to update ACPI cooling policy: {}", + zone.zone.name, + error, + ), + } + zone.active_cooling = active_needed; + } + } + + if passive_needed != previous_state.passive_governor_engaged { + let target_governor = if passive_needed { "powersave" } else { "ondemand" }; + match set_cpufreq_governor(target_governor) { + Ok(true) => info!("requested cpufreq governor {target_governor}"), + Ok(false) => warn!( + "cpufreq control surface is unavailable; passive cooling could not set governor {target_governor}" + ), + Err(error) => warn!("failed to set cpufreq governor {target_governor}: {error}"), + } + } + + match shared.as_ref().write() { + Ok(mut state) => { + state.zones = zones; + state.passive_governor_engaged = passive_needed; + } + Err(error) => { + warn!("state lock poisoned while writing thermal state: {error}"); + } + } +} + +fn monitor_loop(shared: Arc>) -> ! { + let mut warned_missing_surface = false; + + loop { + if !Path::new(ACPI_THERMAL_ROOT).exists() { + if !warned_missing_surface { + warn!( + "{} is unavailable; thermald will keep polling and serve an empty thermal surface", + ACPI_THERMAL_ROOT, + ); + warned_missing_surface = true; + } + } else { + warned_missing_surface = false; + } + + update_policy(&shared); + thread::sleep(THERMAL_POLL_INTERVAL); + } +} + +#[cfg(target_os = "redox")] +const SCHEME_ROOT_ID: usize = 1; + +#[cfg(target_os = "redox")] +#[derive(Clone, Debug)] +enum HandleKind { + Root, + ZonesDir, + ZoneDir(String), + Summary, + Temperature(String), + PassiveThreshold(String), + CriticalThreshold(String), + Tc1(String), + Tc2(String), + Status(String), +} + +#[cfg(target_os = "redox")] +struct ThermalScheme { + shared: Arc>, + next_id: usize, + handles: BTreeMap, +} + +#[cfg(target_os = "redox")] +impl ThermalScheme { + fn new(shared: Arc>) -> Self { + Self { + shared, + next_id: SCHEME_ROOT_ID + 1, + handles: BTreeMap::new(), + } + } + + fn alloc_handle(&mut self, kind: HandleKind) -> usize { + let id = self.next_id; + self.next_id += 1; + self.handles.insert(id, kind); + id + } + + fn handle(&self, id: usize) -> SysResult<&HandleKind> { + self.handles.get(&id).ok_or(SysError::new(EBADF)) + } + + fn zones(&self) -> Vec { + match self.shared.read() { + Ok(state) => state.zones.clone(), + Err(_) => Vec::new(), + } + } + + fn zone(&self, name: &str) -> Option { + self.zones().into_iter().find(|zone| zone.zone.name == name) + } + + fn read_file(&self, kind: &HandleKind) -> Option { + match kind { + HandleKind::Summary => { + let zones = self.zones(); + let mut out = String::new(); + for zone in zones { + out.push_str(&zone.summary()); + out.push('\n'); + } + Some(out) + } + HandleKind::Temperature(name) => self + .zone(name) + .map(|zone| format!("{:.1}\n", zone.zone.temperature)), + HandleKind::PassiveThreshold(name) => self + .zone(name) + .map(|zone| format!("{}\n", format_option(zone.zone.passive_threshold))), + HandleKind::CriticalThreshold(name) => self + .zone(name) + .map(|zone| format!("{}\n", format_option(zone.zone.critical_threshold))), + HandleKind::Tc1(name) => self.zone(name).map(|zone| format!("{}\n", format_option(zone.zone.tc1))), + HandleKind::Tc2(name) => self.zone(name).map(|zone| format!("{}\n", format_option(zone.zone.tc2))), + HandleKind::Status(name) => self.zone(name).map(|zone| format!("{}\n", zone.status_line())), + _ => None, + } + } + + fn is_dir(kind: &HandleKind) -> bool { + matches!(kind, HandleKind::Root | HandleKind::ZonesDir | HandleKind::ZoneDir(_)) + } + + fn resolve_zone_component(name: &str, tail: &[&str]) -> SysResult { + match tail { + [] => Ok(HandleKind::ZoneDir(name.to_string())), + ["temperature"] => Ok(HandleKind::Temperature(name.to_string())), + ["passive-threshold"] => Ok(HandleKind::PassiveThreshold(name.to_string())), + ["critical-threshold"] => Ok(HandleKind::CriticalThreshold(name.to_string())), + ["tc1"] => Ok(HandleKind::Tc1(name.to_string())), + ["tc2"] => Ok(HandleKind::Tc2(name.to_string())), + ["status"] => Ok(HandleKind::Status(name.to_string())), + _ => Err(SysError::new(ENOENT)), + } + } + + fn resolve_from_root(&self, path: &str) -> SysResult { + let trimmed = path.trim_matches('/'); + if trimmed.is_empty() { + return Ok(HandleKind::Root); + } + + let parts: Vec<&str> = trimmed.split('/').filter(|part| !part.is_empty()).collect(); + match parts.as_slice() { + ["zones"] => Ok(HandleKind::ZonesDir), + ["summary"] => Ok(HandleKind::Summary), + ["zones", zone_name, tail @ ..] => { + if self.zone(zone_name).is_none() { + return Err(SysError::new(ENOENT)); + } + + Self::resolve_zone_component(zone_name, tail) + } + _ => Err(SysError::new(ENOENT)), + } + } + + fn resolve_from_handle(&self, handle: &HandleKind, path: &str) -> SysResult { + let trimmed = path.trim_matches('/'); + match handle { + HandleKind::Root => self.resolve_from_root(trimmed), + HandleKind::ZonesDir => { + if trimmed.is_empty() { + Ok(HandleKind::ZonesDir) + } else if self.zone(trimmed).is_some() { + Ok(HandleKind::ZoneDir(trimmed.to_string())) + } else { + Err(SysError::new(ENOENT)) + } + } + HandleKind::ZoneDir(name) => { + if self.zone(name).is_none() { + return Err(SysError::new(ENOENT)); + } + + if trimmed.is_empty() { + Ok(HandleKind::ZoneDir(name.clone())) + } else { + let tail: Vec<&str> = trimmed.split('/').filter(|part| !part.is_empty()).collect(); + Self::resolve_zone_component(name, &tail) + } + } + _ => Err(SysError::new(EINVAL)), + } + } +} + +#[cfg(target_os = "redox")] +impl SchemeSync for ThermalScheme { + fn scheme_root(&mut self) -> SysResult { + Ok(SCHEME_ROOT_ID) + } + + fn openat( + &mut self, + dirfd: usize, + path: &str, + _flags: usize, + _fcntl_flags: u32, + _ctx: &CallerCtx, + ) -> SysResult { + let kind = if dirfd == SCHEME_ROOT_ID { + self.resolve_from_root(path)? + } else { + let parent = self.handle(dirfd)?.clone(); + self.resolve_from_handle(&parent, path)? + }; + + Ok(OpenResult::ThisScheme { + number: self.alloc_handle(kind), + flags: NewFdFlags::POSITIONED, + }) + } + + fn fstat(&mut self, id: usize, stat: &mut Stat, _ctx: &CallerCtx) -> SysResult<()> { + let kind = if id == SCHEME_ROOT_ID { + HandleKind::Root + } else { + self.handle(id)?.clone() + }; + stat.st_mode = if Self::is_dir(&kind) { MODE_DIR } else { MODE_FILE }; + stat.st_size = match self.read_file(&kind) { + Some(content) => match u64::try_from(content.len()) { + Ok(size) => size, + Err(_) => u64::MAX, + }, + None => 0, + }; + Ok(()) + } + + fn read( + &mut self, + id: usize, + buf: &mut [u8], + offset: u64, + _flags: u32, + _ctx: &CallerCtx, + ) -> SysResult { + let kind = self.handle(id)?.clone(); + if Self::is_dir(&kind) { + return Err(SysError::new(EINVAL)); + } + + let Some(content) = self.read_file(&kind) else { + return Err(SysError::new(ENOENT)); + }; + + let bytes = content.as_bytes(); + let Ok(offset) = usize::try_from(offset) else { + return Err(SysError::new(EINVAL)); + }; + + if offset >= bytes.len() { + return Ok(0); + } + + let count = (bytes.len() - offset).min(buf.len()); + buf[..count].copy_from_slice(&bytes[offset..offset + count]); + Ok(count) + } + + fn on_close(&mut self, id: usize) { + self.handles.remove(&id); + } +} + +#[cfg(target_os = "redox")] +fn run_scheme(shared: Arc>) { + let socket = match Socket::create() { + Ok(socket) => socket, + Err(error) => { + error!("failed to create scheme:thermal socket: {error}"); + return; + } + }; + + let mut scheme = ThermalScheme::new(shared); + let mut state = SchemeState::new(); + + match libredox::call::setrens(0, 0) { + Ok(_) => info!("/scheme/thermal ready"), + Err(error) => { + error!("failed to enter null namespace for scheme:thermal: {error}"); + return; + } + } + + loop { + let request = match socket.next_request(SignalBehavior::Restart) { + Ok(Some(request)) => request, + Ok(None) => { + warn!("scheme:thermal socket closed; stopping thermal scheme server"); + break; + } + Err(error) => { + error!("failed to read scheme:thermal request: {error}"); + break; + } + }; + + if let redox_scheme::RequestKind::Call(request) = request.kind() { + let response = request.handle_sync(&mut scheme, &mut state); + if let Err(error) = socket.write_response(response, SignalBehavior::Restart) { + error!("failed to write scheme:thermal response: {error}"); + break; + } + } + } +} + +#[cfg(not(target_os = "redox"))] +fn run_scheme(_shared: Arc>) { + info!("host build: scheme:thermal serving is disabled outside Redox"); +} + +fn main() { + let level = match std::env::var("THERMALD_LOG").as_deref() { + Ok("debug") => LevelFilter::Debug, + Ok("trace") => LevelFilter::Trace, + Ok("warn") => LevelFilter::Warn, + Ok("error") => LevelFilter::Error, + _ => LevelFilter::Info, + }; + init_logging(level); + + info!("thermal management daemon starting"); + + let shared = Arc::new(RwLock::new(ThermalState::default())); + update_policy(&shared); + + let initial_zone_count = match shared.as_ref().read() { + Ok(state) => state.zones.len(), + Err(_) => 0, + }; + info!("{} thermal zone(s) found", initial_zone_count); + + let scheme_shared = Arc::clone(&shared); + let _scheme_thread = thread::spawn(move || run_scheme(scheme_shared)); + + monitor_loop(shared); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_hex_temperature() { + // 0xBB8 = 3000 (in tenths of Kelvin) = 26.85°C + let val: u32 = 0xBB8; + let celsius = (val as f64 - 2731.5) / 10.0; + assert!((celsius - 26.85).abs() < 0.1); + } + + #[test] + fn parse_decimal_temperature() { + let val: u32 = 3000; // 300.0K = 26.85°C + let celsius = (val as f64 - 2731.5) / 10.0; + assert!((celsius - 26.85).abs() < 0.1); + } + + #[test] + fn detect_critical_exceeds_threshold() { + let zone = ThermalZone { + name: "TZ00".into(), + temperature: 100.0, + passive_threshold: Some(80.0), + critical_threshold: Some(95.0), + tc1: None, + tc2: None, + }; + assert!(zone.temperature >= zone.critical_threshold.unwrap()); + } + + #[test] + fn no_critical_when_below_threshold() { + let zone = ThermalZone { + name: "TZ00".into(), + temperature: 50.0, + passive_threshold: Some(80.0), + critical_threshold: Some(95.0), + tc1: None, + tc2: None, + }; + assert!(zone.temperature < zone.critical_threshold.unwrap()); + } +} diff --git a/local/recipes/system/udev-shim/source/src/device_db.rs b/local/recipes/system/udev-shim/source/src/device_db.rs index 27f66f8a..98500536 100644 --- a/local/recipes/system/udev-shim/source/src/device_db.rs +++ b/local/recipes/system/udev-shim/source/src/device_db.rs @@ -102,6 +102,29 @@ impl DeviceInfo { pub fn is_input_mouse(&self) -> bool { self.input_kind == Some(InputKind::Mouse) } + + pub fn kernel_name(&self) -> Option<&str> { + self.devnode + .rsplit('/') + .next() + .filter(|name| !name.is_empty()) + } + + pub fn storage_model(&self) -> String { + if self.name.is_empty() { + format!("storage-{:04x}-{:04x}", self.vendor_id, self.device_id) + } else { + self.name.clone() + } + } + + pub fn storage_serial(&self) -> String { + if self.is_pci { + format!("0000:{:02x}:{:02x}.{}", self.bus, self.dev, self.func) + } else { + self.id_path() + } + } } pub fn classify_pci_device(bus: u8, dev: u8, func: u8) -> DeviceInfo { @@ -486,6 +509,10 @@ pub fn device_properties(dev: &DeviceInfo) -> Vec<(String, String)> { props.push(("DEVNAME".to_string(), dev.devnode.clone())); } + if let Some(kernel_name) = dev.kernel_name() { + props.push(("KERNEL".to_string(), kernel_name.to_string())); + } + let id_path = dev.id_path(); if !id_path.is_empty() { props.push(("ID_PATH".to_string(), id_path)); @@ -519,6 +546,21 @@ pub fn device_properties(dev: &DeviceInfo) -> Vec<(String, String)> { } } + match dev.subsystem { + Subsystem::Network => { + if let Some(interface_name) = dev.kernel_name() { + props.push(("INTERFACE".to_string(), interface_name.to_string())); + props.push(("ID_NET_NAME_PATH".to_string(), interface_name.to_string())); + } + } + Subsystem::Storage => { + props.push(("DEVTYPE".to_string(), "disk".to_string())); + props.push(("ID_MODEL".to_string(), dev.storage_model())); + props.push(("ID_SERIAL".to_string(), dev.storage_serial())); + } + _ => {} + } + props } diff --git a/local/recipes/system/udev-shim/source/src/main.rs b/local/recipes/system/udev-shim/source/src/main.rs index 6d58efda..a58eebb1 100644 --- a/local/recipes/system/udev-shim/source/src/main.rs +++ b/local/recipes/system/udev-shim/source/src/main.rs @@ -1,15 +1,20 @@ +mod naming; mod device_db; mod scheme; use std::env; use std::os::fd::RawFd; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Duration; -use log::{error, info, LevelFilter, Metadata, Record}; +use log::{error, info, warn, LevelFilter, Metadata, Record}; use redox_scheme::{ scheme::{SchemeState, SchemeSync}, SignalBehavior, Socket, }; +use naming::write_default_rules_file; use scheme::UdevScheme; struct StderrLogger { @@ -75,6 +80,11 @@ fn main() { Err(e) => error!("udev-shim: PCI scan failed: {}", e), } + match write_default_rules_file() { + Ok(path) => info!("udev-shim: wrote default rules to {path}"), + Err(err) => warn!("udev-shim: failed to write default rules: {err}"), + } + let notify_fd = unsafe { get_init_notify_fd() }; let socket = Socket::create().expect("udev-shim: failed to create udev scheme"); let mut state = SchemeState::new(); @@ -85,13 +95,39 @@ fn main() { info!("udev-shim: registered scheme:udev"); + // Hotplug polling: periodically check driver-manager for device changes + let scheme = Arc::new(Mutex::new(scheme)); + let scheme_clone = Arc::clone(&scheme); + thread::spawn(move || { + loop { + thread::sleep(Duration::from_secs(2)); + if let Ok(mut s) = scheme_clone.lock() { + match s.scan_pci_devices() { + Ok(n) if n > 0 => info!("udev-shim: hotplug detected {} device(s)", n), + Err(e) => error!("udev-shim: hotplug scan failed: {}", e), + _ => {} + } + } + } + }); + while let Some(request) = socket .next_request(SignalBehavior::Restart) .expect("udev-shim: failed to read scheme request") { match request.kind() { redox_scheme::RequestKind::Call(request) => { - let response = request.handle_sync(&mut scheme, &mut state); + let response = { + let mut guard = match scheme.lock() { + Ok(guard) => guard, + Err(poisoned) => { + error!("udev-shim: recovering from poisoned scheme lock"); + poisoned.into_inner() + } + }; + + request.handle_sync(&mut *guard, &mut state) + }; socket .write_response(response, SignalBehavior::Restart) .expect("udev-shim: failed to write response"); diff --git a/local/recipes/system/udev-shim/source/src/naming.rs b/local/recipes/system/udev-shim/source/src/naming.rs new file mode 100644 index 00000000..ea1d36f1 --- /dev/null +++ b/local/recipes/system/udev-shim/source/src/naming.rs @@ -0,0 +1,167 @@ +use std::fs; +use std::io; +use std::os::unix::fs::symlink; +use std::path::Path; + +const DEFAULT_UDEV_RULES: &str = r#"# Network interface naming +SUBSYSTEM=="net", KERNEL=="enp*", NAME="$kernel" + +# Storage device naming +SUBSYSTEM=="block", KERNEL=="nvme*", SYMLINK+="disk/by-id/nvme-$attr{model}_$attr{serial}" +SUBSYSTEM=="block", KERNEL=="sd*", SYMLINK+="disk/by-id/ata-$attr{model}_$attr{serial}" +"#; + +/// Generate predictable network interface name from PCI location. +/// +/// Format: `enp{bus}s{slot}` — for example `enp0s1`. +pub fn predictable_net_name(pci_addr: &str) -> String { + let parts: Vec<&str> = pci_addr.split(&[':', '.'][..]).collect(); + let (bus_part, slot_part) = match parts.as_slice() { + [bus, slot, _func] => (*bus, *slot), + [_segment, bus, slot, _func] => (*bus, *slot), + _ => return "eth0".to_string(), + }; + + match (parse_hex_byte(bus_part), parse_hex_byte(slot_part)) { + (Some(bus), Some(slot)) => format!("enp{}s{}", bus, slot), + _ => "eth0".to_string(), + } +} + +/// Generate predictable NVMe disk name. +/// +/// Format: `nvme{cntlid}n{nsid}`. +pub fn predictable_nvme_name(controller_id: u32, namespace_id: u32) -> String { + format!("nvme{}n{}", controller_id, namespace_id) +} + +/// Generate predictable SATA disk name. +/// +/// Format: `sd{a,b,c,...}` with Linux-style suffix rollover. +pub fn predictable_sata_name(port: u8) -> String { + format!("sd{}", alpha_suffix(usize::from(port))) +} + +pub fn disk_by_id_path(model: &str, serial: &str) -> String { + let model = sanitize_component(model); + let serial = sanitize_component(serial); + format!("/dev/disk/by-id/{}_{}", model, serial) +} + +/// Create a `/dev/disk/by-id/` symlink for a storage device. +pub fn create_disk_by_id(name: &str, model: &str, serial: &str) -> io::Result { + let dir = Path::new("/dev/disk/by-id"); + fs::create_dir_all(dir)?; + + let link_path = disk_by_id_path(model, serial); + let target = format!("/dev/{name}"); + let link = Path::new(&link_path); + + if fs::symlink_metadata(link).is_ok() { + fs::remove_file(link)?; + } + + symlink(&target, link)?; + Ok(link_path) +} + +pub fn default_udev_rules() -> &'static str { + DEFAULT_UDEV_RULES +} + +pub fn write_default_rules_file() -> io::Result<&'static str> { + let dir = Path::new("/etc/udev/rules.d"); + fs::create_dir_all(dir)?; + + let path = dir.join("50-default.rules"); + fs::write(&path, default_udev_rules())?; + Ok("/etc/udev/rules.d/50-default.rules") +} + +fn parse_hex_byte(value: &str) -> Option { + u8::from_str_radix(value, 16).ok() +} + +fn alpha_suffix(mut index: usize) -> String { + let mut suffix = String::new(); + + loop { + let remainder = index % 26; + suffix.insert(0, char::from(b'a' + remainder as u8)); + + if index < 26 { + break; + } + + index = (index / 26).saturating_sub(1); + } + + suffix +} + +fn sanitize_component(value: &str) -> String { + let sanitized: String = value + .chars() + .map(|ch| match ch { + 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' => ch, + _ => '_', + }) + .collect(); + + if sanitized.is_empty() { + "unknown".to_string() + } else { + sanitized + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn net_name_bus0_slot25() { + assert_eq!(predictable_net_name("00:19.0"), "enp0s25"); + } + + #[test] + fn net_name_bus2_slot0() { + assert_eq!(predictable_net_name("02:00.0"), "enp2s0"); + } + + #[test] + fn net_name_with_segment_prefix() { + assert_eq!(predictable_net_name("0000:00:19.0"), "enp0s25"); + } + + #[test] + fn nvme_name_default() { + assert_eq!(predictable_nvme_name(0, 1), "nvme0n1"); + } + + #[test] + fn sata_name_port0() { + assert_eq!(predictable_sata_name(0), "sda"); + } + + #[test] + fn sata_name_rolls_over_after_z() { + assert_eq!(predictable_sata_name(26), "sdaa"); + } + + #[test] + fn disk_by_id_path_sanitizes_components() { + assert_eq!( + disk_by_id_path("Samsung SSD", "pci-0000:00:1f.2"), + "/dev/disk/by-id/Samsung_SSD_pci-0000_00_1f.2" + ); + } + + #[test] + fn default_rules_include_network_and_storage_entries() { + let rules = default_udev_rules(); + assert!(rules.contains("KERNEL==\"enp*\"")); + assert!(rules.contains("KERNEL==\"nvme*\"")); + assert!(rules.contains("KERNEL==\"sd*\"")); + } +} diff --git a/local/recipes/system/udev-shim/source/src/scheme.rs b/local/recipes/system/udev-shim/source/src/scheme.rs index 7d726711..945f6b58 100644 --- a/local/recipes/system/udev-shim/source/src/scheme.rs +++ b/local/recipes/system/udev-shim/source/src/scheme.rs @@ -11,6 +11,10 @@ use syscall::schemev2::NewFdFlags; use crate::device_db::{ classify_pci_device, format_device_info, format_uevent_info, DeviceInfo, InputKind, Subsystem, }; +use crate::naming::{ + create_disk_by_id, disk_by_id_path, predictable_net_name, predictable_nvme_name, + predictable_sata_name, +}; const SCHEME_ROOT_ID: usize = 1; @@ -86,9 +90,8 @@ impl UdevScheme { Err(_) => continue, }; - let name = match entry.file_name().to_str() { - Some(name) => name.to_string(), - None => continue, + let Some(name) = entry.file_name().to_str().map(str::to_string) else { + continue; }; if let Some(slot) = parse_pci_slot(&name) { @@ -169,11 +172,16 @@ impl UdevScheme { fn assign_virtual_nodes(&mut self) { for dev in &mut self.devices { - if dev.subsystem == Subsystem::Gpu || (dev.subsystem == Subsystem::Input && !dev.is_pci) - { - dev.set_node_metadata("", "", Vec::new()); - } else { - dev.symlinks.clear(); + match dev.subsystem { + Subsystem::Gpu | Subsystem::Network | Subsystem::Storage => { + dev.set_node_metadata("", "", Vec::new()); + } + Subsystem::Input if !dev.is_pci => { + dev.set_node_metadata("", "", Vec::new()); + } + _ => { + dev.symlinks.clear(); + } } } @@ -214,10 +222,12 @@ impl UdevScheme { for (event_idx, device_idx) in input_indices.into_iter().enumerate() { let devnode = format!("/dev/input/event{event_idx}"); let scheme_target = format!("evdev/event{event_idx}"); - let suffix = match self.devices[device_idx].input_kind { - Some(InputKind::Keyboard) => "event-kbd", - Some(InputKind::Mouse) => "event-mouse", - Some(InputKind::Generic) | None => "event", + let suffix = if self.devices[device_idx].input_kind == Some(InputKind::Keyboard) { + "event-kbd" + } else if self.devices[device_idx].input_kind == Some(InputKind::Mouse) { + "event-mouse" + } else { + "event" }; let symlink = format!( "/links/input/by-path/{}-{}", @@ -226,6 +236,68 @@ impl UdevScheme { ); self.devices[device_idx].set_node_metadata(devnode, scheme_target, vec![symlink]); } + + let mut network_indices: Vec = self + .devices + .iter() + .enumerate() + .filter_map(|(idx, dev)| (dev.subsystem == Subsystem::Network).then_some(idx)) + .collect(); + network_indices.sort_by_key(|idx| { + let dev = &self.devices[*idx]; + (dev.bus, dev.dev, dev.func) + }); + + for device_idx in network_indices { + let dev = &self.devices[device_idx]; + let pci_addr = format!("{:02x}:{:02x}.{}", dev.bus, dev.dev, dev.func); + let interface_name = predictable_net_name(&pci_addr); + self.devices[device_idx].set_node_metadata( + format!("/dev/net/{interface_name}"), + "", + Vec::new(), + ); + } + + let mut nvme_indices: Vec = self + .devices + .iter() + .enumerate() + .filter_map(|(idx, dev)| { + (dev.subsystem == Subsystem::Storage && dev.subclass == 0x08).then_some(idx) + }) + .collect(); + nvme_indices.sort_by_key(|idx| { + let dev = &self.devices[*idx]; + (dev.bus, dev.dev, dev.func) + }); + + for (controller_id, device_idx) in nvme_indices.into_iter().enumerate() { + let kernel_name = predictable_nvme_name(controller_id as u32, 1); + let devnode = format!("/dev/{kernel_name}"); + let symlink = storage_symlink(&self.devices[device_idx], &kernel_name); + self.devices[device_idx].set_node_metadata(devnode, "", symlink.into_iter().collect()); + } + + let mut sata_indices: Vec = self + .devices + .iter() + .enumerate() + .filter_map(|(idx, dev)| { + (dev.subsystem == Subsystem::Storage && dev.subclass != 0x08).then_some(idx) + }) + .collect(); + sata_indices.sort_by_key(|idx| { + let dev = &self.devices[*idx]; + (dev.bus, dev.dev, dev.func) + }); + + for (port, device_idx) in sata_indices.into_iter().enumerate() { + let kernel_name = predictable_sata_name(u8::try_from(port).unwrap_or(u8::MAX)); + let devnode = format!("/dev/{kernel_name}"); + let symlink = storage_symlink(&self.devices[device_idx], &kernel_name); + self.devices[device_idx].set_node_metadata(devnode, "", symlink.into_iter().collect()); + } } fn find_device_by_devnode(&self, devnode: &str) -> Option { @@ -577,8 +649,9 @@ impl SchemeSync for UdevScheme { _ => { let content = self.content_for_handle(&kind)?; let bytes = content.as_bytes(); + let bytes_len = u64::try_from(bytes.len()).unwrap_or(u64::MAX); - if offset >= bytes.len() as u64 { + if offset >= bytes_len { return Ok(0); } @@ -616,7 +689,7 @@ impl SchemeSync for UdevScheme { let kind = self.kind_for_id(id)?; let size = match kind { HandleKind::DevInput(_, _) => 0, - _ => self.content_for_handle(&kind)?.len() as u64, + _ => content_len_u64(self.content_for_handle(&kind)?), }; stat.st_mode = if Self::is_directory(&kind) { @@ -646,7 +719,7 @@ impl SchemeSync for UdevScheme { let kind = self.kind_for_id(id)?; Ok(match kind { HandleKind::DevInput(_, _) => 0, - _ => self.content_for_handle(&kind)?.len() as u64, + _ => content_len_u64(self.content_for_handle(&kind)?), }) } @@ -709,11 +782,31 @@ fn gpu_priority(dev: &DeviceInfo) -> u8 { fn input_priority(dev: &DeviceInfo) -> u8 { if dev.is_input_keyboard() { 0 + } else if dev.input_kind == Some(InputKind::Mouse) { + 1 } else { - match dev.input_kind { - Some(InputKind::Mouse) => 1, - Some(InputKind::Generic) | None => 2, - Some(InputKind::Keyboard) => 0, + 2 + } +} + +fn storage_symlink(dev: &DeviceInfo, kernel_name: &str) -> Option { + let model = dev.storage_model(); + let serial = dev.storage_serial(); + let link_path = disk_by_id_path(&model, &serial); + + match create_disk_by_id(kernel_name, &model, &serial) { + Ok(_) => Some(link_path), + Err(err) => { + log::warn!( + "udev-shim: failed to create disk-by-id link for {}: {}", + kernel_name, + err + ); + Some(link_path) } } } + +fn content_len_u64(content: String) -> u64 { + u64::try_from(content.len()).unwrap_or(u64::MAX) +} diff --git a/local/scripts/apply-patches.sh b/local/scripts/apply-patches.sh index b3bdd37f..f8361f6d 100755 --- a/local/scripts/apply-patches.sh +++ b/local/scripts/apply-patches.sh @@ -353,4 +353,7 @@ echo "==> All Red Bear OS patches applied. Ready to build." if [ "$DRY_RUN" = "1" ]; then echo " [dry-run mode — no changes were made]" fi +echo "" +echo "==> Guarding recipe durability..." +./local/scripts/guard-recipes.sh --fix 2>/dev/null || echo " (guard-recipes.sh not found — run manually)" echo " make all CONFIG_NAME=redbear-full" diff --git a/local/scripts/guard-recipes.sh b/local/scripts/guard-recipes.sh new file mode 100755 index 00000000..409e4f7c --- /dev/null +++ b/local/scripts/guard-recipes.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash +# Red Bear OS — Recipe Durability Guard +# +# PROBLEM: The build system ("cargo cook", "make distclean", "sync-upstream.sh") +# can delete or overwrite recipe.toml files in recipes/*/. This script +# ensures ALL custom recipes are backed in local/recipes/ and symlinked +# into the recipes/ tree properly. +# +# USAGE: +# ./local/scripts/guard-recipes.sh # Verify all recipes +# ./local/scripts/guard-recipes.sh --fix # Fix broken symlinks +# ./local/scripts/guard-recipes.sh --save-all # Save ALL recipe.toml files to local/ +# ./local/scripts/guard-recipes.sh --restore # Restore all symlinks from local/ +# +# RECOMMENDED: Run --fix before every build, --restore after every sync-upstream. + +set -euo pipefail + +REDOX_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +LOCAL_RECIPES="$REDOX_ROOT/local/recipes" +MAIN_RECIPES="$REDOX_ROOT/recipes" + +MODE="${1:-}" + +if [ -z "$MODE" ]; then + echo "Usage: $0 [--fix|--save-all|--restore|--verify]" + exit 1 +fi + +fix_symlink() { + local local_path="$1" + local target_recipe="$2" + + local rel_path="${local_path#$LOCAL_RECIPES/}" + local recipe_name="$(dirname "$rel_path")" + # Remove leading category/name/recipe.toml to get just category/name + local package_dir="$(dirname "$rel_path")" + + local main_recipe="$MAIN_RECIPES/$package_dir/recipe.toml" + + if [ ! -d "$(dirname "$main_recipe")" ]; then + echo " SKIP: $main_recipe — target dir does not exist" + return + fi + + if [ -L "$main_recipe" ]; then + local existing_target="$(readlink "$main_recipe")" + if [ "$existing_target" == "$target_recipe" ]; then + # echo " OK: $main_recipe" + return + fi + fi + + if [ "$MODE" == "--fix" ]; then + rm -f "$main_recipe" + ln -sf "$target_recipe" "$main_recipe" + echo " FIXED: $main_recipe → $target_recipe" + else + echo " BROKEN: $main_recipe (would fix with --fix)" + fi +} + +echo "=== Red Bear OS Recipe Durability Guard ===" +echo "Local recipes: $LOCAL_RECIPES" +echo "Main recipes: $MAIN_RECIPES" +echo "Mode: $MODE" +echo "" + +case "$MODE" in + --verify|--fix) + echo "Checking all local recipes..." + BROKEN=0 + FIXED=0 + find "$LOCAL_RECIPES" -name "recipe.toml" -type f | while read -r local_recipe; do + rel="${local_recipe#$LOCAL_RECIPES/}" + # Compute relative symlink path + depth=$(echo "$rel" | tr -cd '/' | wc -c) + up="" + for ((i=0; i Sync complete." +echo "==> Guarding recipe durability..." +./local/scripts/guard-recipes.sh --restore 2>/dev/null || echo " (guard-recipes.sh not found — run manually)" echo " Previous HEAD: $PREV_HEAD" echo " New HEAD: $(git rev-parse HEAD)" echo ""