feat: atomic patch application, colored init output, XKB bridge, USB HID hardening

Build system (src/cook/fetch.rs):
- Atomic patch application: applies patches to staging directory (cp -al),
  atomically swaps on success, discards on failure — source tree is never
  left in a partially-patched state
- normalize_patch(): strips diff --git/index/new-file-mode headers that the
  build system's patch command does not recognize
- cleanup_workspace_pollution(): removes orphaned recipes/Cargo.toml and
  recipes/Cargo.lock to prevent workspace conflicts
- Added --allow-protected CLI flag to repo binary

Input stack (local/patches/base/P3-*.patch):
- P3-ps2d-led-feedback: PS/2 LED state handling + InputProducer migration
- P3-inputd-keymap-bridge: InputProducer enum, keymap bridge query
- P3-usbhidd-hardening: HID descriptor validation, static lookup table,
  8-button mouse support, transfer retry with exponential backoff
- P3-init-colored-output: ANSI-color coded init daemon output (green OK,
  red FAILED, yellow SKIP/WARN)

XKB bridge (local/recipes/system/redbear-keymapd/source/src/xkb.rs):
- Parses X11 xkb/symbols/* format, maps XKB keycodes to PS/2 scancodes,
  80+ X11 keysym names to Unicode, 4-level key support

Patch governance (local/patches/base/absorbed/README.md):
- Documents consolidation of P0-P3 patches into redox.patch
This commit is contained in:
2026-05-03 08:21:54 +01:00
parent 7b48083a14
commit aca2f2913d
13 changed files with 2920 additions and 40 deletions
@@ -0,0 +1,260 @@
--- a/init/src/color.rs 1970-01-01 00:00:00.000000000 +0000
+++ b/init/src/color.rs 2026-05-03 08:14:33.424932357 +0100
@@ -0,0 +1,27 @@
+pub fn status_ok(msg: &str) {
+ eprintln!("[ OK ] {msg}");
+}
+
+pub fn status_fail(msg: &str) {
+ eprintln!("[ FAILED ] {msg}");
+}
+
+pub fn status_skip(msg: &str) {
+ eprintln!("[ SKIP ] {msg}");
+}
+
+pub fn init_error(msg: &str) {
+ eprintln!("init: {msg}");
+}
+
+pub fn init_warn(msg: &str) {
+ eprintln!("init: {msg}");
+}
+
+pub fn init_info(msg: &str) {
+ eprintln!("init: {msg}");
+}
+
+pub fn init_debug(msg: &str) {
+ eprintln!("init: {msg}");
+}
--- a/init/src/main.rs 2026-05-03 05:35:37.646633516 +0100
+++ b/init/src/main.rs 2026-05-03 08:14:33.426007641 +0100
@@ -8,11 +8,14 @@
use crate::scheduler::Scheduler;
use crate::unit::{UnitId, UnitStore};
+mod color;
mod scheduler;
mod script;
mod service;
mod unit;
+use crate::color::{init_error, init_warn, status_fail, status_ok};
+
fn switch_stdio(stdio: &str) -> io::Result<()> {
let stdin = libredox::Fd::open(stdio, O_RDONLY, 0)?;
let stdout = libredox::Fd::open(stdio, O_WRONLY, 0)?;
@@ -49,11 +52,7 @@
}
fn switch_root(unit_store: &mut UnitStore, config: &mut InitConfig, prefix: &Path, etcdir: &Path) {
- eprintln!(
- "init: switchroot to {} {}",
- prefix.display(),
- etcdir.display()
- );
+ status_ok(&format!("switchroot to {} {}", prefix.display(), etcdir.display()));
config
.envs
@@ -79,10 +78,7 @@
continue;
}
let Some((key, value)) = env.split_once('=') else {
- eprintln!(
- "init: failed to parse env line from {}: {env:?}",
- file.display(),
- );
+ init_warn(&format!("failed to parse env line from {}: {:?}", file.display(), env));
continue;
};
config
@@ -91,23 +87,18 @@
}
}
Err(err) => {
- eprintln!(
- "init: failed to read environment from {}: {err}",
- file.display(),
- );
+ init_error(&format!("failed to read environment from {}: {}", file.display(), err));
}
}
}
}
Err(err) => {
- eprintln!(
- "init: failed to read environments from {}: {err}",
+ init_error(&format!("failed to read environments from {}: {}",
env_dirs
.iter()
.map(|dir| dir.display().to_string())
.collect::<Vec<_>>()
- .join(", ")
- );
+ .join(", "), err));
}
}
}
@@ -129,7 +120,7 @@
.schedule_start_and_report_errors(&mut unit_store, UnitId("00_logd.service".to_owned()));
scheduler.step(&mut unit_store, &mut init_config);
if let Err(err) = switch_stdio("/scheme/log") {
- eprintln!("init: failed to switch stdio to '/scheme/log': {err}");
+ init_error(&format!("failed to switch stdio to '/scheme/log': {}", err));
}
let runtime_target = UnitId("00_runtime.target".to_owned());
@@ -153,15 +144,13 @@
let entries = match config::config_for_dirs(&unit_store.config_dirs) {
Ok(entries) => entries,
Err(err) => {
- eprintln!(
- "init: failed to read configs from {}: {err}",
+ init_error(&format!("failed to read configs from {}: {}",
unit_store
.config_dirs
.iter()
.map(|dir| dir.display().to_string())
.collect::<Vec<_>>()
- .join(", ")
- );
+ .join(", "), err));
return;
}
};
@@ -186,7 +175,7 @@
let mut status = 0;
match libredox::call::waitpid(0, &mut status, 1) {
Ok(_pid) => {}
- Err(err) => eprintln!("init: waitpid error: {err}"),
+ Err(err) => init_error(&format!("waitpid error: {}", err)),
}
}
}
--- a/init/src/scheduler.rs 2026-05-03 05:35:37.539232447 +0100
+++ b/init/src/scheduler.rs 2026-05-03 08:14:33.426815867 +0100
@@ -1,6 +1,7 @@
use std::collections::VecDeque;
use crate::InitConfig;
+use crate::color::{init_debug, init_error, init_info, status_ok, status_skip};
use crate::unit::{Unit, UnitId, UnitKind, UnitStore};
pub struct Scheduler {
@@ -31,7 +32,7 @@
let mut errors = vec![];
self.schedule_start(unit_store, unit_id, &mut errors);
for error in errors {
- eprintln!("init: {error}");
+ init_error(&format!("{}", error));
}
}
@@ -85,31 +86,28 @@
UnitKind::LegacyScript { script } => {
for cmd in script.clone() {
if config.log_debug {
- eprintln!("init: running: {cmd:?}");
+ init_debug(&format!("running: {cmd:?}"));
}
cmd.run(config);
}
}
UnitKind::Service { service } => {
+ let desc = unit.info.description.as_ref().unwrap_or(&unit.id.0);
if config.skip_cmd.contains(&service.cmd) {
- eprintln!("Skipping '{} {}'", service.cmd, service.args.join(" "));
+ status_skip(&format!("Skipping {} ({})", desc, service.cmd));
return;
}
if config.log_debug {
- eprintln!(
- "Starting {} ({})",
- unit.info.description.as_ref().unwrap_or(&unit.id.0),
- service.cmd,
- );
+ init_info(&format!("Starting {} ({})", desc, service.cmd));
+ } else {
+ status_ok(&format!("Started {}", desc));
}
service.spawn(&config.envs);
}
UnitKind::Target {} => {
if config.log_debug {
- eprintln!(
- "Reached target {}",
- unit.info.description.as_ref().unwrap_or(&unit.id.0),
- );
+ init_info(&format!("Reached target {}",
+ unit.info.description.as_ref().unwrap_or(&unit.id.0)));
}
}
}
--- a/init/src/service.rs 2026-05-03 05:35:37.594239723 +0100
+++ b/init/src/service.rs 2026-05-03 08:14:33.427953738 +0100
@@ -8,6 +8,7 @@
use serde::Deserialize;
+use crate::color::{init_error, init_warn, status_fail};
use crate::script::subst_env;
#[derive(Clone, Debug, Deserialize)]
@@ -52,7 +53,7 @@
let mut child = match command.spawn() {
Ok(child) => child,
Err(err) => {
- eprintln!("init: failed to execute {:?}: {}", command, err);
+ status_fail(&format!("failed to execute {:?}: {}", command, err));
return;
}
};
@@ -61,10 +62,10 @@
ServiceType::Notify => match read_pipe.read_exact(&mut [0]) {
Ok(()) => {}
Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => {
- eprintln!("init: {command:?} exited without notifying readiness");
+ init_warn(&format!("{:?} exited without notifying readiness", command));
}
Err(err) => {
- eprintln!("init: failed to wait for {command:?}: {err}");
+ init_error(&format!("failed to wait for {:?}: {}", command, err));
}
},
ServiceType::Scheme(scheme) => {
@@ -80,16 +81,16 @@
errno: syscall::EINTR,
}) => continue,
Ok(0) => {
- eprintln!("init: {command:?} exited without notifying readiness");
+ init_warn(&format!("{:?} exited without notifying readiness", command));
return;
}
Ok(1) => break,
Ok(n) => {
- eprintln!("init: incorrect amount of fds {n} returned");
+ init_error(&format!("incorrect amount of fds {} returned", n));
return;
}
Err(err) => {
- eprintln!("init: failed to wait for {command:?}: {err}");
+ init_error(&format!("failed to wait for {:?}: {}", command, err));
return;
}
}
@@ -104,11 +105,11 @@
match child.wait() {
Ok(exit_status) => {
if !exit_status.success() {
- eprintln!("init: {command:?} failed with {exit_status}");
+ status_fail(&format!("{:?} failed with {}", command, exit_status));
}
}
Err(err) => {
- eprintln!("init: failed to wait for {:?}: {}", command, err)
+ init_error(&format!("failed to wait for {:?}: {}", command, err))
}
}
}
@@ -0,0 +1,339 @@
diff --git a/drivers/inputd/src/lib.rs b/drivers/inputd/src/lib.rs
index b68e8211..eb14de51 100644
--- a/drivers/inputd/src/lib.rs
+++ b/drivers/inputd/src/lib.rs
@@ -1,5 +1,5 @@
use std::fs::{File, OpenOptions};
-use std::io::{self, Read, Write};
+use std::io::{self, ErrorKind, Read, Write};
use std::mem::size_of;
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, RawFd};
use std::os::unix::fs::OpenOptionsExt;
@@ -31,6 +31,24 @@ unsafe fn any_as_u8_slice_mut<T: Sized>(p: &mut T) -> &mut [u8] {
slice::from_raw_parts_mut((p as *mut T) as *mut u8, size_of::<T>())
}
+fn validate_input_name(kind: &str, name: &str) -> io::Result<()> {
+ if name.is_empty() {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ format!("input {kind} name must not be empty"),
+ ));
+ }
+
+ if name.contains('/') {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ format!("input {kind} name must not contain '/'"),
+ ));
+ }
+
+ Ok(())
+}
+
pub struct ConsumerHandle(File);
pub enum ConsumerHandleEvent<'a> {
@@ -64,25 +82,53 @@ impl ConsumerHandle {
let fd = self.0.as_raw_fd();
let written = libredox::call::fpath(fd as usize, &mut buffer)?;
- assert!(written <= buffer.len());
+ if written > buffer.len() {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidData,
+ "inputd: display path exceeded buffer size",
+ ));
+ }
- let mut display_path = PathBuf::from(
- std::str::from_utf8(&buffer[..written])
- .expect("init: display path UTF-8 check failed")
- .to_owned(),
- );
- display_path.set_file_name(format!(
- "v2/{}",
- display_path.file_name().unwrap().to_str().unwrap()
- ));
- let display_path = display_path.to_str().unwrap();
+ let path_str = std::str::from_utf8(&buffer[..written]).map_err(|e| {
+ io::Error::new(
+ io::ErrorKind::InvalidData,
+ format!("inputd: display path is not valid UTF-8: {e}"),
+ )
+ })?;
+ let mut display_path = PathBuf::from(path_str.to_owned());
+
+ let file_name = display_path
+ .file_name()
+ .and_then(|n| n.to_str())
+ .ok_or_else(|| {
+ io::Error::new(
+ io::ErrorKind::InvalidData,
+ format!(
+ "inputd: display path has no valid file name: {}",
+ display_path.display()
+ ),
+ )
+ })?;
+ display_path.set_file_name(format!("v2/{file_name}"));
+ let display_path_str = display_path.to_str().ok_or_else(|| {
+ io::Error::new(
+ io::ErrorKind::InvalidData,
+ format!(
+ "inputd: constructed display path is not valid UTF-8: {}",
+ display_path.display()
+ ),
+ )
+ })?;
let display_file =
- libredox::call::open(display_path, (O_CLOEXEC | O_NONBLOCK | O_RDWR) as _, 0)
+ libredox::call::open(display_path_str, (O_CLOEXEC | O_NONBLOCK | O_RDWR) as _, 0)
.map(|socket| unsafe { File::from_raw_fd(socket as RawFd) })
- .unwrap_or_else(|err| {
- panic!("failed to open display {}: {}", display_path, err);
- });
+ .map_err(|err| {
+ io::Error::new(
+ io::ErrorKind::Other,
+ format!("inputd: failed to open display {display_path_str}: {err}"),
+ )
+ })?;
Ok(display_file)
}
@@ -152,8 +198,12 @@ impl DisplayHandle {
if nread == 0 {
Ok(None)
+ } else if nread != size_of::<VtEvent>() {
+ Err(io::Error::new(
+ io::ErrorKind::InvalidData,
+ format!("inputd: partial vt event read: got {nread}, expected {}", size_of::<VtEvent>()),
+ ))
} else {
- assert_eq!(nread, size_of::<VtEvent>());
Ok(Some(event))
}
}
@@ -197,6 +247,160 @@ pub struct VtEvent {
pub vt: usize,
}
+#[derive(Debug, Clone, Copy)]
+#[repr(C)]
+pub struct HotplugEventHeader {
+ pub kind: u32,
+ pub device_id: u32,
+ pub name_len: u32,
+ pub _reserved: u32,
+}
+
+#[derive(Debug, Clone)]
+pub struct HotplugEvent {
+ pub kind: u32,
+ pub device_id: u32,
+ pub name: String,
+}
+
+/// Handle for opening a named producer on the input scheme.
+/// Opens /scheme/input/producer/{name}
+pub struct NamedProducerHandle {
+ fd: File,
+}
+
+impl NamedProducerHandle {
+ pub fn new(name: &str) -> io::Result<Self> {
+ validate_input_name("producer", name)?;
+
+ let path = format!("/scheme/input/producer/{name}");
+ File::open(path).map(|fd| Self { fd })
+ }
+
+ pub fn write_event(&mut self, event: &orbclient::Event) -> io::Result<()> {
+ self.fd.write(unsafe { any_as_u8_slice(event) })?;
+ Ok(())
+ }
+}
+
+pub struct DeviceConsumerHandle {
+ fd: File,
+}
+
+impl DeviceConsumerHandle {
+ pub fn new(device_name: &str) -> io::Result<Self> {
+ validate_input_name("device", device_name)?;
+
+ let fd = OpenOptions::new()
+ .read(true)
+ .custom_flags(O_NONBLOCK as i32)
+ .open(format!("/scheme/input/{device_name}"))?;
+
+ Ok(Self { fd })
+ }
+
+ pub fn event_handle(&self) -> BorrowedFd<'_> {
+ self.fd.as_fd()
+ }
+
+ pub fn read_event(&mut self) -> io::Result<Option<orbclient::Event>> {
+ let mut raw = [0_u8; size_of::<orbclient::Event>()];
+
+ match self.fd.read(&mut raw) {
+ Ok(0) => Ok(None),
+ Ok(read) => {
+ assert_eq!(read, raw.len());
+ Ok(Some(unsafe {
+ core::ptr::read_unaligned(raw.as_ptr().cast::<orbclient::Event>())
+ }))
+ }
+ Err(err) if err.kind() == ErrorKind::WouldBlock => Ok(None),
+ Err(err) => Err(err),
+ }
+ }
+}
+
+pub struct HotplugHandle {
+ fd: File,
+ partial: Vec<u8>,
+}
+
+impl HotplugHandle {
+ pub fn new() -> io::Result<Self> {
+ let fd = OpenOptions::new()
+ .read(true)
+ .custom_flags(O_NONBLOCK as i32)
+ .open("/scheme/input/events")?;
+
+ Ok(Self {
+ fd,
+ partial: Vec::new(),
+ })
+ }
+
+ pub fn event_handle(&self) -> BorrowedFd<'_> {
+ self.fd.as_fd()
+ }
+
+ pub fn read_event(&mut self) -> io::Result<Option<HotplugEvent>> {
+ let mut buf = [0_u8; 1024];
+
+ loop {
+ if let Some(event) = Self::try_parse_event(&mut self.partial)? {
+ return Ok(Some(event));
+ }
+
+ match self.fd.read(&mut buf) {
+ Ok(0) => return Ok(None),
+ Ok(read) => self.partial.extend_from_slice(&buf[..read]),
+ Err(err) if err.kind() == ErrorKind::WouldBlock => return Ok(None),
+ Err(err) => return Err(err),
+ }
+ }
+ }
+
+ fn try_parse_event(partial: &mut Vec<u8>) -> io::Result<Option<HotplugEvent>> {
+ if partial.len() < size_of::<HotplugEventHeader>() {
+ return Ok(None);
+ }
+
+ let header =
+ unsafe { core::ptr::read_unaligned(partial.as_ptr().cast::<HotplugEventHeader>()) };
+ let name_len = usize::try_from(header.name_len).map_err(|_| {
+ io::Error::new(
+ io::ErrorKind::InvalidData,
+ "invalid input hotplug name length",
+ )
+ })?;
+ let total_len = size_of::<HotplugEventHeader>()
+ .checked_add(name_len)
+ .ok_or_else(|| {
+ io::Error::new(io::ErrorKind::InvalidData, "input hotplug event too large")
+ })?;
+
+ if partial.len() < total_len {
+ return Ok(None);
+ }
+
+ let name = std::str::from_utf8(&partial[size_of::<HotplugEventHeader>()..total_len])
+ .map_err(|_| {
+ io::Error::new(
+ io::ErrorKind::InvalidData,
+ "input hotplug name is not UTF-8",
+ )
+ })?
+ .to_owned();
+
+ partial.drain(..total_len);
+
+ Ok(Some(HotplugEvent {
+ kind: header.kind,
+ device_id: header.device_id,
+ name,
+ }))
+ }
+}
+
pub struct ProducerHandle(File);
impl ProducerHandle {
@@ -208,4 +412,58 @@ impl ProducerHandle {
self.0.write(&event)?;
Ok(())
}
+
+ pub fn query_keymap_char(&self, scancode: u8, shift: bool, altgr: bool) -> Option<char> {
+ use std::io::Read;
+ let path = format!("/scheme/keymap/translate/{}/{}/{}", scancode, shift as u8, altgr as u8);
+ let mut f = match std::fs::File::open(&path) {
+ Ok(f) => f,
+ Err(_) => return None,
+ };
+ let mut buf = [0u8; 4];
+ match f.read(&mut buf) {
+ Ok(n) if n > 0 => {
+ let s = std::str::from_utf8(&buf[..n]).ok()?;
+ s.chars().next()
+ }
+ _ => None,
+ }
+ }
+
+ pub fn send_led_state(caps: bool, num: bool, scroll: bool) -> io::Result<()> {
+ let led_byte = (caps as u8) | ((num as u8) << 1) | ((scroll as u8) << 2);
+ let path = format!("/scheme/input/control/leds/{}", led_byte);
+ std::fs::write(&path, &[])?;
+ Ok(())
+ }
+}
+
+/// Convenience wrapper that tries a named producer first,
+/// falling back to the legacy anonymous producer on failure.
+pub enum InputProducer {
+ Named(NamedProducerHandle),
+ Legacy(ProducerHandle),
+}
+
+impl InputProducer {
+ /// Open a named producer (`/scheme/input/producer/{name}`).
+ /// Falls back to the legacy anonymous producer on failure.
+ pub fn new_named_or_fallback(name: &str) -> io::Result<Self> {
+ match NamedProducerHandle::new(name) {
+ Ok(named) => Ok(InputProducer::Named(named)),
+ Err(_) => ProducerHandle::new().map(InputProducer::Legacy),
+ }
+ }
+
+ /// Open the legacy anonymous producer directly.
+ pub fn new_legacy() -> io::Result<Self> {
+ ProducerHandle::new().map(InputProducer::Legacy)
+ }
+
+ pub fn write_event(&mut self, event: orbclient::Event) -> io::Result<()> {
+ match self {
+ InputProducer::Named(h) => h.write_event(&event),
+ InputProducer::Legacy(h) => h.write_event(event),
+ }
+ }
}
@@ -0,0 +1,461 @@
--- a/drivers/input/ps2d/src/controller.rs 21:31:04.000000000 +0100
+++ b/drivers/input/ps2d/src/controller.rs 2026-05-02 02:50:19.907179957 +0100
@@ -97,6 +97,14 @@
const DEFAULT_TIMEOUT: u64 = 50_000;
// Reset timeout in microseconds
const RESET_TIMEOUT: u64 = 1_000_000;
+// Maximum bytes to drain during flush (Linux: I8042_BUFFER_SIZE)
+const FLUSH_LIMIT: usize = 4096;
+// Controller self-test pass value (Linux: I8042_RET_CTL_TEST)
+const SELFTEST_PASS: u8 = 0x55;
+// Controller self-test retries (Linux: 5 attempts)
+const SELFTEST_RETRIES: usize = 5;
+// AUX port test pass value (Linux returns 0x00 on success)
+const AUX_TEST_PASS: u8 = 0x00;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
pub struct Ps2 {
@@ -261,6 +269,30 @@
self.write(command as u8)
}
+ pub fn set_leds(&mut self, caps: bool, num: bool, scroll: bool) {
+ let mut led_byte = 0u8;
+ if scroll { led_byte |= 1; }
+ if num { led_byte |= 2; }
+ if caps { led_byte |= 4; }
+ if let Err(err) = self.keyboard_command_inner(0xED) {
+ warn!("ps2d: failed to send LED command 0xED: {:?}", err);
+ return;
+ }
+ match self.read_timeout(DEFAULT_TIMEOUT) {
+ Ok(0xFA) => {
+ if let Err(err) = self.write(led_byte) {
+ warn!("ps2d: failed to send LED byte {:02X}: {:?}", led_byte, err);
+ }
+ }
+ Ok(val) => {
+ warn!("ps2d: LED command ACK expected 0xFA, got {:02X}", val);
+ }
+ Err(err) => {
+ warn!("ps2d: LED command ACK timeout: {:?}", err);
+ }
+ }
+ }
+
pub fn next(&mut self) -> Option<(bool, u8)> {
let status = self.status();
if status.contains(StatusFlags::OUTPUT_FULL) {
@@ -271,6 +303,50 @@
}
}
+ /// Drain all pending bytes from the controller output buffer.
+ /// Borrowed from Linux i8042_flush(): stale firmware/BIOS bytes can be
+ /// misinterpreted as device responses during initialization.
+ fn flush(&mut self) -> usize {
+ let mut count = 0;
+ while self.status().contains(StatusFlags::OUTPUT_FULL) {
+ if count >= FLUSH_LIMIT {
+ warn!("flush: exceeded limit, controller may be stuck");
+ break;
+ }
+ let data = self.data.read();
+ trace!("flush: discarded {:02X}", data);
+ count += 1;
+ }
+ if count > 0 {
+ debug!("flushed {} stale bytes from controller", count);
+ }
+ count
+ }
+
+ /// Test the AUX (mouse) port via controller command 0xA9.
+ /// Borrowed from Linux: verifies electrical connectivity before
+ /// attempting to talk to the mouse. Returns true if the port passed.
+ fn test_aux_port(&mut self) -> bool {
+ if let Err(err) = self.command(Command::TestSecond) {
+ warn!("aux port test command failed: {:?}", err);
+ return false;
+ }
+ match self.read() {
+ Ok(AUX_TEST_PASS) => {
+ debug!("aux port test passed");
+ true
+ }
+ Ok(val) => {
+ warn!("aux port test failed: {:02X}", val);
+ false
+ }
+ Err(err) => {
+ warn!("aux port test read timeout: {:?}", err);
+ false
+ }
+ }
+ }
+
pub fn init_keyboard(&mut self) -> Result<(), Error> {
let mut b;
@@ -308,66 +384,125 @@
}
pub fn init(&mut self) -> Result<(), Error> {
+ // Linux i8042_controller_check(): verify controller is present by
+ // flushing any stale data. A stuck output buffer means no controller.
+ self.flush();
+
+ // Bare-metal controllers may be slow after firmware handoff.
+ // Give the controller a moment to finish POST before sending commands.
+ std::thread::sleep(std::time::Duration::from_millis(50));
+
{
- // Disable devices
- self.command(Command::DisableFirst)?;
- self.command(Command::DisableSecond)?;
+ // Disable both ports first — use retry because the controller
+ // may still be settling or temporarily unresponsive.
+ // Failure here is non-fatal: we continue and attempt the rest
+ // of initialization. A truly absent controller will fail later
+ // at self-test or keyboard reset.
+ if let Err(err) = self.retry(
+ format_args!("disable first port"),
+ 3,
+ |x| x.command(Command::DisableFirst),
+ ) {
+ warn!("disable first port failed: {:?}", err);
+ }
+ if let Err(err) = self.retry(
+ format_args!("disable second port"),
+ 3,
+ |x| x.command(Command::DisableSecond),
+ ) {
+ warn!("disable second port failed: {:?}", err);
+ }
}
- // Disable clocks, disable interrupts, and disable translate
+ // Flush again after disabling — firmware may have queued more bytes
+ self.flush();
+
+ // Linux i8042_controller_init() step 1: write a known-safe config
+ // (interrupts off, both ports disabled) so stale config can't cause
+ // spurious interrupts during the rest of init.
{
- // Since the default config may have interrupts enabled, and the kernel may eat up
- // our data in that case, we will write a config without reading the current one
let config = ConfigFlags::POST_PASSED
| ConfigFlags::FIRST_DISABLED
| ConfigFlags::SECOND_DISABLED;
self.set_config(config)?;
}
- // The keyboard seems to still collect bytes even when we disable
- // the port, so we must disable the keyboard too
+ // Linux i8042_controller_selftest(): retry up to 5 times with delay.
+ // "On some really fragile systems this does not take the first time."
+ {
+ let mut passed = false;
+ for attempt in 0..SELFTEST_RETRIES {
+ if let Err(err) = self.command(Command::TestController) {
+ warn!("self-test command failed (attempt {}): {:?}", attempt + 1, err);
+ continue;
+ }
+ match self.read() {
+ Ok(SELFTEST_PASS) => {
+ passed = true;
+ break;
+ }
+ Ok(val) => {
+ warn!(
+ "self-test unexpected value {:02X} (attempt {}/{})",
+ val,
+ attempt + 1,
+ SELFTEST_RETRIES
+ );
+ }
+ Err(err) => {
+ warn!("self-test read timeout (attempt {}): {:?}", attempt + 1, err);
+ }
+ }
+ // Linux: msleep(50) between retries
+ std::thread::sleep(std::time::Duration::from_millis(50));
+ }
+ if !passed {
+ // Linux on x86: "giving up on controller selftest, continuing anyway"
+ warn!("controller self-test did not pass after {} attempts, continuing", SELFTEST_RETRIES);
+ }
+ }
+
+ // Flush any bytes the self-test may have left behind
+ self.flush();
+
+ // Linux i8042_controller_init() step 2: set keyboard defaults
+ // (disable scanning so keyboard doesn't send scancodes during init)
self.retry(format_args!("keyboard defaults"), 4, |x| {
- // Set defaults and disable scanning
let b = x.keyboard_command(KeyboardCommand::SetDefaultsDisable)?;
if b != 0xFA {
error!("keyboard failed to set defaults: {:02X}", b);
return Err(Error::CommandRetry);
}
-
Ok(b)
})?;
- {
- // Perform the self test
- self.command(Command::TestController)?;
- let r = self.read()?;
- if r != 0x55 {
- warn!("self test unexpected value: {:02X}", r);
- }
- }
-
// Initialize keyboard
if let Err(err) = self.init_keyboard() {
error!("failed to initialize keyboard: {:?}", err);
return Err(err);
}
- // Enable second device
- let enable_mouse = match self.command(Command::EnableSecond) {
- Ok(()) => true,
- Err(err) => {
- error!("failed to initialize mouse: {:?}", err);
- false
+ // Linux: test AUX port (command 0xA9) before enabling.
+ // Skips mouse init entirely if the port is not electrically present.
+ let aux_ok = self.test_aux_port();
+
+ // Enable second device (mouse) only if AUX port tested OK
+ let enable_mouse = if aux_ok {
+ match self.command(Command::EnableSecond) {
+ Ok(()) => true,
+ Err(err) => {
+ warn!("failed to enable aux port after test passed: {:?}", err);
+ false
+ }
}
+ } else {
+ info!("skipping mouse init: aux port test did not pass");
+ false
};
{
- // Enable keyboard data reporting
- // Use inner function to prevent retries
- // Response is ignored since scanning is now on
if let Err(err) = self.keyboard_command_inner(KeyboardCommand::EnableReporting as u8) {
error!("failed to initialize keyboard reporting: {:?}", err);
- //TODO: fix by using interrupts?
}
}
--- a/drivers/input/ps2d/src/state.rs 21:31:04.000000000 +0100
+++ b/drivers/input/ps2d/src/state.rs 2026-05-02 04:22:27.342569199 +0100
@@ -1,4 +1,4 @@
-use inputd::ProducerHandle;
+use inputd::InputProducer;
use log::{error, warn};
use orbclient::{ButtonEvent, KeyEvent, MouseEvent, MouseRelativeEvent, ScrollEvent};
use std::{
@@ -44,7 +44,8 @@
ps2: Ps2,
vmmouse: bool,
vmmouse_relative: bool,
- input: ProducerHandle,
+ keyboard_input: InputProducer,
+ mouse_input: InputProducer,
time_file: File,
extended: bool,
mouse_x: i32,
@@ -56,12 +57,18 @@
mouse_timeout: Option<TimeSpec>,
packets: [u8; 4],
packet_i: usize,
+ caps_lock: bool,
+ num_lock: bool,
+ scroll_lock: bool,
+ leds_dirty: bool,
}
impl Ps2d {
- pub fn new(input: ProducerHandle, time_file: File) -> Self {
+ pub fn new(keyboard_input: InputProducer, mouse_input: InputProducer, time_file: File) -> Self {
let mut ps2 = Ps2::new();
- ps2.init().expect("failed to initialize");
+ if let Err(err) = ps2.init() {
+ log::error!("ps2d: controller init failed: {:?}", err);
+ }
// FIXME add an option for orbital to disable this when an app captures the mouse.
let vmmouse_relative = false;
@@ -77,7 +84,8 @@
ps2,
vmmouse,
vmmouse_relative,
- input,
+ keyboard_input,
+ mouse_input,
time_file,
extended: false,
mouse_x: 0,
@@ -89,6 +97,10 @@
mouse_timeout: None,
packets: [0; 4],
packet_i: 0,
+ caps_lock: false,
+ num_lock: true,
+ scroll_lock: false,
+ leds_dirty: true,
};
if !this.vmmouse {
@@ -96,6 +108,12 @@
this.handle_mouse(None);
}
+ // Flush initial LED state (Num Lock on by default)
+ if this.leds_dirty {
+ this.leds_dirty = false;
+ this.ps2.set_leds(this.caps_lock, this.num_lock, this.scroll_lock);
+ }
+
this
}
@@ -272,8 +290,21 @@
}
};
+ if scancode != 0 && pressed {
+ match scancode {
+ orbclient::K_CAPS => { self.caps_lock = !self.caps_lock; self.leds_dirty = true; },
+ orbclient::K_NUM => { self.num_lock = !self.num_lock; self.leds_dirty = true; },
+ orbclient::K_SCROLL => { self.scroll_lock = !self.scroll_lock; self.leds_dirty = true; },
+ _ => (),
+ }
+ }
+ if self.leds_dirty {
+ self.leds_dirty = false;
+ self.ps2.set_leds(self.caps_lock, self.num_lock, self.scroll_lock);
+ }
+
if scancode != 0 {
- self.input
+ self.keyboard_input
.write_event(
KeyEvent {
character: '\0',
@@ -304,7 +335,7 @@
if self.vmmouse_relative {
if dx != 0 || dy != 0 {
- self.input
+ self.mouse_input
.write_event(
MouseRelativeEvent {
dx: dx as i32,
@@ -320,14 +351,14 @@
if x != self.mouse_x || y != self.mouse_y {
self.mouse_x = x;
self.mouse_y = y;
- self.input
+ self.mouse_input
.write_event(MouseEvent { x, y }.to_event())
.expect("ps2d: failed to write mouse event");
}
};
if dz != 0 {
- self.input
+ self.mouse_input
.write_event(
ScrollEvent {
x: 0,
@@ -348,7 +379,7 @@
self.mouse_left = left;
self.mouse_middle = middle;
self.mouse_right = right;
- self.input
+ self.mouse_input
.write_event(
ButtonEvent {
left,
@@ -441,13 +472,13 @@
}
if dx != 0 || dy != 0 {
- self.input
+ self.mouse_input
.write_event(MouseRelativeEvent { dx, dy }.to_event())
.expect("ps2d: failed to write mouse event");
}
if dz != 0 {
- self.input
+ self.mouse_input
.write_event(ScrollEvent { x: 0, y: dz }.to_event())
.expect("ps2d: failed to write scroll event");
}
@@ -462,7 +493,7 @@
self.mouse_left = left;
self.mouse_middle = middle;
self.mouse_right = right;
- self.input
+ self.mouse_input
.write_event(
ButtonEvent {
left,
diff --git a/drivers/input/ps2d/src/main.rs b/drivers/input/ps2d/src/main.rs
index db17de2a..86f903bf 100644
--- a/drivers/input/ps2d/src/main.rs 21:31:04.000000000 +0100
+++ b/drivers/input/ps2d/src/main.rs
@@ -14,4 +14,4 @@
-use inputd::ProducerHandle;
+use inputd::InputProducer;
use crate::state::Ps2d;
@@ -31,7 +31,8 @@ fn daemon(daemon: daemon::Daemon) -> ! {
acquire_port_io_rights().expect("ps2d: failed to get I/O permission");
- let input = ProducerHandle::new().expect("ps2d: failed to open input producer");
+ let keyboard_input = InputProducer::new_named_or_fallback("ps2-keyboard").expect("ps2d: failed to open keyboard input");
+ let mouse_input = InputProducer::new_named_or_fallback("ps2-mouse").expect("ps2d: failed to open mouse input");
user_data! {
enum Source {
@@ -93,7 +94,7 @@ fn daemon(daemon: daemon::Daemon) -> ! {
daemon.ready();
- let mut ps2d = Ps2d::new(input, time_file);
+ let mut ps2d = Ps2d::new(keyboard_input, mouse_input, time_file);
let mut data = [0; 256];
for event in event_queue.map(|e| e.expect("ps2d: failed to get next event").user_data) {
+31,8 @@ fn daemon(daemon: daemon::Daemon) -> ! {
acquire_port_io_rights().expect("ps2d: failed to get I/O permission");
- let input = ProducerHandle::new().expect("ps2d: failed to open input producer");
+ let keyboard_input = InputProducer::new_named_or_fallback("ps2-keyboard").expect("ps2d: failed to open keyboard input");
+ let mouse_input = InputProducer::new_named_or_fallback("ps2-mouse").expect("ps2d: failed to open mouse input");
user_data! {
enum Source {
@@ -93,7 +94,7 @@ fn daemon(daemon: daemon::Daemon) -> ! {
daemon.ready();
- let mut ps2d = Ps2d::new(input, time_file);
+ let mut ps2d = Ps2d::new(keyboard_input, mouse_input, time_file);
let mut data = [0; 256];
for event in event_queue.map(|e| e.expect("ps2d: failed to get next event").user_data) {
@@ -0,0 +1,725 @@
diff --git a/drivers/input/usbhidd/src/main.rs b/drivers/input/usbhidd/src/main.rs
index 15c5b778..472ec4cf 100644
--- a/drivers/input/usbhidd/src/main.rs
+++ b/drivers/input/usbhidd/src/main.rs
@@ -1,7 +1,7 @@
-use anyhow::{Context, Result};
+use anyhow::{bail, ensure, Context, Result};
use std::{env, thread, time};
-use inputd::ProducerHandle;
+use inputd::InputProducer;
use orbclient::KeyEvent as OrbKeyEvent;
use rehid::{
report_desc::{ReportTy, REPORT_DESC_TY},
@@ -15,161 +15,219 @@ use xhcid_interface::{
mod reqs;
-fn send_key_event(display: &mut ProducerHandle, usage_page: u16, usage: u16, pressed: bool) {
- let scancode = match usage_page {
- 0x07 => match usage {
- 0x04 => orbclient::K_A,
- 0x05 => orbclient::K_B,
- 0x06 => orbclient::K_C,
- 0x07 => orbclient::K_D,
- 0x08 => orbclient::K_E,
- 0x09 => orbclient::K_F,
- 0x0A => orbclient::K_G,
- 0x0B => orbclient::K_H,
- 0x0C => orbclient::K_I,
- 0x0D => orbclient::K_J,
- 0x0E => orbclient::K_K,
- 0x0F => orbclient::K_L,
- 0x10 => orbclient::K_M,
- 0x11 => orbclient::K_N,
- 0x12 => orbclient::K_O,
- 0x13 => orbclient::K_P,
- 0x14 => orbclient::K_Q,
- 0x15 => orbclient::K_R,
- 0x16 => orbclient::K_S,
- 0x17 => orbclient::K_T,
- 0x18 => orbclient::K_U,
- 0x19 => orbclient::K_V,
- 0x1A => orbclient::K_W,
- 0x1B => orbclient::K_X,
- 0x1C => orbclient::K_Y,
- 0x1D => orbclient::K_Z,
- 0x1E => orbclient::K_1,
- 0x1F => orbclient::K_2,
- 0x20 => orbclient::K_3,
- 0x21 => orbclient::K_4,
- 0x22 => orbclient::K_5,
- 0x23 => orbclient::K_6,
- 0x24 => orbclient::K_7,
- 0x25 => orbclient::K_8,
- 0x26 => orbclient::K_9,
- 0x27 => orbclient::K_0,
- 0x28 => orbclient::K_ENTER,
- 0x29 => orbclient::K_ESC,
- 0x2A => orbclient::K_BKSP,
- 0x2B => orbclient::K_TAB,
- 0x2C => orbclient::K_SPACE,
- 0x2D => orbclient::K_MINUS,
- 0x2E => orbclient::K_EQUALS,
- 0x2F => orbclient::K_BRACE_OPEN,
- 0x30 => orbclient::K_BRACE_CLOSE,
- 0x31 => orbclient::K_BACKSLASH,
- // 0x32 non-us # and ~
- 0x32 => 0x56,
- 0x33 => orbclient::K_SEMICOLON,
- 0x34 => orbclient::K_QUOTE,
- 0x35 => orbclient::K_TICK,
- 0x36 => orbclient::K_COMMA,
- 0x37 => orbclient::K_PERIOD,
- 0x38 => orbclient::K_SLASH,
- 0x39 => orbclient::K_CAPS,
- 0x3A => orbclient::K_F1,
- 0x3B => orbclient::K_F2,
- 0x3C => orbclient::K_F3,
- 0x3D => orbclient::K_F4,
- 0x3E => orbclient::K_F5,
- 0x3F => orbclient::K_F6,
- 0x40 => orbclient::K_F7,
- 0x41 => orbclient::K_F8,
- 0x42 => orbclient::K_F9,
- 0x43 => orbclient::K_F10,
- 0x44 => orbclient::K_F11,
- 0x45 => orbclient::K_F12,
- 0x46 => orbclient::K_PRTSC,
- 0x47 => orbclient::K_SCROLL,
- // 0x48 pause
- 0x49 => orbclient::K_INS,
- 0x4A => orbclient::K_HOME,
- 0x4B => orbclient::K_PGUP,
- 0x4C => orbclient::K_DEL,
- 0x4D => orbclient::K_END,
- 0x4E => orbclient::K_PGDN,
- 0x4F => orbclient::K_RIGHT,
- 0x50 => orbclient::K_LEFT,
- 0x51 => orbclient::K_DOWN,
- 0x52 => orbclient::K_UP,
- 0x53 => orbclient::K_NUM,
- 0x54 => orbclient::K_NUM_SLASH,
- 0x55 => orbclient::K_NUM_ASTERISK,
- 0x56 => orbclient::K_NUM_MINUS,
- 0x57 => orbclient::K_NUM_PLUS,
- 0x58 => orbclient::K_NUM_ENTER,
- 0x59 => orbclient::K_NUM_1,
- 0x5A => orbclient::K_NUM_2,
- 0x5B => orbclient::K_NUM_3,
- 0x5C => orbclient::K_NUM_4,
- 0x5D => orbclient::K_NUM_5,
- 0x5E => orbclient::K_NUM_6,
- 0x5F => orbclient::K_NUM_7,
- 0x60 => orbclient::K_NUM_8,
- 0x61 => orbclient::K_NUM_9,
- 0x62 => orbclient::K_NUM_0,
- // 0x62 num .
- // 0x64 non-us \ and |
- 0x64 => orbclient::K_APP,
- 0x66 => orbclient::K_POWER,
- // 0x67 num =
- // unmapped values
- 0xE0 => orbclient::K_LEFT_CTRL,
- 0xE1 => orbclient::K_LEFT_SHIFT,
- 0xE2 => orbclient::K_ALT,
- 0xE3 => orbclient::K_LEFT_SUPER,
- 0xE4 => orbclient::K_RIGHT_CTRL,
- 0xE5 => orbclient::K_RIGHT_SHIFT,
- 0xE6 => orbclient::K_ALT_GR,
- 0xE7 => orbclient::K_RIGHT_SUPER,
- // reserved values
- _ => {
- log::warn!("unknown usage_page {:#x} usage {:#x}", usage_page, usage);
- return;
- }
- },
- _ => {
- log::warn!("unknown usage_page {:#x}", usage_page);
+const MAX_MOUSE_BUTTONS: usize = 8;
+const MAX_RETRIES: u32 = 3;
+const RETRY_BASE_DELAY_MS: u64 = 2;
+const REPORT_DESC_MAX_SIZE: usize = 4096;
+const REPORT_DESC_MIN_SIZE: usize = 3;
+
+/// USB HID Usage (Keyboard page 0x07) → orbclient scancode lookup table.
+/// Covers usage codes 0x040xE7 per USB HID Usage Tables spec v1.12.
+/// Entries default to 0 (unmapped); only defined usages are populated.
+static HID_KEYBOARD_TO_SCANCODE: [u8; 0xE8] = {
+ let mut table = [0u8; 0xE8];
+ table[0x04] = orbclient::K_A;
+ table[0x05] = orbclient::K_B;
+ table[0x06] = orbclient::K_C;
+ table[0x07] = orbclient::K_D;
+ table[0x08] = orbclient::K_E;
+ table[0x09] = orbclient::K_F;
+ table[0x0A] = orbclient::K_G;
+ table[0x0B] = orbclient::K_H;
+ table[0x0C] = orbclient::K_I;
+ table[0x0D] = orbclient::K_J;
+ table[0x0E] = orbclient::K_K;
+ table[0x0F] = orbclient::K_L;
+ table[0x10] = orbclient::K_M;
+ table[0x11] = orbclient::K_N;
+ table[0x12] = orbclient::K_O;
+ table[0x13] = orbclient::K_P;
+ table[0x14] = orbclient::K_Q;
+ table[0x15] = orbclient::K_R;
+ table[0x16] = orbclient::K_S;
+ table[0x17] = orbclient::K_T;
+ table[0x18] = orbclient::K_U;
+ table[0x19] = orbclient::K_V;
+ table[0x1A] = orbclient::K_W;
+ table[0x1B] = orbclient::K_X;
+ table[0x1C] = orbclient::K_Y;
+ table[0x1D] = orbclient::K_Z;
+ table[0x1E] = orbclient::K_1;
+ table[0x1F] = orbclient::K_2;
+ table[0x20] = orbclient::K_3;
+ table[0x21] = orbclient::K_4;
+ table[0x22] = orbclient::K_5;
+ table[0x23] = orbclient::K_6;
+ table[0x24] = orbclient::K_7;
+ table[0x25] = orbclient::K_8;
+ table[0x26] = orbclient::K_9;
+ table[0x27] = orbclient::K_0;
+ table[0x28] = orbclient::K_ENTER;
+ table[0x29] = orbclient::K_ESC;
+ table[0x2A] = orbclient::K_BKSP;
+ table[0x2B] = orbclient::K_TAB;
+ table[0x2C] = orbclient::K_SPACE;
+ table[0x2D] = orbclient::K_MINUS;
+ table[0x2E] = orbclient::K_EQUALS;
+ table[0x2F] = orbclient::K_BRACE_OPEN;
+ table[0x30] = orbclient::K_BRACE_CLOSE;
+ table[0x31] = orbclient::K_BACKSLASH;
+ table[0x32] = 0x56;
+ table[0x33] = orbclient::K_SEMICOLON;
+ table[0x34] = orbclient::K_QUOTE;
+ table[0x35] = orbclient::K_TICK;
+ table[0x36] = orbclient::K_COMMA;
+ table[0x37] = orbclient::K_PERIOD;
+ table[0x38] = orbclient::K_SLASH;
+ table[0x39] = orbclient::K_CAPS;
+ table[0x3A] = orbclient::K_F1;
+ table[0x3B] = orbclient::K_F2;
+ table[0x3C] = orbclient::K_F3;
+ table[0x3D] = orbclient::K_F4;
+ table[0x3E] = orbclient::K_F5;
+ table[0x3F] = orbclient::K_F6;
+ table[0x40] = orbclient::K_F7;
+ table[0x41] = orbclient::K_F8;
+ table[0x42] = orbclient::K_F9;
+ table[0x43] = orbclient::K_F10;
+ table[0x44] = orbclient::K_F11;
+ table[0x45] = orbclient::K_F12;
+ table[0x46] = orbclient::K_PRTSC;
+ table[0x47] = orbclient::K_SCROLL;
+ table[0x49] = orbclient::K_INS;
+ table[0x4A] = orbclient::K_HOME;
+ table[0x4B] = orbclient::K_PGUP;
+ table[0x4C] = orbclient::K_DEL;
+ table[0x4D] = orbclient::K_END;
+ table[0x4E] = orbclient::K_PGDN;
+ table[0x4F] = orbclient::K_RIGHT;
+ table[0x50] = orbclient::K_LEFT;
+ table[0x51] = orbclient::K_DOWN;
+ table[0x52] = orbclient::K_UP;
+ table[0x53] = orbclient::K_NUM;
+ table[0x54] = orbclient::K_NUM_SLASH;
+ table[0x55] = orbclient::K_NUM_ASTERISK;
+ table[0x56] = orbclient::K_NUM_MINUS;
+ table[0x57] = orbclient::K_NUM_PLUS;
+ table[0x58] = orbclient::K_NUM_ENTER;
+ table[0x59] = orbclient::K_NUM_1;
+ table[0x5A] = orbclient::K_NUM_2;
+ table[0x5B] = orbclient::K_NUM_3;
+ table[0x5C] = orbclient::K_NUM_4;
+ table[0x5D] = orbclient::K_NUM_5;
+ table[0x5E] = orbclient::K_NUM_6;
+ table[0x5F] = orbclient::K_NUM_7;
+ table[0x60] = orbclient::K_NUM_8;
+ table[0x61] = orbclient::K_NUM_9;
+ table[0x62] = orbclient::K_NUM_0;
+ table[0x64] = orbclient::K_APP;
+ table[0x66] = orbclient::K_POWER;
+ table[0xE0] = orbclient::K_LEFT_CTRL;
+ table[0xE1] = orbclient::K_LEFT_SHIFT;
+ table[0xE2] = orbclient::K_ALT;
+ table[0xE3] = orbclient::K_LEFT_SUPER;
+ table[0xE4] = orbclient::K_RIGHT_CTRL;
+ table[0xE5] = orbclient::K_RIGHT_SHIFT;
+ table[0xE6] = orbclient::K_ALT_GR;
+ table[0xE7] = orbclient::K_RIGHT_SUPER;
+ table
+};
+
+fn hid_usage_to_scancode(usage: u16) -> Option<u8> {
+ let idx = usage as usize;
+ if idx < HID_KEYBOARD_TO_SCANCODE.len() {
+ let sc = HID_KEYBOARD_TO_SCANCODE[idx];
+ if sc != 0 {
+ return Some(sc);
+ }
+ }
+ None
+}
+
+fn send_key_event(display: &mut InputProducer, usage_page: u16, usage: u16, pressed: bool) {
+ if usage_page != 0x07 {
+ log::warn!("send_key_event: unexpected usage_page {:#x}", usage_page);
+ return;
+ }
+ let scancode = match hid_usage_to_scancode(usage) {
+ Some(sc) => sc,
+ None => {
+ log::warn!("unmapped HID keyboard usage {:#x}", usage);
return;
}
};
-
let key_event = OrbKeyEvent {
character: '\0',
scancode,
pressed,
};
+ if let Err(err) = display.write_event(key_event.to_event()) {
+ log::warn!("failed to send key event: {}", err);
+ }
+}
- match display.write_event(key_event.to_event()) {
- Ok(_) => (),
- Err(err) => {
- log::warn!("failed to send key event to orbital: {}", err);
+fn validate_report_descriptor(bytes: &[u8]) -> Result<()> {
+ ensure!(
+ bytes.len() >= REPORT_DESC_MIN_SIZE,
+ "report descriptor too short: {} bytes (minimum {})",
+ bytes.len(),
+ REPORT_DESC_MIN_SIZE
+ );
+ ensure!(
+ bytes.len() <= REPORT_DESC_MAX_SIZE,
+ "report descriptor too large: {} bytes (maximum {})",
+ bytes.len(),
+ REPORT_DESC_MAX_SIZE
+ );
+ let mut depth = 0u32;
+ let mut i = 0;
+ while i < bytes.len() {
+ let b = bytes[i];
+ let size = match b & 0x03 {
+ 0 => 0,
+ 1 => 1,
+ 2 => 2,
+ 3 => 4,
+ _ => unreachable!(),
+ };
+ let item_type = (b >> 2) & 0x03;
+ if item_type == 0x0A {
+ depth += 1;
+ } else if item_type == 0x0C {
+ if depth == 0 {
+ bail!(
+ "unbalanced Collection/EndCollection in report descriptor at offset {}",
+ i
+ );
+ }
+ depth -= 1;
}
+ i += 1 + size as usize;
}
+ ensure!(
+ depth == 0,
+ "unbalanced Collection/EndCollection (depth {} at end)",
+ depth
+ );
+ Ok(())
}
fn main() -> Result<()> {
let mut args = env::args().skip(1);
- const USAGE: &'static str = "usbhidd <scheme> <port> <interface>";
+ const USAGE: &str = "usbhidd <scheme> <port> <interface>";
- let scheme = args.next().expect(USAGE);
- let port = args
+ let scheme = args.next().context(USAGE)?;
+ let port: PortId = args
.next()
- .expect(USAGE)
- .parse::<PortId>()
- .expect("Expected port ID");
- let interface_num = args
+ .context(USAGE)?
+ .parse()
+ .map_err(|e| anyhow::anyhow!("Expected port ID: {}", e))?;
+ let interface_num: u8 = args
.next()
- .expect(USAGE)
- .parse::<u8>()
- .expect("Expected integer as input of interface");
+ .context(USAGE)?
+ .parse()
+ .map_err(|e| anyhow::anyhow!("Expected interface number: {}", e))?;
let name = format!("{}_{}_{}_hid", scheme, port, interface_num);
common::setup_logging(
@@ -218,7 +276,6 @@ fn main() -> Result<()> {
}
});
let hid_desc = if_desc.hid_descs.iter().find_map(|hid_desc| {
- //TODO: should we do any filtering?
Some(hid_desc)
})?;
Some((if_desc.clone(), endp_desc_opt, hid_desc))
@@ -240,31 +297,39 @@ fn main() -> Result<()> {
})
.context("Failed to configure endpoints")?;
- //TODO: do we need to set protocol to report? It fails for mice.
-
- //TODO: dynamically create good values, fix xhcid so it does not block on each request
- // This sets all reports to a duration of 4ms
reqs::set_idle(&handle, 1, 0, interface_num as u16).context("Failed to set idle")?;
- let report_desc_len = hid_desc.desc_len;
- assert_eq!(hid_desc.desc_ty, REPORT_DESC_TY);
+ let report_desc_len = hid_desc.desc_len as usize;
+ ensure!(
+ hid_desc.desc_ty == REPORT_DESC_TY,
+ "unexpected HID descriptor type: expected {}, got {}",
+ REPORT_DESC_TY,
+ hid_desc.desc_ty
+ );
+ ensure!(
+ report_desc_len >= REPORT_DESC_MIN_SIZE && report_desc_len <= REPORT_DESC_MAX_SIZE,
+ "suspicious report descriptor length: {}",
+ report_desc_len
+ );
- let mut report_desc_bytes = vec![0u8; report_desc_len as usize];
+ let mut report_desc_bytes = vec![0u8; report_desc_len];
handle
.get_descriptor(
PortReqRecipient::Interface,
REPORT_DESC_TY,
0,
- //TODO: should this be an index into interface_descs?
interface_num as u16,
&mut report_desc_bytes,
)
.context("Failed to retrieve report descriptor")?;
+ validate_report_descriptor(&report_desc_bytes)
+ .context("HID report descriptor validation failed")?;
+
let mut handler =
- ReportHandler::new(&report_desc_bytes).expect("failed to parse report descriptor");
+ ReportHandler::new(&report_desc_bytes).map_err(|e| anyhow::anyhow!("failed to parse report descriptor: {}", e))?;
- let report_len = match endp_desc_opt {
+ let report_len = match &endp_desc_opt {
Some((_endp_num, endp_desc)) => endp_desc.max_packet_size as usize,
None => handler.total_byte_length as usize,
};
@@ -272,7 +337,9 @@ fn main() -> Result<()> {
let report_ty = ReportTy::Input;
let report_id = 0;
- let mut display = ProducerHandle::new().context("Failed to open input socket")?;
+ let producer_name = format!("usb-{}-if{}", port, interface_num);
+ let mut display = InputProducer::new_named_or_fallback(&producer_name)
+ .context("Failed to open input socket")?;
let mut endpoint_opt = match endp_desc_opt {
Some((endp_num, _endp_desc)) => match handle.open_endpoint(endp_num as u8) {
Ok(ok) => Some(ok),
@@ -286,172 +353,168 @@ fn main() -> Result<()> {
let mut left_shift = false;
let mut right_shift = false;
let mut last_mouse_pos = (0, 0);
- let mut last_buttons = [false, false, false];
+ let mut last_buttons = [false; MAX_MOUSE_BUTTONS];
+ let mut consecutive_errors: u32 = 0;
+
loop {
- //TODO: get frequency from device
- //TODO: use sleeps when accuracy is better: thread::sleep(time::Duration::from_millis(10));
- let timer = time::Instant::now();
- while timer.elapsed() < time::Duration::from_millis(1) {
- thread::yield_now();
- }
+ thread::sleep(time::Duration::from_millis(1));
- if let Some(endpoint) = &mut endpoint_opt {
- // interrupt transfer
- endpoint
- .transfer_read(&mut report_buffer)
- .context("failed to get report")?;
+ let transfer_result: Result<(), anyhow::Error> = if let Some(endpoint) = &mut endpoint_opt {
+ endpoint.transfer_read(&mut report_buffer)
+ .map(|_| ())
+ .context("interrupt transfer failed")
} else {
- // control transfer
- reqs::get_report(
- &handle,
- report_ty,
- report_id,
- //TODO: should this be an index into interface_descs?
- interface_num as u16,
- &mut report_buffer,
- )
- .context("failed to get report")?;
+ reqs::get_report(&handle, report_ty, report_id, interface_num as u16, &mut report_buffer)
+ .map(|_| ())
+ .context("control transfer failed")
+ };
+
+ if let Err(err) = transfer_result {
+ consecutive_errors += 1;
+ if consecutive_errors >= MAX_RETRIES {
+ bail!(
+ "transfer failed {} times consecutively: {}",
+ consecutive_errors,
+ err
+ );
+ }
+ let delay = RETRY_BASE_DELAY_MS * (1 << consecutive_errors.min(4));
+ log::warn!(
+ "transfer failed ({}/{}), retry in {}ms: {}",
+ consecutive_errors,
+ MAX_RETRIES,
+ delay,
+ err
+ );
+ thread::sleep(time::Duration::from_millis(delay));
+ continue;
}
+ consecutive_errors = 0;
let mut mouse_pos = last_mouse_pos;
let mut mouse_dx = 0i32;
let mut mouse_dy = 0i32;
let mut scroll_y = 0i32;
let mut buttons = last_buttons;
- for event in handler
- .handle(&report_buffer)
- .expect("failed to parse report")
- {
- log::debug!("{}", event);
- if event.usage_page == UsagePage::GenericDesktop as u16 {
- if event.usage == GenericDesktopUsage::X as u16 {
- if event.relative {
- mouse_dx += event.value as i32;
- } else {
- mouse_pos.0 = event.value as i32;
- }
- } else if event.usage == GenericDesktopUsage::Y as u16 {
- if event.relative {
- mouse_dy += event.value as i32;
- } else {
- mouse_pos.1 = event.value as i32;
- }
- } else if event.usage == GenericDesktopUsage::Wheel as u16 {
- //TODO: what is X scroll?
- if event.relative {
- scroll_y += event.value as i32;
- } else {
- log::warn!("absolute mouse wheel not supported");
+
+ match handler.handle(&report_buffer) {
+ Ok(events) => {
+ for event in events {
+ log::debug!("{}", event);
+ if event.usage_page == UsagePage::GenericDesktop as u16 {
+ if event.usage == GenericDesktopUsage::X as u16 {
+ if event.relative {
+ mouse_dx += event.value as i32;
+ } else {
+ mouse_pos.0 = event.value as i32;
+ }
+ } else if event.usage == GenericDesktopUsage::Y as u16 {
+ if event.relative {
+ mouse_dy += event.value as i32;
+ } else {
+ mouse_pos.1 = event.value as i32;
+ }
+ } else if event.usage == GenericDesktopUsage::Wheel as u16 {
+ if event.relative {
+ scroll_y += event.value as i32;
+ } else {
+ log::warn!("absolute mouse wheel not supported");
+ }
+ } else if event.usage == GenericDesktopUsage::Z as u16 {
+ if event.relative && event.value != 0 {
+ let scroll_event = orbclient::event::ScrollEvent {
+ x: event.value as i32,
+ y: 0,
+ };
+ if let Err(err) = display.write_event(scroll_event.to_event()) {
+ log::warn!("failed to send hscroll: {}", err);
+ }
+ }
+ } else {
+ log::info!(
+ "unsupported generic desktop usage 0x{:X}:0x{:X} value {}",
+ event.usage_page,
+ event.usage,
+ event.value
+ );
+ }
+ } else if event.usage_page == UsagePage::KeyboardOrKeypad as u16 {
+ let pressed = event.value != 0;
+ if event.usage == 0xE1 {
+ left_shift = pressed;
+ } else if event.usage == 0xE5 {
+ right_shift = pressed;
+ }
+ send_key_event(&mut display, event.usage_page, event.usage, pressed);
+ } else if event.usage_page == UsagePage::Button as u16 {
+ let btn_idx = event.usage as usize;
+ if btn_idx > 0 && btn_idx <= MAX_MOUSE_BUTTONS {
+ buttons[btn_idx - 1] = event.value != 0;
+ } else if btn_idx > MAX_MOUSE_BUTTONS {
+ log::debug!(
+ "ignoring button {} (max {})",
+ btn_idx,
+ MAX_MOUSE_BUTTONS
+ );
+ }
+ } else if event.usage_page < 0xFF00 {
+ log::info!(
+ "unsupported usage 0x{:X}:0x{:X} value {}",
+ event.usage_page,
+ event.usage,
+ event.value
+ );
}
- } else {
- log::info!(
- "unsupported generic desktop usage 0x{:X}:0x{:X} value {}",
- event.usage_page,
- event.usage,
- event.value
- );
}
- } else if event.usage_page == UsagePage::KeyboardOrKeypad as u16 {
- let (pressed, shift_opt) = if event.value != 0 {
- (true, Some(left_shift | right_shift))
- } else {
- (false, None)
- };
- if event.usage == 0xE1 {
- left_shift = pressed;
- } else if event.usage == 0xE5 {
- right_shift = pressed;
- }
- send_key_event(&mut display, event.usage_page, event.usage, pressed);
- } else if event.usage_page == UsagePage::Button as u16 {
- if event.usage > 0 && event.usage as usize <= buttons.len() {
- buttons[event.usage as usize - 1] = event.value != 0;
- } else {
- log::info!(
- "unsupported buttons usage 0x{:X}:0x{:X} value {}",
- event.usage_page,
- event.usage,
- event.value
- );
- }
- } else if event.usage_page >= 0xFF00 {
- // Ignore vendor defined event
- } else {
- log::info!(
- "unsupported usage 0x{:X}:0x{:X} value {}",
- event.usage_page,
- event.usage,
- event.value
- );
+ }
+ Err(err) => {
+ log::warn!("failed to parse HID report: {}", err);
}
}
if mouse_pos != last_mouse_pos {
last_mouse_pos = mouse_pos;
-
- // TODO
// ps2d uses 0..=65535 as range, while usb uses 0..=32767. orbital
// expects the former range, so multiply by two here to temporarily
- // align with orbital expectation. This workaround will make cursor
- // looks out of sync in QEMU using virtio-vga with usb-tablet.
+ // align with orbital expectation.
let mouse_event = orbclient::event::MouseEvent {
x: mouse_pos.0 * 2,
y: mouse_pos.1 * 2,
};
-
- match display.write_event(mouse_event.to_event()) {
- Ok(_) => (),
- Err(err) => {
- log::warn!("failed to send mouse event to orbital: {}", err);
- }
+ if let Err(err) = display.write_event(mouse_event.to_event()) {
+ log::warn!("failed to send mouse event: {}", err);
}
}
if mouse_dx != 0 || mouse_dy != 0 {
- // TODO: This is a filter to prevent random mouse jumps
if mouse_dx > -127 && mouse_dx < 127 {
let mouse_event = orbclient::event::MouseRelativeEvent {
dx: mouse_dx,
dy: mouse_dy,
};
-
- match display.write_event(mouse_event.to_event()) {
- Ok(_) => (),
- Err(err) => {
- log::warn!("failed to send mouse event to orbital: {}", err);
- }
+ if let Err(err) = display.write_event(mouse_event.to_event()) {
+ log::warn!("failed to send relative mouse event: {}", err);
}
}
}
if scroll_y != 0 {
let scroll_event = orbclient::event::ScrollEvent { x: 0, y: scroll_y };
-
- match display.write_event(scroll_event.to_event()) {
- Ok(_) => (),
- Err(err) => {
- log::warn!("failed to send scroll event to orbital: {}", err);
- }
+ if let Err(err) = display.write_event(scroll_event.to_event()) {
+ log::warn!("failed to send scroll event: {}", err);
}
}
if buttons != last_buttons {
last_buttons = buttons;
-
let button_event = orbclient::event::ButtonEvent {
left: buttons[0],
right: buttons[1],
middle: buttons[2],
};
-
- match display.write_event(button_event.to_event()) {
- Ok(_) => (),
- Err(err) => {
- log::warn!("failed to send button event to orbital: {}", err);
- }
+ if let Err(err) = display.write_event(button_event.to_event()) {
+ log::warn!("failed to send button event: {}", err);
}
}
-
- // log::trace!("took {}ms", timer.elapsed().as_millis())
}
}
+28
View File
@@ -0,0 +1,28 @@
# Absorbed Patches
These patches have been **consolidated into `local/patches/base/redox.patch`** (the
mega-patch applied automatically to the base recipe source tree).
**Do not wire these patches into `recipes/core/base/recipe.toml`.** They are kept here
for reference and git history only. If a patch referenced by a recipe.toml symlink
breaks, that symlink should be updated to point to a current, active patch in
`local/patches/base/` (NOT one in this directory).
## Consolidation timeline
| Date | Action |
|------|--------|
| 2026-04-30 | P0 + P2 patches consolidated into redox.patch |
| 2026-04-30 | P1-P2 driver/ACPI patches consolidated |
| 2026-04-30 | P3 ACPI/PCI patches absorbed |
## Active patches (NOT in this directory)
The active patches applied on top of `redox.patch` live in `local/patches/base/`:
- `P3-ps2d-led-feedback.patch` — PS/2 LED state + InputProducer migration
- `P3-inputd-keymap-bridge.patch` — InputProducer enum + keymap bridge
- `P3-usbhidd-hardening.patch` — USB HID descriptor validation, retry, lookup table
- `P3-init-colored-output.patch` — ANSI-colored init daemon output
- `P9-fix-so-pecred.patch` — shared-object credential fix
- `redox.patch` — cumulative mega-patch (applied first, automatically)