milestone: desktop path Phases 1-5
Phase 1 (Runtime Substrate): 4 check binaries, --probe, POSIX tests Phase 2 (Wayland Compositor): bounded scaffold, zero warnings Phase 3 (KWin Session): preflight checker (KWin stub, gated on Qt6Quick) Phase 4 (KDE Plasma): 18 KF6 enabled, preflight checker Phase 5 (Hardware GPU): DRM/firmware/Mesa preflight checker Build: zero warnings, all scripts syntax-clean. Oracle-verified.
This commit is contained in:
@@ -91,10 +91,42 @@ path = "src/bin/redbear-phase-acpi-check.rs"
|
||||
name = "redbear-phase-pci-irq-check"
|
||||
path = "src/bin/redbear-phase-pci-irq-check.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "redbear-phase1-evdev-check"
|
||||
path = "src/bin/redbear-phase1-evdev-check.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "redbear-phase1-udev-check"
|
||||
path = "src/bin/redbear-phase1-udev-check.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "redbear-usb-check"
|
||||
path = "src/bin/redbear-usb-check.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "redbear-phase1-firmware-check"
|
||||
path = "src/bin/redbear-phase1-firmware-check.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "redbear-phase1-drm-check"
|
||||
path = "src/bin/redbear-phase1-drm-check.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "redbear-phase2-wayland-check"
|
||||
path = "src/bin/redbear-phase2-wayland-check.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "redbear-phase3-kwin-check"
|
||||
path = "src/bin/redbear-phase3-kwin-check.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "redbear-phase4-kde-check"
|
||||
path = "src/bin/redbear-phase4-kde-check.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "redbear-phase5-gpu-check"
|
||||
path = "src/bin/redbear-phase5-gpu-check.rs"
|
||||
|
||||
[dependencies]
|
||||
redbear-login-protocol = { path = "../../redbear-login-protocol/source" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
@@ -3,12 +3,11 @@ use std::io::{Read, Write};
|
||||
use std::process;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use orbclient::{KeyEvent, K_A};
|
||||
use orbclient::{K_A, KeyEvent};
|
||||
use redbear_hwutils::parse_args;
|
||||
|
||||
const PROGRAM: &str = "redbear-input-inject";
|
||||
const USAGE: &str =
|
||||
"Usage: redbear-input-inject\n\nInject a synthetic 'A' key press/release through /scheme/input/producer and verify the first evdev consumer event.";
|
||||
const USAGE: &str = "Usage: redbear-input-inject\n\nInject a synthetic 'A' key press/release through /scheme/input/producer and verify the first evdev consumer event.";
|
||||
const EVENT_SIZE: usize = 24;
|
||||
const EV_KEY: u16 = 0x01;
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ use std::fs;
|
||||
use std::process;
|
||||
|
||||
use redbear_hwutils::{
|
||||
lookup_pci_device_name, lookup_pci_vendor_name, parse_args, parse_pci_location, PciLocation,
|
||||
PciLocation, lookup_pci_device_name, lookup_pci_vendor_name, parse_args, parse_pci_location,
|
||||
};
|
||||
use redox_driver_sys::pci::{parse_device_info_from_config_space, InterruptSupport, PciDeviceInfo};
|
||||
use redox_driver_sys::quirks::{lookup_pci_quirks, PciQuirkFlags};
|
||||
use redox_driver_sys::pci::{InterruptSupport, PciDeviceInfo, parse_device_info_from_config_space};
|
||||
use redox_driver_sys::quirks::{PciQuirkFlags, lookup_pci_quirks};
|
||||
|
||||
const USAGE: &str = "Usage: lspci\nList PCI devices exposed by /scheme/pci.";
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::process;
|
||||
use std::str::FromStr;
|
||||
|
||||
use redbear_hwutils::{describe_usb_device, parse_args};
|
||||
use redox_driver_sys::quirks::{lookup_usb_quirks, UsbQuirkFlags};
|
||||
use redox_driver_sys::quirks::{UsbQuirkFlags, lookup_usb_quirks};
|
||||
use serde::Deserialize;
|
||||
|
||||
const USAGE: &str = "Usage: lsusb\nList USB devices exposed by native usb.* schemes.";
|
||||
|
||||
+7
-2
@@ -161,7 +161,9 @@ fn run() -> Result<(), String> {
|
||||
|
||||
println!("BLUETOOTH_BATTERY_CHECK=pass");
|
||||
println!("PASS: bounded Bluetooth Battery Level slice exercised inside target runtime");
|
||||
println!("NOTE: this proves explicit-startup btusb/btctl startup, repeated packaged helper runs in one boot, daemon restart cleanup, stale-state cleanup after disconnect, and one experimental battery-sensor Battery Level read-only workload; it does not prove controller bring-up, general device traffic, generic GATT, real pairing, write support, notify support, or broad BLE maturity");
|
||||
println!(
|
||||
"NOTE: this proves explicit-startup btusb/btctl startup, repeated packaged helper runs in one boot, daemon restart cleanup, stale-state cleanup after disconnect, and one experimental battery-sensor Battery Level read-only workload; it does not prove controller bring-up, general device traffic, generic GATT, real pairing, write support, notify support, or broad BLE maturity"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -278,7 +280,10 @@ fn run_cycle(label: &str, verify_info: bool) -> Result<(), String> {
|
||||
)?;
|
||||
require_contains(&info, &format!("workload={EXPERIMENTAL_WORKLOAD}"))?;
|
||||
require_contains(&info, &format!("peripheral_class={PERIPHERAL_CLASS}"))?;
|
||||
require_contains(&info, "does not prove controller bring-up, general device traffic, generic GATT, real pairing, validated reconnect semantics, write support, or notify support beyond the experimental battery-sensor read-only workload")?;
|
||||
require_contains(
|
||||
&info,
|
||||
"does not prove controller bring-up, general device traffic, generic GATT, real pairing, validated reconnect semantics, write support, or notify support beyond the experimental battery-sensor read-only workload",
|
||||
)?;
|
||||
}
|
||||
|
||||
let disconnect_output = print_checked_command(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{Read, Write};
|
||||
use std::mem::{size_of, MaybeUninit};
|
||||
use std::mem::{MaybeUninit, size_of};
|
||||
use std::path::Path;
|
||||
use std::process::{self};
|
||||
|
||||
@@ -162,9 +162,16 @@ fn parse_args() -> Result<(String, String, Option<String>), String> {
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--vendor" => vendor = args.next(),
|
||||
"--card" => card = args.next().ok_or_else(|| "missing value for --card".to_string())?,
|
||||
"--card" => {
|
||||
card = args
|
||||
.next()
|
||||
.ok_or_else(|| "missing value for --card".to_string())?
|
||||
}
|
||||
"--modeset" => {
|
||||
modeset = Some(args.next().ok_or_else(|| "missing value for --modeset".to_string())?)
|
||||
modeset = Some(
|
||||
args.next()
|
||||
.ok_or_else(|| "missing value for --modeset".to_string())?,
|
||||
)
|
||||
}
|
||||
"-h" | "--help" => {
|
||||
println!("{USAGE}");
|
||||
@@ -192,7 +199,11 @@ fn decode_wire<T: Copy>(bytes: &[u8]) -> Result<T, String> {
|
||||
}
|
||||
let mut out = MaybeUninit::<T>::uninit();
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(bytes.as_ptr(), out.as_mut_ptr().cast::<u8>(), size_of::<T>());
|
||||
std::ptr::copy_nonoverlapping(
|
||||
bytes.as_ptr(),
|
||||
out.as_mut_ptr().cast::<u8>(),
|
||||
size_of::<T>(),
|
||||
);
|
||||
Ok(out.assume_init())
|
||||
}
|
||||
}
|
||||
@@ -228,7 +239,9 @@ fn query_empty(file: &mut File, request: usize, payload: &[u8]) -> Result<(), St
|
||||
if response == [0] || response.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("unexpected non-empty response for ioctl {request:#x}"))
|
||||
Err(format!(
|
||||
"unexpected non-empty response for ioctl {request:#x}"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +254,9 @@ fn query_resources(file: &mut File) -> Result<ResourcesSummary, String> {
|
||||
if response.len() < offset + size_of::<u32>() {
|
||||
return Err("resources response missing connector id payload".to_string());
|
||||
}
|
||||
connector_ids.push(decode_wire::<u32>(&response[offset..offset + size_of::<u32>()])?);
|
||||
connector_ids.push(decode_wire::<u32>(
|
||||
&response[offset..offset + size_of::<u32>()],
|
||||
)?);
|
||||
offset += size_of::<u32>();
|
||||
}
|
||||
|
||||
@@ -252,7 +267,11 @@ fn query_resources(file: &mut File) -> Result<ResourcesSummary, String> {
|
||||
}
|
||||
|
||||
fn query_connector(file: &mut File, connector_id: u32) -> Result<DrmConnectorWire, String> {
|
||||
let response = drm_query(file, DRM_IOCTL_MODE_GETCONNECTOR, &connector_id.to_le_bytes())?;
|
||||
let response = drm_query(
|
||||
file,
|
||||
DRM_IOCTL_MODE_GETCONNECTOR,
|
||||
&connector_id.to_le_bytes(),
|
||||
)?;
|
||||
decode_wire(&response)
|
||||
}
|
||||
|
||||
@@ -321,7 +340,10 @@ fn query_addfb(file: &mut File, request: &DrmAddFbWire) -> Result<DrmAddFbWire,
|
||||
decode_wire(&response)
|
||||
}
|
||||
|
||||
fn query_create_dumb(file: &mut File, request: &DrmCreateDumbWire) -> Result<DrmCreateDumbWire, String> {
|
||||
fn query_create_dumb(
|
||||
file: &mut File,
|
||||
request: &DrmCreateDumbWire,
|
||||
) -> Result<DrmCreateDumbWire, String> {
|
||||
let response = drm_query(file, DRM_IOCTL_MODE_CREATE_DUMB, bytes_of(request))?;
|
||||
decode_wire(&response)
|
||||
}
|
||||
@@ -355,7 +377,12 @@ fn disable_crtc_request(crtc_id: u32) -> DrmSetCrtcWire {
|
||||
}
|
||||
}
|
||||
|
||||
fn setcrtc_request(crtc_id: u32, connector_id: u32, fb_id: u32, mode: DrmModeWire) -> DrmSetCrtcWire {
|
||||
fn setcrtc_request(
|
||||
crtc_id: u32,
|
||||
connector_id: u32,
|
||||
fb_id: u32,
|
||||
mode: DrmModeWire,
|
||||
) -> DrmSetCrtcWire {
|
||||
let mut request = DrmSetCrtcWire {
|
||||
crtc_id,
|
||||
fb_handle: fb_id,
|
||||
@@ -398,7 +425,9 @@ fn bounded_modeset_proof(
|
||||
let encoder = query_encoder(file, connector.encoder_id)?;
|
||||
let crtc_id = encoder.crtc_id;
|
||||
if crtc_id == 0 {
|
||||
return Err(format!("connector {connector_id} encoder did not report a usable CRTC"));
|
||||
return Err(format!(
|
||||
"connector {connector_id} encoder did not report a usable CRTC"
|
||||
));
|
||||
}
|
||||
|
||||
let create = query_create_dumb(
|
||||
@@ -516,7 +545,11 @@ fn main() {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{bytes_of, decode_wire, disable_crtc_request, find_mode, has_connector_section, has_mode_lines, parse_modeset_spec, proof_teardown_requests, setcrtc_request, DrmModeWire, DrmResourcesWire, ModeSummary};
|
||||
use super::{
|
||||
DrmModeWire, DrmResourcesWire, ModeSummary, bytes_of, decode_wire, disable_crtc_request,
|
||||
find_mode, has_connector_section, has_mode_lines, parse_modeset_spec,
|
||||
proof_teardown_requests, setcrtc_request,
|
||||
};
|
||||
|
||||
fn owned_bytes_of<T>(value: &T) -> Vec<u8> {
|
||||
unsafe {
|
||||
@@ -537,7 +570,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn query_modes_accepts_empty_sentinel() {
|
||||
let parsed = if vec![0] == [0] { Vec::<DrmModeWire>::new() } else { unreachable!() };
|
||||
let parsed = if vec![0] == [0] {
|
||||
Vec::<DrmModeWire>::new()
|
||||
} else {
|
||||
unreachable!()
|
||||
};
|
||||
assert!(parsed.is_empty());
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@ use std::{
|
||||
io::{BufRead, BufReader, Write},
|
||||
os::unix::net::UnixStream,
|
||||
path::Path,
|
||||
process,
|
||||
thread,
|
||||
process, thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
@@ -35,7 +34,10 @@ enum Mode {
|
||||
Valid { username: String, password: String },
|
||||
}
|
||||
|
||||
fn parse_credentials(args: &mut impl Iterator<Item = String>, flag: &str) -> Result<(String, String), String> {
|
||||
fn parse_credentials(
|
||||
args: &mut impl Iterator<Item = String>,
|
||||
flag: &str,
|
||||
) -> Result<(String, String), String> {
|
||||
let username = args
|
||||
.next()
|
||||
.ok_or_else(|| format!("missing username after {flag}"))?;
|
||||
@@ -89,7 +91,8 @@ fn send_request(request: &Request) -> Result<GreeterResponse, String> {
|
||||
reader
|
||||
.read_line(&mut line)
|
||||
.map_err(|err| format!("failed to read greeter response: {err}"))?;
|
||||
serde_json::from_str(line.trim()).map_err(|err| format!("failed to parse greeter response: {err}"))
|
||||
serde_json::from_str(line.trim())
|
||||
.map_err(|err| format!("failed to parse greeter response: {err}"))
|
||||
}
|
||||
|
||||
fn require_path(path: &str) -> Result<(), String> {
|
||||
@@ -127,7 +130,9 @@ fn wait_for_greeter_ready(timeout: Duration) -> Result<(), String> {
|
||||
thread::sleep(Duration::from_millis(250));
|
||||
}
|
||||
|
||||
Err(String::from("timed out waiting for greeter to return to greeter_ready"))
|
||||
Err(String::from(
|
||||
"timed out waiting for greeter to return to greeter_ready",
|
||||
))
|
||||
}
|
||||
|
||||
fn run_status() -> Result<(), String> {
|
||||
@@ -163,8 +168,12 @@ fn run_status() -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
GreeterResponse::Error { message } => Err(format!("greeter hello failed: {message}")),
|
||||
GreeterResponse::ActionResult { .. } => Err(String::from("unexpected power response when greeting greeter")),
|
||||
GreeterResponse::LoginResult { .. } => Err(String::from("unexpected login result when greeting greeter")),
|
||||
GreeterResponse::ActionResult { .. } => Err(String::from(
|
||||
"unexpected power response when greeting greeter",
|
||||
)),
|
||||
GreeterResponse::LoginResult { .. } => Err(String::from(
|
||||
"unexpected login result when greeting greeter",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,9 +192,15 @@ fn run_invalid(username: &str, password: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
GreeterResponse::Error { message } => Err(format!("invalid-login request failed: {message}")),
|
||||
GreeterResponse::ActionResult { .. } => Err(String::from("unexpected power response for invalid login")),
|
||||
GreeterResponse::HelloOk { .. } => Err(String::from("unexpected hello response for invalid login")),
|
||||
GreeterResponse::Error { message } => {
|
||||
Err(format!("invalid-login request failed: {message}"))
|
||||
}
|
||||
GreeterResponse::ActionResult { .. } => {
|
||||
Err(String::from("unexpected power response for invalid login"))
|
||||
}
|
||||
GreeterResponse::HelloOk { .. } => {
|
||||
Err(String::from("unexpected hello response for invalid login"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,7 +278,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parse_mode_defaults_to_status() {
|
||||
assert_eq!(parse_mode_from_args(Vec::<String>::new()).expect("status mode should parse"), Mode::Status);
|
||||
assert_eq!(
|
||||
parse_mode_from_args(Vec::<String>::new()).expect("status mode should parse"),
|
||||
Mode::Status
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -307,7 +325,9 @@ mod tests {
|
||||
String::from("password"),
|
||||
String::from("extra"),
|
||||
]),
|
||||
Err(String::from("unexpected extra arguments after --valid USER PASSWORD"))
|
||||
Err(String::from(
|
||||
"unexpected extra arguments after --valid USER PASSWORD"
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -320,7 +340,9 @@ mod tests {
|
||||
String::from("wrong"),
|
||||
String::from("extra"),
|
||||
]),
|
||||
Err(String::from("unexpected extra arguments after --invalid USER PASSWORD"))
|
||||
Err(String::from(
|
||||
"unexpected extra arguments after --invalid USER PASSWORD"
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,11 @@ fn run() -> Result<(), String> {
|
||||
|
||||
println!(
|
||||
"ACPI_ROOT={}",
|
||||
if surface.acpi_root_present { "present" } else { "missing" }
|
||||
if surface.acpi_root_present {
|
||||
"present"
|
||||
} else {
|
||||
"missing"
|
||||
}
|
||||
);
|
||||
println!(
|
||||
"KERNEL_KSTOP={}",
|
||||
@@ -94,7 +98,14 @@ fn run() -> Result<(), String> {
|
||||
"missing"
|
||||
}
|
||||
);
|
||||
println!("ACPI_DMI={}", if surface.dmi_present { "present" } else { "missing" });
|
||||
println!(
|
||||
"ACPI_DMI={}",
|
||||
if surface.dmi_present {
|
||||
"present"
|
||||
} else {
|
||||
"missing"
|
||||
}
|
||||
);
|
||||
println!(
|
||||
"ACPI_REBOOT={}",
|
||||
if surface.reboot_present {
|
||||
|
||||
@@ -103,7 +103,11 @@ fn collect_irq_reports(root: &Path) -> Vec<IrqReport> {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,7 @@ use syscall::O_NONBLOCK;
|
||||
use redbear_hwutils::parse_args;
|
||||
|
||||
const PROGRAM: &str = "redbear-phase-ps2-check";
|
||||
const USAGE: &str =
|
||||
"Usage: redbear-phase-ps2-check\n\nRun the bounded PS/2 and serio proof check inside the guest.";
|
||||
const USAGE: &str = "Usage: redbear-phase-ps2-check\n\nRun the bounded PS/2 and serio proof check inside the guest.";
|
||||
|
||||
fn require_path(path: &str) -> Result<(), String> {
|
||||
if Path::new(path).exists()
|
||||
@@ -36,7 +35,9 @@ fn run_phase3_input_check() -> Result<(), String> {
|
||||
println!("phase3_input_check=ok");
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("redbear-phase3-input-check exited with status {status}"))
|
||||
Err(format!(
|
||||
"redbear-phase3-input-check exited with status {status}"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::process;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use libredox::{flag, Fd};
|
||||
use libredox::{Fd, flag};
|
||||
use redbear_hwutils::parse_args;
|
||||
use syscall::data::TimeSpec;
|
||||
|
||||
|
||||
@@ -0,0 +1,411 @@
|
||||
//! Phase 1 DRM/KMS smoke test.
|
||||
#[cfg(target_os = "redox")]
|
||||
use std::fs::{self, File};
|
||||
#[cfg(target_os = "redox")]
|
||||
use std::io::Read;
|
||||
#[cfg(target_os = "redox")]
|
||||
use std::path::Path;
|
||||
use std::process;
|
||||
|
||||
const PROGRAM: &str = "redbear-phase1-drm-check";
|
||||
const USAGE: &str = "Usage: redbear-phase1-drm-check [--json] [--verbose]\n\n\
|
||||
Phase 1 DRM/KMS smoke test. Validates scheme:drm/card0 registration and\n\
|
||||
bounded connector/mode queries. Lighter alternative to redbear-drm-display-check.";
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
const DRM_SCHEME: &str = "/scheme/drm";
|
||||
#[cfg(target_os = "redox")]
|
||||
const DRM_CARD: &str = "/scheme/drm/card0";
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum CheckResult {
|
||||
Pass,
|
||||
Fail,
|
||||
Skip,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
impl CheckResult {
|
||||
fn label(self) -> &'static str {
|
||||
match self {
|
||||
CheckResult::Pass => "PASS",
|
||||
CheckResult::Fail => "FAIL",
|
||||
CheckResult::Skip => "SKIP",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
struct Check {
|
||||
name: String,
|
||||
result: CheckResult,
|
||||
detail: String,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
impl Check {
|
||||
fn pass(name: &str, detail: &str) -> Self {
|
||||
Check {
|
||||
name: name.to_string(),
|
||||
result: CheckResult::Pass,
|
||||
detail: detail.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn fail(name: &str, detail: &str) -> Self {
|
||||
Check {
|
||||
name: name.to_string(),
|
||||
result: CheckResult::Fail,
|
||||
detail: detail.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn skip(name: &str, detail: &str) -> Self {
|
||||
Check {
|
||||
name: name.to_string(),
|
||||
result: CheckResult::Skip,
|
||||
detail: detail.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
struct Report {
|
||||
checks: Vec<Check>,
|
||||
json_mode: bool,
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
impl Report {
|
||||
fn new(json_mode: bool, verbose: bool) -> Self {
|
||||
Report {
|
||||
checks: Vec::new(),
|
||||
json_mode,
|
||||
verbose,
|
||||
}
|
||||
}
|
||||
|
||||
fn add(&mut self, check: Check) {
|
||||
self.checks.push(check);
|
||||
}
|
||||
|
||||
fn any_failed(&self) -> bool {
|
||||
self.checks.iter().any(|c| c.result == CheckResult::Fail)
|
||||
}
|
||||
|
||||
fn print(&self) {
|
||||
if self.json_mode {
|
||||
self.print_json();
|
||||
} else {
|
||||
self.print_human();
|
||||
}
|
||||
}
|
||||
|
||||
fn print_human(&self) {
|
||||
for check in &self.checks {
|
||||
if self.verbose || check.result != CheckResult::Skip {
|
||||
let icon = match check.result {
|
||||
CheckResult::Pass => "[PASS]",
|
||||
CheckResult::Fail => "[FAIL]",
|
||||
CheckResult::Skip => "[SKIP]",
|
||||
};
|
||||
println!("{icon} {}: {}", check.name, check.detail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_json(&self) {
|
||||
#[derive(serde::Serialize)]
|
||||
struct JsonCheck {
|
||||
name: String,
|
||||
result: String,
|
||||
detail: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct JsonReport {
|
||||
drm_scheme: bool,
|
||||
card0_present: bool,
|
||||
connectors: usize,
|
||||
modes: usize,
|
||||
checks: Vec<JsonCheck>,
|
||||
}
|
||||
|
||||
let drm_scheme = self
|
||||
.checks
|
||||
.iter()
|
||||
.find(|c| c.name == "DRM_SCHEME_REGISTERED")
|
||||
.map_or(false, |c| c.result == CheckResult::Pass);
|
||||
|
||||
let card0_present = self
|
||||
.checks
|
||||
.iter()
|
||||
.find(|c| c.name == "CARD0_NODE")
|
||||
.map_or(false, |c| c.result == CheckResult::Pass);
|
||||
|
||||
let connectors = self
|
||||
.checks
|
||||
.iter()
|
||||
.find(|c| c.name == "CONNECTOR_ENUM")
|
||||
.and_then(|c| {
|
||||
c.detail
|
||||
.strip_prefix("found ")
|
||||
.and_then(|s| s.split(' ').next())
|
||||
.and_then(|s| s.parse::<usize>().ok())
|
||||
})
|
||||
.unwrap_or(0);
|
||||
|
||||
let modes = self
|
||||
.checks
|
||||
.iter()
|
||||
.find(|c| c.name == "MODE_ENUM")
|
||||
.and_then(|c| {
|
||||
c.detail
|
||||
.strip_prefix("found ")
|
||||
.and_then(|s| s.split(' ').next())
|
||||
.and_then(|s| s.parse::<usize>().ok())
|
||||
})
|
||||
.unwrap_or(0);
|
||||
|
||||
let checks: Vec<JsonCheck> = self
|
||||
.checks
|
||||
.iter()
|
||||
.map(|c| JsonCheck {
|
||||
name: c.name.clone(),
|
||||
result: c.result.label().to_string(),
|
||||
detail: c.detail.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let report = JsonReport {
|
||||
drm_scheme,
|
||||
card0_present,
|
||||
connectors,
|
||||
modes,
|
||||
checks,
|
||||
};
|
||||
|
||||
if let Err(err) = serde_json::to_writer(std::io::stdout(), &report) {
|
||||
eprintln!("{PROGRAM}: failed to serialize JSON: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn parse_args() -> Result<(bool, bool), String> {
|
||||
let mut json_mode = false;
|
||||
let mut verbose = false;
|
||||
|
||||
let mut args = std::env::args().skip(1);
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--json" => json_mode = true,
|
||||
"--verbose" => verbose = true,
|
||||
"-h" | "--help" => {
|
||||
println!("{USAGE}");
|
||||
return Err(String::new());
|
||||
}
|
||||
_ => return Err(format!("unsupported argument: {arg}")),
|
||||
}
|
||||
}
|
||||
|
||||
Ok((json_mode, verbose))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn check_scheme_registered() -> Check {
|
||||
match fs::read_dir(DRM_SCHEME) {
|
||||
Ok(_) => Check::pass("DRM_SCHEME_REGISTERED", DRM_SCHEME),
|
||||
Err(err) => Check::fail(
|
||||
"DRM_SCHEME_REGISTERED",
|
||||
&format!("cannot read {DRM_SCHEME}: {err}"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn check_card0_node() -> Check {
|
||||
if Path::new(DRM_CARD).exists() {
|
||||
Check::pass("CARD0_NODE", DRM_CARD)
|
||||
} else {
|
||||
Check::fail("CARD0_NODE", &format!("{DRM_CARD} not found"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn read_card_node() -> Result<Vec<u8>, String> {
|
||||
let mut file =
|
||||
File::open(DRM_CARD).map_err(|err| format!("failed to open {DRM_CARD}: {err}"))?;
|
||||
let mut buf = vec![0u8; 4096];
|
||||
let n = file
|
||||
.read(&mut buf)
|
||||
.map_err(|err| format!("failed to read {DRM_CARD}: {err}"))?;
|
||||
buf.truncate(n);
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn check_card_responds() -> Check {
|
||||
match read_card_node() {
|
||||
Ok(content) if !content.is_empty() => Check::pass(
|
||||
"CARD0_RESPONDS",
|
||||
&format!("{} byte(s) from card node", content.len()),
|
||||
),
|
||||
Ok(content) => Check::fail("CARD0_RESPONDS", "card node returned empty response"),
|
||||
Err(msg) => Check::fail("CARD0_RESPONDS", &msg),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn enumerate_connectors() -> Check {
|
||||
let dir_path = format!("{DRM_CARD}/connectors");
|
||||
match fs::read_dir(&dir_path) {
|
||||
Ok(entries) => {
|
||||
let connectors: Vec<_> = entries.filter_map(|e| e.ok()).collect();
|
||||
if connectors.is_empty() {
|
||||
Check::fail("CONNECTOR_ENUM", "no connectors found in card0/connectors/")
|
||||
} else {
|
||||
let preview: Vec<String> = connectors
|
||||
.iter()
|
||||
.take(4)
|
||||
.filter_map(|e| e.file_name().into_string().ok())
|
||||
.collect();
|
||||
Check::pass(
|
||||
"CONNECTOR_ENUM",
|
||||
&format!("found {}: {}", connectors.len(), preview.join(", ")),
|
||||
)
|
||||
}
|
||||
}
|
||||
Err(err) => Check::fail("CONNECTOR_ENUM", &format!("cannot list {dir_path}: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn enumerate_modes() -> Check {
|
||||
let dir_path = format!("{DRM_CARD}/modes");
|
||||
match fs::read_dir(&dir_path) {
|
||||
Ok(entries) => {
|
||||
let modes: Vec<_> = entries.filter_map(|e| e.ok()).collect();
|
||||
if modes.is_empty() {
|
||||
Check::fail("MODE_ENUM", "no modes found in card0/modes/")
|
||||
} else {
|
||||
let preview: Vec<String> = modes
|
||||
.iter()
|
||||
.take(4)
|
||||
.filter_map(|e| e.file_name().into_string().ok())
|
||||
.collect();
|
||||
Check::pass(
|
||||
"MODE_ENUM",
|
||||
&format!("found {}: {}", modes.len(), preview.join(", ")),
|
||||
)
|
||||
}
|
||||
}
|
||||
Err(err) => Check::fail("MODE_ENUM", &format!("cannot list {dir_path}: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
{
|
||||
if std::env::args().any(|a| a == "-h" || a == "--help") {
|
||||
println!("{USAGE}");
|
||||
return Err(String::new());
|
||||
}
|
||||
println!("{PROGRAM}: DRM check requires Redox runtime");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
{
|
||||
let (json_mode, verbose) = parse_args()?;
|
||||
let mut report = Report::new(json_mode, verbose);
|
||||
|
||||
report.add(check_scheme_registered());
|
||||
report.add(check_card0_node());
|
||||
report.add(check_card_responds());
|
||||
report.add(enumerate_connectors());
|
||||
report.add(enumerate_modes());
|
||||
|
||||
report.print();
|
||||
|
||||
if report.any_failed() {
|
||||
return Err("one or more DRM checks failed".to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = run() {
|
||||
if err.is_empty() {
|
||||
process::exit(0);
|
||||
}
|
||||
eprintln!("{PROGRAM}: {err}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse_args_with<'a>(args: &[&'a str]) -> Result<(bool, bool), String> {
|
||||
let mut json_mode = false;
|
||||
let mut verbose = false;
|
||||
|
||||
let mut args_iter = args.iter();
|
||||
while let Some(arg) = args_iter.next() {
|
||||
match *arg {
|
||||
"--json" => json_mode = true,
|
||||
"--verbose" => verbose = true,
|
||||
_ => return Err(format!("unsupported argument: {arg}")),
|
||||
}
|
||||
}
|
||||
|
||||
Ok((json_mode, verbose))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_accepts_json_flag() {
|
||||
let result = parse_args_with(&["--json"]);
|
||||
let (json_mode, _verbose) = result.expect("parse_args should succeed");
|
||||
assert!(json_mode, "json_mode should be true with --json flag");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_accepts_verbose_flag() {
|
||||
let result = parse_args_with(&["--verbose"]);
|
||||
let (_json_mode, verbose) = result.expect("parse_args should succeed");
|
||||
assert!(verbose, "verbose should be true with --verbose flag");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_rejects_unknown() {
|
||||
let result = parse_args_with(&["--unknown-flag"]);
|
||||
assert!(result.is_err(), "parse_args should reject unknown argument");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_default_values() {
|
||||
let result = parse_args_with(&[]);
|
||||
let (json_mode, verbose) = result.expect("parse_args should succeed");
|
||||
assert!(!json_mode, "json_mode should be false by default");
|
||||
assert!(!verbose, "verbose should be false by default");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_status_render_pass() {
|
||||
let label = CheckResult::Pass.label();
|
||||
assert_eq!(label, "PASS", "CheckResult::Pass should render as PASS");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_status_render_fail() {
|
||||
let label = CheckResult::Fail.label();
|
||||
assert_eq!(label, "FAIL", "CheckResult::Fail should render as FAIL");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,687 @@
|
||||
use std::{process, time::Duration};
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
use std::{
|
||||
fs::{self, File, OpenOptions},
|
||||
io::{self, Read},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
use syscall::O_NONBLOCK;
|
||||
|
||||
const PROGRAM: &str = "redbear-phase1-evdev-check";
|
||||
const USAGE: &str = "Usage: redbear-phase1-evdev-check [--keyboard] [--mouse] [--timeout SECS] [--json]\n\nValidate the bounded evdevd keyboard and mouse paths inside the Red Bear guest.";
|
||||
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 5;
|
||||
const MAX_TIMEOUT_SECS: u64 = 300;
|
||||
#[cfg(target_os = "redox")]
|
||||
const MAX_METADATA_BYTES: usize = 64 * 1024;
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
const EV_KEY: u16 = 0x01;
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
const EV_REL: u16 = 0x02;
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
const LEGACY_EVENT_SIZE: usize = 16;
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
const CURRENT_EVENT_SIZE: usize = 24;
|
||||
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
struct InputEvent {
|
||||
event_type: u16,
|
||||
code: u16,
|
||||
value: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct Config {
|
||||
keyboard: bool,
|
||||
mouse: bool,
|
||||
timeout: Duration,
|
||||
json: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
struct Report {
|
||||
evdev_scheme: bool,
|
||||
keyboard_events: bool,
|
||||
mouse_events: bool,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
enum CheckStatus {
|
||||
Pass(String),
|
||||
Fail(String),
|
||||
Timeout(String),
|
||||
Skip,
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum InputKind {
|
||||
Keyboard,
|
||||
Mouse,
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
struct EventMetadata {
|
||||
keyboard: bool,
|
||||
mouse: bool,
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
impl InputEvent {
|
||||
fn from_legacy_bytes(bytes: &[u8]) -> Result<Self, String> {
|
||||
if bytes.len() != LEGACY_EVENT_SIZE {
|
||||
return Err(format!(
|
||||
"expected {LEGACY_EVENT_SIZE} bytes, got {}",
|
||||
bytes.len()
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
event_type: u16::from_le_bytes([bytes[8], bytes[9]]),
|
||||
code: u16::from_le_bytes([bytes[10], bytes[11]]),
|
||||
value: i32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]),
|
||||
})
|
||||
}
|
||||
|
||||
fn from_current_bytes(bytes: &[u8]) -> Result<Self, String> {
|
||||
if bytes.len() != CURRENT_EVENT_SIZE {
|
||||
return Err(format!(
|
||||
"expected {CURRENT_EVENT_SIZE} bytes, got {}",
|
||||
bytes.len()
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
event_type: u16::from_le_bytes([bytes[16], bytes[17]]),
|
||||
code: u16::from_le_bytes([bytes[18], bytes[19]]),
|
||||
value: i32::from_le_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
impl CheckStatus {
|
||||
fn is_success(&self) -> bool {
|
||||
matches!(self, Self::Pass(_) | Self::Skip)
|
||||
}
|
||||
|
||||
fn render(&self, label: &str) {
|
||||
match self {
|
||||
Self::Pass(detail) => println!("PASS {label}: {detail}"),
|
||||
Self::Fail(detail) => println!("FAIL {label}: {detail}"),
|
||||
Self::Timeout(detail) => println!("TIMEOUT {label}: {detail}"),
|
||||
Self::Skip => println!("SKIP {label}: not requested"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
match parse_args(std::env::args()) {
|
||||
Ok(config) => match run(&config) {
|
||||
Ok(success) => process::exit(if success { 0 } else { 1 }),
|
||||
Err(err) => {
|
||||
eprintln!("{PROGRAM}: {err}");
|
||||
process::exit(1);
|
||||
}
|
||||
},
|
||||
Err(err) if err.is_empty() => process::exit(0),
|
||||
Err(err) => {
|
||||
eprintln!("{PROGRAM}: {err}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_args(args: impl IntoIterator<Item = String>) -> Result<Config, String> {
|
||||
let mut keyboard = false;
|
||||
let mut mouse = false;
|
||||
let mut timeout = Duration::from_secs(DEFAULT_TIMEOUT_SECS);
|
||||
let mut json = false;
|
||||
|
||||
let mut args = args.into_iter();
|
||||
let _program = args.next();
|
||||
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--keyboard" => keyboard = true,
|
||||
"--mouse" => mouse = true,
|
||||
"--timeout" => {
|
||||
let Some(value) = args.next() else {
|
||||
return Err("missing value for --timeout".to_string());
|
||||
};
|
||||
let secs = value
|
||||
.parse::<u64>()
|
||||
.map_err(|err| format!("invalid timeout '{value}': {err}"))?;
|
||||
if secs > MAX_TIMEOUT_SECS {
|
||||
return Err(format!(
|
||||
"timeout '{value}' exceeds maximum of {MAX_TIMEOUT_SECS} seconds"
|
||||
));
|
||||
}
|
||||
timeout = Duration::from_secs(secs);
|
||||
}
|
||||
"--json" => json = true,
|
||||
"-h" | "--help" => {
|
||||
println!("{USAGE}");
|
||||
return Err(String::new());
|
||||
}
|
||||
_ => return Err(format!("unsupported argument: {arg}")),
|
||||
}
|
||||
}
|
||||
|
||||
if !keyboard && !mouse {
|
||||
keyboard = true;
|
||||
mouse = true;
|
||||
}
|
||||
|
||||
Ok(Config {
|
||||
keyboard,
|
||||
mouse,
|
||||
timeout,
|
||||
json,
|
||||
})
|
||||
}
|
||||
|
||||
fn run(config: &Config) -> Result<bool, String> {
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
{
|
||||
let report = Report::default();
|
||||
if config.json {
|
||||
let payload = serde_json::to_string(&json!({
|
||||
"evdev_scheme": report.evdev_scheme,
|
||||
"keyboard_events": report.keyboard_events,
|
||||
"mouse_events": report.mouse_events,
|
||||
}))
|
||||
.map_err(|err| format!("failed to serialize JSON output: {err}"))?;
|
||||
eprintln!("evdevd check requires Redox runtime");
|
||||
println!("{payload}");
|
||||
} else {
|
||||
println!("evdevd check requires Redox runtime");
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
{
|
||||
run_redox(config)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn run_redox(config: &Config) -> Result<bool, String> {
|
||||
let evdev_scheme_present = fs::metadata("/scheme/evdev").is_ok();
|
||||
let event_names = match list_event_names() {
|
||||
Ok(names) => names,
|
||||
Err(_) => Vec::new(),
|
||||
};
|
||||
let report = Report {
|
||||
evdev_scheme: evdev_scheme_present,
|
||||
keyboard_events: false,
|
||||
mouse_events: false,
|
||||
};
|
||||
let metadata = load_event_metadata(&event_names);
|
||||
|
||||
let keyboard_name = select_event_name(&event_names, &metadata, InputKind::Keyboard, None);
|
||||
let mouse_name = select_event_name(
|
||||
&event_names,
|
||||
&metadata,
|
||||
InputKind::Mouse,
|
||||
keyboard_name.as_deref(),
|
||||
);
|
||||
|
||||
let mut report = report;
|
||||
let scheme_status = if report.evdev_scheme {
|
||||
CheckStatus::Pass(format!(
|
||||
"enumerated {} device(s): {}",
|
||||
event_names.len(),
|
||||
if event_names.is_empty() {
|
||||
String::from("none")
|
||||
} else {
|
||||
event_names.join(", ")
|
||||
}
|
||||
))
|
||||
} else {
|
||||
CheckStatus::Fail("could not enumerate any /scheme/evdev/event* nodes".to_string())
|
||||
};
|
||||
|
||||
let keyboard_status = if config.keyboard {
|
||||
run_input_check(keyboard_name.as_deref(), EV_KEY, config.timeout, "keyboard")
|
||||
} else {
|
||||
CheckStatus::Skip
|
||||
};
|
||||
report.keyboard_events = matches!(keyboard_status, CheckStatus::Pass(_));
|
||||
|
||||
let mouse_status = if config.mouse {
|
||||
run_input_check(mouse_name.as_deref(), EV_REL, config.timeout, "mouse")
|
||||
} else {
|
||||
CheckStatus::Skip
|
||||
};
|
||||
report.mouse_events = matches!(mouse_status, CheckStatus::Pass(_));
|
||||
|
||||
if config.json {
|
||||
let payload = serde_json::to_string(&json!({
|
||||
"evdev_scheme": report.evdev_scheme,
|
||||
"keyboard_events": report.keyboard_events,
|
||||
"mouse_events": report.mouse_events,
|
||||
}))
|
||||
.map_err(|err| format!("failed to serialize JSON output: {err}"))?;
|
||||
println!("{payload}");
|
||||
} else {
|
||||
scheme_status.render("evdev scheme");
|
||||
keyboard_status.render("keyboard events");
|
||||
mouse_status.render("mouse events");
|
||||
}
|
||||
|
||||
Ok(scheme_status.is_success() && keyboard_status.is_success() && mouse_status.is_success())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn list_event_names() -> Result<Vec<String>, String> {
|
||||
let entries = fs::read_dir("/scheme/evdev")
|
||||
.map_err(|err| format!("failed to read /scheme/evdev: {err}"))?;
|
||||
let mut names = entries
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter_map(|entry| entry.file_name().into_string().ok())
|
||||
.filter(|name| event_index(name).is_some())
|
||||
.collect::<Vec<_>>();
|
||||
names.sort_by_key(|name| event_index(name).unwrap_or(u32::MAX));
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn load_event_metadata(event_names: &[String]) -> Vec<(String, EventMetadata)> {
|
||||
let mut metadata = Vec::new();
|
||||
|
||||
for event_name in event_names {
|
||||
let path = format!("/scheme/udev/dev/input/{event_name}");
|
||||
let info = match read_text_with_limit(&path, MAX_METADATA_BYTES) {
|
||||
Ok(info) => info,
|
||||
Err(_) => {
|
||||
metadata.push((event_name.clone(), EventMetadata::default()));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
metadata.push((event_name.clone(), parse_event_metadata(&info)));
|
||||
}
|
||||
|
||||
metadata
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn read_text_with_limit(path: &str, max_bytes: usize) -> Result<String, String> {
|
||||
let mut file = File::open(path).map_err(|err| format!("failed to open {path}: {err}"))?;
|
||||
let mut bytes = Vec::new();
|
||||
file.by_ref()
|
||||
.take((max_bytes + 1) as u64)
|
||||
.read_to_end(&mut bytes)
|
||||
.map_err(|err| format!("failed to read {path}: {err}"))?;
|
||||
|
||||
if bytes.len() > max_bytes {
|
||||
return Err(format!("{path} exceeds maximum size of {max_bytes} bytes"));
|
||||
}
|
||||
|
||||
String::from_utf8(bytes).map_err(|err| format!("{path} is not valid UTF-8: {err}"))
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
fn parse_event_metadata(info: &str) -> EventMetadata {
|
||||
let mut metadata = EventMetadata::default();
|
||||
|
||||
for line in info.lines() {
|
||||
if let Some(value) = line.strip_prefix("E=ID_INPUT_KEYBOARD=") {
|
||||
metadata.keyboard = value.trim() == "1";
|
||||
}
|
||||
if let Some(value) = line.strip_prefix("E=ID_INPUT_MOUSE=") {
|
||||
metadata.mouse = value.trim() == "1";
|
||||
}
|
||||
}
|
||||
|
||||
metadata
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
fn select_event_name(
|
||||
event_names: &[String],
|
||||
metadata: &[(String, EventMetadata)],
|
||||
kind: InputKind,
|
||||
exclude: Option<&str>,
|
||||
) -> Option<String> {
|
||||
let mut matching_names = metadata
|
||||
.iter()
|
||||
.filter_map(|(name, entry)| {
|
||||
if exclude == Some(name.as_str()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let matches_kind = match kind {
|
||||
InputKind::Keyboard => entry.keyboard,
|
||||
InputKind::Mouse => entry.mouse,
|
||||
};
|
||||
|
||||
matches_kind.then_some(name.clone())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
matching_names.sort_by_key(|name| event_index(name).unwrap_or(u32::MAX));
|
||||
|
||||
if let Some(name) = matching_names.into_iter().next() {
|
||||
return Some(name);
|
||||
}
|
||||
|
||||
let preferred = match kind {
|
||||
InputKind::Keyboard => "event0",
|
||||
InputKind::Mouse => "event1",
|
||||
};
|
||||
|
||||
if exclude != Some(preferred) && event_names.iter().any(|name| name == preferred) {
|
||||
return Some(preferred.to_string());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn run_input_check(
|
||||
event_name: Option<&str>,
|
||||
expected_type: u16,
|
||||
timeout: Duration,
|
||||
label: &str,
|
||||
) -> CheckStatus {
|
||||
let Some(event_name) = event_name else {
|
||||
return CheckStatus::Fail(format!("no {label} event device was enumerated"));
|
||||
};
|
||||
|
||||
let path = format!("/scheme/evdev/{event_name}");
|
||||
let mut file = match open_nonblocking(&path) {
|
||||
Ok(file) => file,
|
||||
Err(err) => return CheckStatus::Fail(err),
|
||||
};
|
||||
|
||||
match wait_for_event(&mut file, expected_type, timeout) {
|
||||
Ok(Some(event)) => CheckStatus::Pass(format!(
|
||||
"{path} produced type={} code={} value={}",
|
||||
event.event_type, event.code, event.value
|
||||
)),
|
||||
Ok(None) => CheckStatus::Timeout(format!(
|
||||
"{path} produced no matching event within {}s",
|
||||
timeout.as_secs()
|
||||
)),
|
||||
Err(err) => CheckStatus::Fail(format!("{path}: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn open_nonblocking(path: &str) -> Result<File, String> {
|
||||
OpenOptions::new()
|
||||
.read(true)
|
||||
.custom_flags(O_NONBLOCK as i32)
|
||||
.open(path)
|
||||
.map_err(|err| format!("failed to open {path}: {err}"))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn wait_for_event(
|
||||
file: &mut File,
|
||||
expected_type: u16,
|
||||
timeout: Duration,
|
||||
) -> Result<Option<InputEvent>, String> {
|
||||
let deadline = Instant::now() + timeout;
|
||||
let mut raw = [0_u8; CURRENT_EVENT_SIZE * 4];
|
||||
|
||||
while Instant::now() < deadline {
|
||||
match file.read(&mut raw) {
|
||||
Ok(0) => std::thread::sleep(Duration::from_millis(25)),
|
||||
Ok(len) => {
|
||||
let events = parse_events_for_expected(&raw[..len], expected_type)?;
|
||||
if let Some(event) = events
|
||||
.into_iter()
|
||||
.find(|event| event.event_type == expected_type)
|
||||
{
|
||||
return Ok(Some(event));
|
||||
}
|
||||
}
|
||||
Err(err)
|
||||
if matches!(
|
||||
err.kind(),
|
||||
io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted
|
||||
) =>
|
||||
{
|
||||
std::thread::sleep(Duration::from_millis(25));
|
||||
}
|
||||
Err(err) => return Err(format!("failed to read event data: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
fn parse_events_for_expected(bytes: &[u8], expected_type: u16) -> Result<Vec<InputEvent>, String> {
|
||||
if bytes.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let current =
|
||||
parse_events_with_layout(bytes, CURRENT_EVENT_SIZE, InputEvent::from_current_bytes);
|
||||
let legacy = parse_events_with_layout(bytes, LEGACY_EVENT_SIZE, InputEvent::from_legacy_bytes);
|
||||
|
||||
match (current, legacy) {
|
||||
(Ok(current_events), Ok(legacy_events)) => {
|
||||
let current_matches = current_events
|
||||
.iter()
|
||||
.any(|event| event.event_type == expected_type);
|
||||
let legacy_matches = legacy_events
|
||||
.iter()
|
||||
.any(|event| event.event_type == expected_type);
|
||||
|
||||
match (current_matches, legacy_matches) {
|
||||
(true, false) => Ok(current_events),
|
||||
(false, true) => Ok(legacy_events),
|
||||
(true, true) | (false, false) => Ok(current_events),
|
||||
}
|
||||
}
|
||||
(Ok(current_events), Err(_)) => Ok(current_events),
|
||||
(Err(_), Ok(legacy_events)) => Ok(legacy_events),
|
||||
(Err(current_err), Err(legacy_err)) => Err(format!(
|
||||
"failed to decode evdev payload as 24-byte or 16-byte events: {current_err}; {legacy_err}"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
fn parse_events_with_layout(
|
||||
bytes: &[u8],
|
||||
event_size: usize,
|
||||
decode: fn(&[u8]) -> Result<InputEvent, String>,
|
||||
) -> Result<Vec<InputEvent>, String> {
|
||||
if bytes.len() % event_size != 0 {
|
||||
return Err(format!(
|
||||
"payload length {} is not divisible by event size {event_size}",
|
||||
bytes.len()
|
||||
));
|
||||
}
|
||||
|
||||
bytes.chunks_exact(event_size).map(decode).collect()
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
fn event_index(name: &str) -> Option<u32> {
|
||||
name.strip_prefix("event")?.parse().ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn vec_args(values: &[&str]) -> Vec<String> {
|
||||
values.iter().map(|value| value.to_string()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_defaults_to_keyboard_and_mouse() {
|
||||
let config = parse_args(vec_args(&[PROGRAM])).unwrap();
|
||||
assert!(config.keyboard);
|
||||
assert!(config.mouse);
|
||||
assert_eq!(config.timeout, Duration::from_secs(DEFAULT_TIMEOUT_SECS));
|
||||
assert!(!config.json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_accepts_targeted_flags() {
|
||||
let config = parse_args(vec_args(&[
|
||||
PROGRAM,
|
||||
"--keyboard",
|
||||
"--timeout",
|
||||
"9",
|
||||
"--json",
|
||||
]))
|
||||
.unwrap();
|
||||
assert!(config.keyboard);
|
||||
assert!(!config.mouse);
|
||||
assert_eq!(config.timeout, Duration::from_secs(9));
|
||||
assert!(config.json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_rejects_invalid_timeout() {
|
||||
let err = parse_args(vec_args(&[PROGRAM, "--timeout", "abc"])).unwrap_err();
|
||||
assert!(err.contains("invalid timeout"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_rejects_timeout_over_limit() {
|
||||
let err = parse_args(vec_args(&[PROGRAM, "--timeout", "301"])).unwrap_err();
|
||||
assert!(err.contains("exceeds maximum"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_current_input_event_layout() {
|
||||
let bytes = [
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 30, 0, 1, 0, 0, 0,
|
||||
];
|
||||
let event = InputEvent::from_current_bytes(&bytes).unwrap();
|
||||
assert_eq!(
|
||||
event,
|
||||
InputEvent {
|
||||
event_type: EV_KEY,
|
||||
code: 30,
|
||||
value: 1,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_legacy_input_event_layout() {
|
||||
let bytes = [0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 5, 0, 0, 0];
|
||||
let event = InputEvent::from_legacy_bytes(&bytes).unwrap();
|
||||
assert_eq!(
|
||||
event,
|
||||
InputEvent {
|
||||
event_type: EV_REL,
|
||||
code: 0,
|
||||
value: 5,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_index_parses_numeric_suffix() {
|
||||
assert_eq!(event_index("event0"), Some(0));
|
||||
assert_eq!(event_index("event17"), Some(17));
|
||||
assert_eq!(event_index("mouse"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_event_metadata_extracts_keyboard_and_mouse_flags() {
|
||||
let metadata =
|
||||
parse_event_metadata("E=ID_INPUT=1\nE=ID_INPUT_KEYBOARD=1\nE=ID_INPUT_MOUSE=0\n");
|
||||
assert!(metadata.keyboard);
|
||||
assert!(!metadata.mouse);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_event_name_prefers_metadata_match() {
|
||||
let event_names = vec!["event0".to_string(), "event1".to_string()];
|
||||
let metadata = vec![
|
||||
(
|
||||
"event0".to_string(),
|
||||
EventMetadata {
|
||||
keyboard: true,
|
||||
mouse: false,
|
||||
},
|
||||
),
|
||||
(
|
||||
"event1".to_string(),
|
||||
EventMetadata {
|
||||
keyboard: false,
|
||||
mouse: true,
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
select_event_name(&event_names, &metadata, InputKind::Mouse, None),
|
||||
Some("event1".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_event_name_prefers_keyboard_metadata_match() {
|
||||
let event_names = vec!["event0".to_string(), "event1".to_string()];
|
||||
let metadata = vec![
|
||||
(
|
||||
"event0".to_string(),
|
||||
EventMetadata {
|
||||
keyboard: true,
|
||||
mouse: false,
|
||||
},
|
||||
),
|
||||
(
|
||||
"event1".to_string(),
|
||||
EventMetadata {
|
||||
keyboard: false,
|
||||
mouse: true,
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
select_event_name(&event_names, &metadata, InputKind::Keyboard, None),
|
||||
Some("event0".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_event_name_does_not_fallback_to_arbitrary_device() {
|
||||
let event_names = vec!["event2".to_string()];
|
||||
let metadata = vec![("event2".to_string(), EventMetadata::default())];
|
||||
|
||||
assert_eq!(
|
||||
select_event_name(&event_names, &metadata, InputKind::Mouse, None),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_events_prefers_legacy_layout_when_only_legacy_matches_expected_type() {
|
||||
let bytes = [
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 30, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
];
|
||||
|
||||
let events = parse_events_for_expected(&bytes, EV_KEY).unwrap();
|
||||
assert_eq!(events.len(), 3);
|
||||
assert_eq!(events[0].event_type, EV_KEY);
|
||||
assert_eq!(events[0].code, 30);
|
||||
assert_eq!(events[0].value, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,467 @@
|
||||
//! Phase 1 firmware-loader smoke test.
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
use std::fs;
|
||||
#[cfg(target_os = "redox")]
|
||||
use std::io::Read;
|
||||
#[cfg(target_os = "redox")]
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process;
|
||||
|
||||
const PROGRAM: &str = "redbear-phase1-firmware-check";
|
||||
const USAGE: &str = "Usage: redbear-phase1-firmware-check [--json] [--blob KEY]\n\n\
|
||||
Phase 1 firmware-loader smoke test. Validates scheme:firmware registration\n\
|
||||
and at least one readable firmware blob.";
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
const FALLBACK_BLOBS: &[&str] = &[
|
||||
"amdgpu/dce_11_0_dmcu.bin",
|
||||
"amdgpu/dcn_3_2_mall.bin",
|
||||
"i915/kbl_dmc_ver1_04.bin",
|
||||
"r8168n.bin",
|
||||
"rtl_nic/rtl8105e-1_0_0.fw",
|
||||
];
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum CheckResult {
|
||||
Pass,
|
||||
Fail,
|
||||
Skip,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
impl CheckResult {
|
||||
fn label(self) -> &'static str {
|
||||
match self {
|
||||
CheckResult::Pass => "PASS",
|
||||
CheckResult::Fail => "FAIL",
|
||||
CheckResult::Skip => "SKIP",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
struct Check {
|
||||
name: String,
|
||||
result: CheckResult,
|
||||
detail: String,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
impl Check {
|
||||
fn pass(name: &str, detail: &str) -> Self {
|
||||
Check {
|
||||
name: name.to_string(),
|
||||
result: CheckResult::Pass,
|
||||
detail: detail.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn fail(name: &str, detail: &str) -> Self {
|
||||
Check {
|
||||
name: name.to_string(),
|
||||
result: CheckResult::Fail,
|
||||
detail: detail.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn skip(name: &str, detail: &str) -> Self {
|
||||
Check {
|
||||
name: name.to_string(),
|
||||
result: CheckResult::Skip,
|
||||
detail: detail.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
struct Report {
|
||||
checks: Vec<Check>,
|
||||
json_mode: bool,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
impl Report {
|
||||
fn new(json_mode: bool) -> Self {
|
||||
Report {
|
||||
checks: Vec::new(),
|
||||
json_mode,
|
||||
}
|
||||
}
|
||||
|
||||
fn add(&mut self, check: Check) {
|
||||
self.checks.push(check);
|
||||
}
|
||||
|
||||
fn any_failed(&self) -> bool {
|
||||
self.checks.iter().any(|c| c.result == CheckResult::Fail)
|
||||
}
|
||||
|
||||
fn print(&self) {
|
||||
if self.json_mode {
|
||||
self.print_json();
|
||||
} else {
|
||||
self.print_human();
|
||||
}
|
||||
}
|
||||
|
||||
fn print_human(&self) {
|
||||
for check in &self.checks {
|
||||
let icon = match check.result {
|
||||
CheckResult::Pass => "[PASS]",
|
||||
CheckResult::Fail => "[FAIL]",
|
||||
CheckResult::Skip => "[SKIP]",
|
||||
};
|
||||
println!("{icon} {}: {}", check.name, check.detail);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_json(&self) {
|
||||
#[derive(serde::Serialize)]
|
||||
struct JsonCheck {
|
||||
name: String,
|
||||
result: String,
|
||||
detail: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct JsonReport {
|
||||
firmware_scheme: bool,
|
||||
blob_read: bool,
|
||||
blob_size: usize,
|
||||
checks: Vec<JsonCheck>,
|
||||
}
|
||||
|
||||
let firmware_scheme = self
|
||||
.checks
|
||||
.iter()
|
||||
.find(|c| c.name == "FIRMWARE_SCHEME_REGISTERED")
|
||||
.map_or(false, |c| c.result == CheckResult::Pass);
|
||||
|
||||
let blob_read = self
|
||||
.checks
|
||||
.iter()
|
||||
.find(|c| c.name == "BLOB_READ")
|
||||
.map_or(false, |c| c.result == CheckResult::Pass);
|
||||
|
||||
let blob_size = self
|
||||
.checks
|
||||
.iter()
|
||||
.find(|c| c.name == "BLOB_READ")
|
||||
.and_then(|c| {
|
||||
c.detail
|
||||
.strip_prefix("size=")
|
||||
.and_then(|s| s.split(' ').next())
|
||||
.and_then(|s| s.parse::<usize>().ok())
|
||||
})
|
||||
.unwrap_or(0);
|
||||
|
||||
let checks: Vec<JsonCheck> = self
|
||||
.checks
|
||||
.iter()
|
||||
.map(|c| JsonCheck {
|
||||
name: c.name.clone(),
|
||||
result: c.result.label().to_string(),
|
||||
detail: c.detail.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let report = JsonReport {
|
||||
firmware_scheme,
|
||||
blob_read,
|
||||
blob_size,
|
||||
checks,
|
||||
};
|
||||
|
||||
if let Err(err) = serde_json::to_writer(std::io::stdout(), &report) {
|
||||
eprintln!("{PROGRAM}: failed to serialize JSON: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn parse_args() -> Result<(bool, Option<String>), String> {
|
||||
let mut json_mode = false;
|
||||
let mut blob_key = None;
|
||||
|
||||
let mut args = std::env::args().skip(1);
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--json" => json_mode = true,
|
||||
"--blob" => {
|
||||
blob_key = Some(
|
||||
args.next()
|
||||
.ok_or_else(|| "missing value for --blob".to_string())?,
|
||||
);
|
||||
}
|
||||
"-h" | "--help" => {
|
||||
println!("{USAGE}");
|
||||
return Err(String::new());
|
||||
}
|
||||
_ => return Err(format!("unsupported argument: {arg}")),
|
||||
}
|
||||
}
|
||||
|
||||
Ok((json_mode, blob_key))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn check_scheme_registered() -> Check {
|
||||
match fs::read_dir("/scheme/") {
|
||||
Ok(entries) => {
|
||||
let names: Vec<String> = entries
|
||||
.filter_map(|e| e.ok())
|
||||
.filter_map(|e| e.file_name().into_string().ok())
|
||||
.collect();
|
||||
|
||||
if names.iter().any(|n| n == "firmware") {
|
||||
Check::pass(
|
||||
"FIRMWARE_SCHEME_REGISTERED",
|
||||
&format!("found {} scheme(s)", names.len()),
|
||||
)
|
||||
} else {
|
||||
Check::fail(
|
||||
"FIRMWARE_SCHEME_REGISTERED",
|
||||
"firmware not found in /scheme/",
|
||||
)
|
||||
}
|
||||
}
|
||||
Err(err) => Check::fail(
|
||||
"FIRMWARE_SCHEME_REGISTERED",
|
||||
&format!("cannot read /scheme/: {err}"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn list_firmware_keys() -> Check {
|
||||
match fs::read_dir("/scheme/firmware/") {
|
||||
Ok(entries) => {
|
||||
let keys: Vec<String> = entries
|
||||
.filter_map(|e| e.ok())
|
||||
.filter_map(|e| e.file_name().into_string().ok())
|
||||
.collect();
|
||||
|
||||
if keys.is_empty() {
|
||||
Check::fail("FIRMWARE_KEY_LIST", "no keys found in /scheme/firmware/")
|
||||
} else {
|
||||
let preview = keys.iter().take(4).cloned().collect::<Vec<_>>().join(", ");
|
||||
Check::pass(
|
||||
"FIRMWARE_KEY_LIST",
|
||||
&format!("{} key(s): {}", keys.len(), preview),
|
||||
)
|
||||
}
|
||||
}
|
||||
Err(err) => Check::fail(
|
||||
"FIRMWARE_KEY_LIST",
|
||||
&format!("cannot list /scheme/firmware/: {err}"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn read_firmware_blob(key: &str) -> Result<(usize, Vec<u8>), String> {
|
||||
let path = format!("/scheme/firmware/{key}");
|
||||
let mut file =
|
||||
std::fs::File::open(&path).map_err(|err| format!("failed to open {path}: {err}"))?;
|
||||
let mut buf = Vec::new();
|
||||
let size = file
|
||||
.read_to_end(&mut buf)
|
||||
.map_err(|err| format!("failed to read {path}: {err}"))?;
|
||||
Ok((size, buf))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn check_blob_fstat(key: &str) -> Check {
|
||||
let path = format!("/scheme/firmware/{key}");
|
||||
match std::fs::File::open(&path) {
|
||||
Ok(file) => match file.metadata() {
|
||||
Ok(meta) => {
|
||||
let size = meta.len();
|
||||
if size > 0 {
|
||||
Check::pass(
|
||||
"BLOB_MMAP_PATH",
|
||||
&format!("size={} via fstat on {}", size, key),
|
||||
)
|
||||
} else {
|
||||
Check::fail("BLOB_MMAP_PATH", &format!("blob {key} has zero size"))
|
||||
}
|
||||
}
|
||||
Err(err) => Check::fail("BLOB_MMAP_PATH", &format!("fstat failed for {path}: {err}")),
|
||||
},
|
||||
Err(err) => Check::fail("BLOB_MMAP_PATH", &format!("cannot open {path}: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn check_lib_firmware_dir() -> Check {
|
||||
let dir = Path::new("/lib/firmware/");
|
||||
match fs::read_dir(dir) {
|
||||
Ok(entries) => {
|
||||
let blobs: Vec<PathBuf> = entries
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| e.file_type().map_or(false, |ft| ft.is_file()))
|
||||
.map(|e| e.path())
|
||||
.collect();
|
||||
|
||||
if blobs.is_empty() {
|
||||
Check::skip("LIB_FIRMWARE_DIR", "/lib/firmware/ is empty")
|
||||
} else {
|
||||
let preview = blobs
|
||||
.iter()
|
||||
.take(3)
|
||||
.filter_map(|p| p.file_name().and_then(|n| n.to_str()))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
Check::pass(
|
||||
"LIB_FIRMWARE_DIR",
|
||||
&format!("{} blob(s) in /lib/firmware/: {}", blobs.len(), preview),
|
||||
)
|
||||
}
|
||||
}
|
||||
Err(err) => Check::skip(
|
||||
"LIB_FIRMWARE_DIR",
|
||||
&format!("/lib/firmware/ not accessible: {err}"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
{
|
||||
if std::env::args().any(|a| a == "-h" || a == "--help") {
|
||||
println!("{USAGE}");
|
||||
return Err(String::new());
|
||||
}
|
||||
println!("{PROGRAM}: firmware-loader check requires Redox runtime");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
{
|
||||
let (json_mode, blob_key) = parse_args()?;
|
||||
let mut report = Report::new(json_mode);
|
||||
|
||||
report.add(check_scheme_registered());
|
||||
report.add(list_firmware_keys());
|
||||
report.add(check_lib_firmware_dir());
|
||||
|
||||
let blob_to_try = blob_key.or_else(|| {
|
||||
FALLBACK_BLOBS
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|&k| Path::new(&format!("/scheme/firmware/{k}")).exists())
|
||||
.map(String::from)
|
||||
});
|
||||
|
||||
match blob_to_try {
|
||||
Some(key) => {
|
||||
match read_firmware_blob(&key) {
|
||||
Ok((size, _content)) => {
|
||||
if size > 0 {
|
||||
report.add(Check::pass("BLOB_READ", &format!("size={} key={}", size, key)));
|
||||
} else {
|
||||
report.add(Check::fail("BLOB_READ", &format!("blob {key} has zero size")));
|
||||
}
|
||||
}
|
||||
Err(msg) => {
|
||||
report.add(Check::fail("BLOB_READ", &msg));
|
||||
}
|
||||
}
|
||||
|
||||
report.add(check_blob_fstat(&key));
|
||||
}
|
||||
None => {
|
||||
report.add(Check::skip("BLOB_READ", "no known blob key found in /scheme/firmware/"));
|
||||
report.add(Check::skip("BLOB_MMAP_PATH", "no blob to check"));
|
||||
}
|
||||
}
|
||||
|
||||
report.print();
|
||||
|
||||
if report.any_failed() {
|
||||
return Err("one or more firmware checks failed".to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = run() {
|
||||
if err.is_empty() {
|
||||
process::exit(0);
|
||||
}
|
||||
eprintln!("{PROGRAM}: {err}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse_args_with<'a>(args: &[&'a str]) -> Result<(bool, Option<String>), String> {
|
||||
let mut json_mode = false;
|
||||
let mut blob_key = None;
|
||||
|
||||
let mut args_iter = args.iter();
|
||||
while let Some(arg) = args_iter.next() {
|
||||
match *arg {
|
||||
"--json" => json_mode = true,
|
||||
"--blob" => {
|
||||
blob_key = Some(
|
||||
args_iter
|
||||
.next()
|
||||
.ok_or_else(|| "missing value for --blob".to_string())?
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
_ => return Err(format!("unsupported argument: {arg}")),
|
||||
}
|
||||
}
|
||||
|
||||
Ok((json_mode, blob_key))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_accepts_json_flag() {
|
||||
let result = parse_args_with(&["--json"]);
|
||||
let (json_mode, _blob_key) = result.expect("parse_args should succeed");
|
||||
assert!(json_mode, "json_mode should be true with --json flag");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_accepts_blob_flag() {
|
||||
let result = parse_args_with(&["--blob", "somename"]);
|
||||
let (_json_mode, blob_key) = result.expect("parse_args should succeed");
|
||||
assert_eq!(blob_key, Some("somename".to_string()), "blob_key should be Some(\"somename\")");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_rejects_unknown() {
|
||||
let result = parse_args_with(&["--unknown-flag"]);
|
||||
assert!(result.is_err(), "parse_args should reject unknown argument");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_default_no_json() {
|
||||
let result = parse_args_with(&[]);
|
||||
let (json_mode, _blob_key) = result.expect("parse_args should succeed");
|
||||
assert!(!json_mode, "json_mode should be false by default");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_status_render_pass() {
|
||||
let label = CheckResult::Pass.label();
|
||||
assert_eq!(label, "PASS", "CheckResult::Pass should render as PASS");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_status_render_fail() {
|
||||
let label = CheckResult::Fail.label();
|
||||
assert_eq!(label, "FAIL", "CheckResult::Fail should render as FAIL");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
use std::process;
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
use std::{fs, io::Read};
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
const PROGRAM: &str = "redbear-phase1-udev-check";
|
||||
const USAGE: &str = "Usage: redbear-phase1-udev-check [--keyboard] [--pointer] [--drm] [--json]\n\nValidate bounded udev-shim device enumeration inside the Red Bear guest.";
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
const MAX_DEVICE_INFO_BYTES: usize = 64 * 1024;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct Config {
|
||||
keyboard: bool,
|
||||
pointer: bool,
|
||||
drm: bool,
|
||||
json: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
struct Report {
|
||||
udev_scheme: bool,
|
||||
keyboard_count: usize,
|
||||
pointer_count: usize,
|
||||
drm_count: usize,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
enum CheckStatus {
|
||||
Pass(String),
|
||||
Fail(String),
|
||||
Skip,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
impl CheckStatus {
|
||||
fn render(&self, label: &str) {
|
||||
match self {
|
||||
Self::Pass(detail) => println!("PASS {label}: {detail}"),
|
||||
Self::Fail(detail) => println!("FAIL {label}: {detail}"),
|
||||
Self::Skip => println!("SKIP {label}: not requested"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
match parse_args(std::env::args()) {
|
||||
Ok(config) => match run(&config) {
|
||||
Ok(success) => process::exit(if success { 0 } else { 1 }),
|
||||
Err(err) => {
|
||||
eprintln!("{PROGRAM}: {err}");
|
||||
process::exit(1);
|
||||
}
|
||||
},
|
||||
Err(err) if err.is_empty() => process::exit(0),
|
||||
Err(err) => {
|
||||
eprintln!("{PROGRAM}: {err}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_args(args: impl IntoIterator<Item = String>) -> Result<Config, String> {
|
||||
let mut keyboard = false;
|
||||
let mut pointer = false;
|
||||
let mut drm = false;
|
||||
let mut json = false;
|
||||
|
||||
let mut args = args.into_iter();
|
||||
let _program = args.next();
|
||||
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--keyboard" => keyboard = true,
|
||||
"--pointer" => pointer = true,
|
||||
"--drm" => drm = true,
|
||||
"--json" => json = true,
|
||||
"-h" | "--help" => {
|
||||
println!("{USAGE}");
|
||||
return Err(String::new());
|
||||
}
|
||||
_ => return Err(format!("unsupported argument: {arg}")),
|
||||
}
|
||||
}
|
||||
|
||||
if !keyboard && !pointer && !drm {
|
||||
keyboard = true;
|
||||
pointer = true;
|
||||
drm = true;
|
||||
}
|
||||
|
||||
Ok(Config {
|
||||
keyboard,
|
||||
pointer,
|
||||
drm,
|
||||
json,
|
||||
})
|
||||
}
|
||||
|
||||
fn run(config: &Config) -> Result<bool, String> {
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
{
|
||||
let report = Report::default();
|
||||
if config.json {
|
||||
let payload = serde_json::to_string(&json!({
|
||||
"udev_scheme": report.udev_scheme,
|
||||
"keyboard_count": report.keyboard_count,
|
||||
"pointer_count": report.pointer_count,
|
||||
"drm_count": report.drm_count,
|
||||
}))
|
||||
.map_err(|err| format!("failed to serialize JSON output: {err}"))?;
|
||||
eprintln!("udev-shim check requires Redox runtime");
|
||||
println!("{payload}");
|
||||
} else {
|
||||
println!("udev-shim check requires Redox runtime");
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
{
|
||||
run_redox(config)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn run_redox(config: &Config) -> Result<bool, String> {
|
||||
let udev_scheme_present = fs::metadata("/scheme/udev").is_ok();
|
||||
let device_entries = match list_dir_names("/scheme/udev/devices") {
|
||||
Ok(entries) => entries,
|
||||
Err(_) => Vec::new(),
|
||||
};
|
||||
|
||||
let report = Report {
|
||||
udev_scheme: udev_scheme_present,
|
||||
keyboard_count: count_devices_with_property(&device_entries, "ID_INPUT_KEYBOARD", "1"),
|
||||
pointer_count: count_devices_with_property(&device_entries, "ID_INPUT_MOUSE", "1"),
|
||||
drm_count: count_drm_devices(),
|
||||
};
|
||||
|
||||
let scheme_status = if report.udev_scheme {
|
||||
CheckStatus::Pass(format!(
|
||||
"enumerated {} /scheme/udev/devices entries",
|
||||
device_entries.len()
|
||||
))
|
||||
} else {
|
||||
CheckStatus::Fail("could not enumerate any /scheme/udev/devices entries".to_string())
|
||||
};
|
||||
let keyboard_status = if config.keyboard {
|
||||
count_status(report.keyboard_count, "keyboard")
|
||||
} else {
|
||||
CheckStatus::Skip
|
||||
};
|
||||
let pointer_status = if config.pointer {
|
||||
count_status(report.pointer_count, "pointer")
|
||||
} else {
|
||||
CheckStatus::Skip
|
||||
};
|
||||
let drm_status = if config.drm {
|
||||
count_status(report.drm_count, "DRM")
|
||||
} else {
|
||||
CheckStatus::Skip
|
||||
};
|
||||
|
||||
if config.json {
|
||||
let payload = serde_json::to_string(&json!({
|
||||
"udev_scheme": report.udev_scheme,
|
||||
"keyboard_count": report.keyboard_count,
|
||||
"pointer_count": report.pointer_count,
|
||||
"drm_count": report.drm_count,
|
||||
}))
|
||||
.map_err(|err| format!("failed to serialize JSON output: {err}"))?;
|
||||
println!("{payload}");
|
||||
} else {
|
||||
scheme_status.render("udev scheme");
|
||||
keyboard_status.render("keyboard devices");
|
||||
pointer_status.render("pointer devices");
|
||||
drm_status.render("DRM devices");
|
||||
}
|
||||
|
||||
Ok(overall_success(&report, &config))
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
fn overall_success(report: &Report, config: &Config) -> bool {
|
||||
report.udev_scheme
|
||||
&& (!config.keyboard || report.keyboard_count > 0)
|
||||
&& (!config.pointer || report.pointer_count > 0)
|
||||
&& (!config.drm || report.drm_count > 0)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn list_dir_names(path: &str) -> Result<Vec<String>, String> {
|
||||
let entries = fs::read_dir(path).map_err(|err| format!("failed to read {path}: {err}"))?;
|
||||
let mut names = entries
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter_map(|entry| entry.file_name().into_string().ok())
|
||||
.collect::<Vec<_>>();
|
||||
names.sort();
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn count_devices_with_property(device_entries: &[String], key: &str, value: &str) -> usize {
|
||||
device_entries
|
||||
.iter()
|
||||
.filter(|entry| {
|
||||
let path = format!("/scheme/udev/devices/{entry}");
|
||||
let Ok(info) = read_text_with_limit(&path, MAX_DEVICE_INFO_BYTES) else {
|
||||
return false;
|
||||
};
|
||||
has_property(&info, key, value)
|
||||
})
|
||||
.count()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn count_drm_devices() -> usize {
|
||||
list_dir_names("/dev/dri")
|
||||
.map(|entries| {
|
||||
entries
|
||||
.into_iter()
|
||||
.filter(|name| name.starts_with("card"))
|
||||
.count()
|
||||
})
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn read_text_with_limit(path: &str, max_bytes: usize) -> Result<String, String> {
|
||||
let mut file = fs::File::open(path).map_err(|err| format!("failed to open {path}: {err}"))?;
|
||||
let mut bytes = Vec::new();
|
||||
file.by_ref()
|
||||
.take((max_bytes + 1) as u64)
|
||||
.read_to_end(&mut bytes)
|
||||
.map_err(|err| format!("failed to read {path}: {err}"))?;
|
||||
|
||||
if bytes.len() > max_bytes {
|
||||
return Err(format!("{path} exceeds maximum size of {max_bytes} bytes"));
|
||||
}
|
||||
|
||||
String::from_utf8(bytes).map_err(|err| format!("{path} is not valid UTF-8: {err}"))
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
fn has_property(info: &str, key: &str, expected: &str) -> bool {
|
||||
let prefix = format!("E={key}=");
|
||||
info.lines()
|
||||
.find_map(|line| line.strip_prefix(&prefix))
|
||||
.map(|value| value.trim() == expected)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn vec_args(values: &[&str]) -> Vec<String> {
|
||||
values.iter().map(|value| value.to_string()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_defaults_to_all_checks() {
|
||||
let config = parse_args(vec_args(&[PROGRAM])).unwrap();
|
||||
assert!(config.keyboard);
|
||||
assert!(config.pointer);
|
||||
assert!(config.drm);
|
||||
assert!(!config.json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_accepts_targeted_flags() {
|
||||
let config = parse_args(vec_args(&[PROGRAM, "--keyboard", "--json"])).unwrap();
|
||||
assert!(config.keyboard);
|
||||
assert!(!config.pointer);
|
||||
assert!(!config.drm);
|
||||
assert!(config.json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_rejects_unknown_flag() {
|
||||
let err = parse_args(vec_args(&[PROGRAM, "--bogus"])).unwrap_err();
|
||||
assert!(err.contains("unsupported argument"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn has_property_matches_expected_key_and_value() {
|
||||
let info = "P=/devices/platform/evdev-keyboard0\nE=ID_INPUT=1\nE=ID_INPUT_KEYBOARD=1\n";
|
||||
assert!(has_property(info, "ID_INPUT_KEYBOARD", "1"));
|
||||
assert!(!has_property(info, "ID_INPUT_MOUSE", "1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overall_success_requires_all_requested_runtime_surfaces() {
|
||||
let all_flags = Config {
|
||||
keyboard: true,
|
||||
pointer: true,
|
||||
drm: true,
|
||||
json: false,
|
||||
};
|
||||
let passing = Report {
|
||||
udev_scheme: true,
|
||||
keyboard_count: 1,
|
||||
pointer_count: 1,
|
||||
drm_count: 1,
|
||||
};
|
||||
let missing_drm = Report {
|
||||
drm_count: 0,
|
||||
..passing.clone()
|
||||
};
|
||||
|
||||
assert!(overall_success(&passing, &all_flags));
|
||||
assert!(!overall_success(&missing_drm, &all_flags));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overall_success_respects_targeted_flags() {
|
||||
let passing = Report {
|
||||
udev_scheme: true,
|
||||
keyboard_count: 1,
|
||||
pointer_count: 0,
|
||||
drm_count: 0,
|
||||
};
|
||||
let keyboard_only = Config {
|
||||
keyboard: true,
|
||||
pointer: false,
|
||||
drm: false,
|
||||
json: false,
|
||||
};
|
||||
|
||||
assert!(overall_success(&passing, &keyboard_only));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
//! Phase 2 Wayland compositor proof checker.
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
use std::{
|
||||
env, fs,
|
||||
io::{Read, Write},
|
||||
os::unix::net::UnixStream,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
time::Duration,
|
||||
};
|
||||
use std::process;
|
||||
|
||||
const PROGRAM: &str = "redbear-phase2-wayland-check";
|
||||
const USAGE: &str = "Usage: redbear-phase2-wayland-check [--json]\n\n\
|
||||
Phase 2 Wayland compositor proof checker. Validates the compositor socket,\n\
|
||||
compositor process, Wayland protocol connectivity, EGL/GBM presence,\n\
|
||||
software renderer evidence, and the optional qt6-wayland-smoke client.";
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
const DEFAULT_RUNTIME_DIR: &str = "/run/user/1000";
|
||||
#[cfg(target_os = "redox")]
|
||||
const DEFAULT_WAYLAND_DISPLAY: &str = "wayland-0";
|
||||
#[cfg(target_os = "redox")]
|
||||
const QT6_WAYLAND_SMOKE: &str = "/usr/bin/qt6-wayland-smoke";
|
||||
|
||||
fn parse_args_from<I>(args: I) -> Result<bool, String>
|
||||
where
|
||||
I: IntoIterator<Item = String>,
|
||||
{
|
||||
let mut json_mode = false;
|
||||
|
||||
let mut args = args.into_iter();
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--json" => json_mode = true,
|
||||
"-h" | "--help" => {
|
||||
println!("{USAGE}");
|
||||
return Err(String::new());
|
||||
}
|
||||
_ => return Err(format!("unsupported argument: {arg}")),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(json_mode)
|
||||
}
|
||||
|
||||
fn parse_args() -> Result<bool, String> {
|
||||
parse_args_from(std::env::args().skip(1))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum CheckResult {
|
||||
Pass,
|
||||
Fail,
|
||||
Skip,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
impl CheckResult {
|
||||
fn label(self) -> &'static str {
|
||||
match self {
|
||||
CheckResult::Pass => "PASS",
|
||||
CheckResult::Fail => "FAIL",
|
||||
CheckResult::Skip => "SKIP",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
struct Check {
|
||||
name: String,
|
||||
result: CheckResult,
|
||||
detail: String,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
impl Check {
|
||||
fn pass(name: &str, detail: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
result: CheckResult::Pass,
|
||||
detail: detail.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn fail(name: &str, detail: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
result: CheckResult::Fail,
|
||||
detail: detail.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn skip(name: &str, detail: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
result: CheckResult::Skip,
|
||||
detail: detail.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
struct Report {
|
||||
checks: Vec<Check>,
|
||||
json_mode: bool,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
impl Report {
|
||||
fn new(json_mode: bool) -> Self {
|
||||
Self {
|
||||
checks: Vec::new(),
|
||||
json_mode,
|
||||
}
|
||||
}
|
||||
|
||||
fn add(&mut self, check: Check) {
|
||||
self.checks.push(check);
|
||||
}
|
||||
|
||||
fn any_failed(&self) -> bool {
|
||||
self.checks.iter().any(|check| check.result == CheckResult::Fail)
|
||||
}
|
||||
|
||||
fn check_passed(&self, name: &str) -> bool {
|
||||
self.checks
|
||||
.iter()
|
||||
.find(|check| check.name == name)
|
||||
.is_some_and(|check| check.result == CheckResult::Pass)
|
||||
}
|
||||
|
||||
fn optional_check_passed(&self, name: &str) -> Option<bool> {
|
||||
self.checks
|
||||
.iter()
|
||||
.find(|check| check.name == name)
|
||||
.and_then(|check| match check.result {
|
||||
CheckResult::Pass => Some(true),
|
||||
CheckResult::Fail => Some(false),
|
||||
CheckResult::Skip => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn print(&self) {
|
||||
if self.json_mode {
|
||||
self.print_json();
|
||||
} else {
|
||||
self.print_human();
|
||||
}
|
||||
}
|
||||
|
||||
fn print_human(&self) {
|
||||
println!("=== Red Bear OS Phase 2 Wayland Compositor Check ===");
|
||||
for check in &self.checks {
|
||||
let icon = match check.result {
|
||||
CheckResult::Pass => "[PASS]",
|
||||
CheckResult::Fail => "[FAIL]",
|
||||
CheckResult::Skip => "[SKIP]",
|
||||
};
|
||||
println!("{icon} {}: {}", check.name, check.detail);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_json(&self) {
|
||||
#[derive(serde::Serialize)]
|
||||
struct JsonCheck {
|
||||
name: String,
|
||||
result: String,
|
||||
detail: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct JsonReport {
|
||||
overall_success: bool,
|
||||
compositor_socket: bool,
|
||||
compositor_process: bool,
|
||||
wayland_registry: bool,
|
||||
egl_present: bool,
|
||||
gbm_present: bool,
|
||||
software_renderer: bool,
|
||||
qt6_wayland_smoke_present: Option<bool>,
|
||||
checks: Vec<JsonCheck>,
|
||||
}
|
||||
|
||||
let report = JsonReport {
|
||||
overall_success: !self.any_failed(),
|
||||
compositor_socket: self.check_passed("WAYLAND_SOCKET"),
|
||||
compositor_process: self.check_passed("COMPOSITOR_PROCESS"),
|
||||
wayland_registry: self.check_passed("WAYLAND_PROTOCOL_REGISTRY"),
|
||||
egl_present: self.check_passed("LIBEGL_PRESENT"),
|
||||
gbm_present: self.check_passed("LIBGBM_PRESENT"),
|
||||
software_renderer: self.check_passed("SOFTWARE_RENDERER"),
|
||||
qt6_wayland_smoke_present: self.optional_check_passed("QT6_WAYLAND_SMOKE"),
|
||||
checks: self
|
||||
.checks
|
||||
.iter()
|
||||
.map(|check| JsonCheck {
|
||||
name: check.name.clone(),
|
||||
result: check.result.label().to_string(),
|
||||
detail: check.detail.clone(),
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
if let Err(err) = serde_json::to_writer(std::io::stdout(), &report) {
|
||||
eprintln!("{PROGRAM}: failed to serialize JSON: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
#[derive(Debug, Clone)]
|
||||
struct WaylandEndpoint {
|
||||
path: PathBuf,
|
||||
display: String,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
struct WaylandClient {
|
||||
stream: UnixStream,
|
||||
next_id: u32,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
impl WaylandClient {
|
||||
fn connect(path: &Path) -> Result<Self, String> {
|
||||
let stream = UnixStream::connect(path)
|
||||
.map_err(|err| format!("failed to connect to {}: {err}", path.display()))?;
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_secs(2)))
|
||||
.map_err(|err| format!("failed to set read timeout on {}: {err}", path.display()))?;
|
||||
stream
|
||||
.set_write_timeout(Some(Duration::from_secs(2)))
|
||||
.map_err(|err| format!("failed to set write timeout on {}: {err}", path.display()))?;
|
||||
Ok(Self { stream, next_id: 2 })
|
||||
}
|
||||
|
||||
fn alloc_id(&mut self) -> u32 {
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
id
|
||||
}
|
||||
|
||||
fn send_message(&mut self, object_id: u32, opcode: u16, payload: &[u8]) -> Result<(), String> {
|
||||
let size = 8 + payload.len();
|
||||
let mut message = Vec::with_capacity(size);
|
||||
message.extend_from_slice(&object_id.to_le_bytes());
|
||||
let header = ((size as u32) << 16) | u32::from(opcode);
|
||||
message.extend_from_slice(&header.to_le_bytes());
|
||||
message.extend_from_slice(payload);
|
||||
self.stream
|
||||
.write_all(&message)
|
||||
.map_err(|err| format!("failed to write Wayland message: {err}"))
|
||||
}
|
||||
|
||||
fn read_message(&mut self) -> Result<(u32, u16, Vec<u8>), String> {
|
||||
let mut header = [0u8; 8];
|
||||
self.stream
|
||||
.read_exact(&mut header)
|
||||
.map_err(|err| format!("failed to read Wayland header: {err}"))?;
|
||||
|
||||
let object_id = u32::from_le_bytes([header[0], header[1], header[2], header[3]]);
|
||||
let size_opcode = u32::from_le_bytes([header[4], header[5], header[6], header[7]]);
|
||||
let size = ((size_opcode >> 16) & 0xFFFF) as usize;
|
||||
let opcode = (size_opcode & 0xFFFF) as u16;
|
||||
if size < 8 {
|
||||
return Err(format!("invalid Wayland message size {size}"));
|
||||
}
|
||||
|
||||
let payload_len = size - 8;
|
||||
let mut payload = vec![0u8; payload_len];
|
||||
if payload_len > 0 {
|
||||
self.stream
|
||||
.read_exact(&mut payload)
|
||||
.map_err(|err| format!("failed to read Wayland payload: {err}"))?;
|
||||
}
|
||||
|
||||
Ok((object_id, opcode, payload))
|
||||
}
|
||||
|
||||
fn get_registry(&mut self) -> Result<u32, String> {
|
||||
let registry_id = self.alloc_id();
|
||||
self.send_message(1, 1, ®istry_id.to_le_bytes())?;
|
||||
Ok(registry_id)
|
||||
}
|
||||
|
||||
fn sync(&mut self) -> Result<u32, String> {
|
||||
let callback_id = self.alloc_id();
|
||||
self.send_message(1, 0, &callback_id.to_le_bytes())?;
|
||||
Ok(callback_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn env_value(name: &str) -> Option<String> {
|
||||
env::var(name).ok().filter(|value| !value.trim().is_empty())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn wayland_socket_candidates(runtime_dir: Option<&str>, display: Option<&str>) -> Vec<PathBuf> {
|
||||
let display = display.unwrap_or(DEFAULT_WAYLAND_DISPLAY);
|
||||
let mut candidates = Vec::new();
|
||||
|
||||
if let Some(runtime_dir) = runtime_dir {
|
||||
candidates.push(PathBuf::from(runtime_dir).join(display));
|
||||
}
|
||||
|
||||
let fallback = PathBuf::from(DEFAULT_RUNTIME_DIR).join(DEFAULT_WAYLAND_DISPLAY);
|
||||
if !candidates.iter().any(|candidate| candidate == &fallback) {
|
||||
candidates.push(fallback);
|
||||
}
|
||||
|
||||
candidates
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn resolve_wayland_endpoint() -> Result<WaylandEndpoint, String> {
|
||||
let runtime_dir = env_value("XDG_RUNTIME_DIR");
|
||||
let display = env_value("WAYLAND_DISPLAY").unwrap_or_else(|| DEFAULT_WAYLAND_DISPLAY.to_string());
|
||||
let candidates = wayland_socket_candidates(runtime_dir.as_deref(), Some(&display));
|
||||
|
||||
for candidate in candidates {
|
||||
if candidate.exists() {
|
||||
return Ok(WaylandEndpoint {
|
||||
path: candidate,
|
||||
display: display.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let paths = wayland_socket_candidates(runtime_dir.as_deref(), Some(&display))
|
||||
.iter()
|
||||
.map(|path| path.display().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
Err(format!("missing Wayland socket at any of: {paths}"))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn run_command(program: &str, args: &[&str], label: &str) -> Result<String, String> {
|
||||
let output = Command::new(program)
|
||||
.args(args)
|
||||
.output()
|
||||
.map_err(|err| format!("failed to run {label}: {err}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let detail = if !stderr.trim().is_empty() {
|
||||
stderr.trim().to_string()
|
||||
} else if !stdout.trim().is_empty() {
|
||||
stdout.trim().to_string()
|
||||
} else {
|
||||
String::from("no output")
|
||||
};
|
||||
return Err(format!("{label} exited with status {}: {detail}", output.status));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn detect_compositor_process(output: &str) -> Option<&'static str> {
|
||||
if output.contains("redbear-compositor") {
|
||||
Some("redbear-compositor")
|
||||
} else if output.contains("kwin_wayland") {
|
||||
Some("kwin_wayland")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn check_compositor_process() -> Check {
|
||||
match run_command("ps", &[], "ps") {
|
||||
Ok(output) => match detect_compositor_process(&output) {
|
||||
Some(process_name) => Check::pass(
|
||||
"COMPOSITOR_PROCESS",
|
||||
format!("{process_name} appears in process list"),
|
||||
),
|
||||
None => Check::fail(
|
||||
"COMPOSITOR_PROCESS",
|
||||
"neither redbear-compositor nor kwin_wayland appears in ps output",
|
||||
),
|
||||
},
|
||||
Err(err) => Check::fail("COMPOSITOR_PROCESS", err),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn verify_registry_roundtrip(endpoint: &WaylandEndpoint) -> Result<String, String> {
|
||||
let mut client = WaylandClient::connect(&endpoint.path)?;
|
||||
let registry_id = client.get_registry()?;
|
||||
let callback_id = client.sync()?;
|
||||
|
||||
for _ in 0..8 {
|
||||
let (object_id, opcode, _) = client.read_message()?;
|
||||
if object_id == registry_id {
|
||||
return Ok(format!(
|
||||
"{} responded to wl_display.get_registry with opcode {} on {}",
|
||||
endpoint.display,
|
||||
opcode,
|
||||
endpoint.path.display()
|
||||
));
|
||||
}
|
||||
if object_id == callback_id {
|
||||
return Ok(format!(
|
||||
"{} completed bounded roundtrip after wl_display.get_registry on {}",
|
||||
endpoint.display,
|
||||
endpoint.path.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"{} did not answer wl_display.get_registry within bounded read window",
|
||||
endpoint.path.display()
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn contains_software_renderer_text(output: &str) -> bool {
|
||||
let lower = output.to_ascii_lowercase();
|
||||
lower.contains("llvmpipe")
|
||||
|| lower.contains("software rasterizer")
|
||||
|| lower.contains("kms_swrast")
|
||||
|| lower.contains("swrast")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn is_software_driver_name(name: &str) -> bool {
|
||||
let lower = name.to_ascii_lowercase();
|
||||
lower.contains("llvmpipe") || lower.contains("kms_swrast") || lower.contains("swrast")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn software_driver_names_in_dir(dir: &Path) -> Result<Vec<String>, String> {
|
||||
let entries = fs::read_dir(dir)
|
||||
.map_err(|err| format!("cannot list {}: {err}", dir.display()))?
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter_map(|entry| entry.file_name().into_string().ok())
|
||||
.filter(|name| is_software_driver_name(name))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn check_software_renderer() -> Check {
|
||||
let mut details = Vec::new();
|
||||
|
||||
if Path::new("/usr/bin/glxinfo").exists() {
|
||||
match run_command("glxinfo", &[], "glxinfo") {
|
||||
Ok(output) if contains_software_renderer_text(&output) => {
|
||||
return Check::pass("SOFTWARE_RENDERER", "glxinfo reports llvmpipe/software renderer");
|
||||
}
|
||||
Ok(_) => details.push(String::from("glxinfo ran but did not report llvmpipe")),
|
||||
Err(err) => details.push(err),
|
||||
}
|
||||
} else {
|
||||
details.push(String::from("/usr/bin/glxinfo not installed"));
|
||||
}
|
||||
|
||||
let dri_dir = Path::new("/usr/lib/dri");
|
||||
match software_driver_names_in_dir(dri_dir) {
|
||||
Ok(driver_names) if !driver_names.is_empty() => Check::pass(
|
||||
"SOFTWARE_RENDERER",
|
||||
format!(
|
||||
"software DRI driver(s) present in {}: {}",
|
||||
dri_dir.display(),
|
||||
driver_names.join(", ")
|
||||
),
|
||||
),
|
||||
Ok(_) => {
|
||||
details.push(format!("{} has no llvmpipe/swrast-style drivers", dri_dir.display()));
|
||||
Check::fail("SOFTWARE_RENDERER", details.join("; "))
|
||||
}
|
||||
Err(err) => {
|
||||
details.push(err);
|
||||
Check::fail("SOFTWARE_RENDERER", details.join("; "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn check_optional_qt_smoke() -> Check {
|
||||
if Path::new(QT6_WAYLAND_SMOKE).exists() {
|
||||
Check::pass("QT6_WAYLAND_SMOKE", QT6_WAYLAND_SMOKE)
|
||||
} else {
|
||||
Check::skip(
|
||||
"QT6_WAYLAND_SMOKE",
|
||||
format!("optional binary not installed at {QT6_WAYLAND_SMOKE}"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
let json_mode = parse_args()?;
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
{
|
||||
let _ = json_mode;
|
||||
println!("{PROGRAM}: Wayland compositor check requires Redox runtime");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
{
|
||||
let mut report = Report::new(json_mode);
|
||||
|
||||
match resolve_wayland_endpoint() {
|
||||
Ok(endpoint) => {
|
||||
report.add(Check::pass(
|
||||
"WAYLAND_SOCKET",
|
||||
format!("{} ({})", endpoint.path.display(), endpoint.display),
|
||||
));
|
||||
report.add(check_compositor_process());
|
||||
report.add(match verify_registry_roundtrip(&endpoint) {
|
||||
Ok(detail) => Check::pass("WAYLAND_PROTOCOL_REGISTRY", detail),
|
||||
Err(err) => Check::fail("WAYLAND_PROTOCOL_REGISTRY", err),
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
report.add(Check::fail("WAYLAND_SOCKET", err));
|
||||
report.add(check_compositor_process());
|
||||
report.add(Check::fail(
|
||||
"WAYLAND_PROTOCOL_REGISTRY",
|
||||
"cannot attempt wl_display.get_registry without a Wayland socket",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
report.add(if Path::new("/usr/lib/libEGL.so").exists() {
|
||||
Check::pass("LIBEGL_PRESENT", "/usr/lib/libEGL.so")
|
||||
} else {
|
||||
Check::fail("LIBEGL_PRESENT", "missing /usr/lib/libEGL.so")
|
||||
});
|
||||
|
||||
report.add(if Path::new("/usr/lib/libGBM.so").exists() {
|
||||
Check::pass("LIBGBM_PRESENT", "/usr/lib/libGBM.so")
|
||||
} else {
|
||||
Check::fail("LIBGBM_PRESENT", "missing /usr/lib/libGBM.so")
|
||||
});
|
||||
|
||||
report.add(check_software_renderer());
|
||||
report.add(check_optional_qt_smoke());
|
||||
report.print();
|
||||
|
||||
if report.any_failed() {
|
||||
return Err(String::from("one or more Phase 2 Wayland checks failed"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = run() {
|
||||
if err.is_empty() {
|
||||
process::exit(0);
|
||||
}
|
||||
eprintln!("{PROGRAM}: {err}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
#[test]
|
||||
fn wayland_socket_candidates_include_runtime_then_default() {
|
||||
let candidates = wayland_socket_candidates(Some("/tmp/runtime"), Some("wayland-9"));
|
||||
assert_eq!(candidates[0], PathBuf::from("/tmp/runtime/wayland-9"));
|
||||
assert!(candidates.contains(&PathBuf::from("/run/user/1000/wayland-0")));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
#[test]
|
||||
fn detect_compositor_process_matches_kwin_wrapper_line() {
|
||||
let output = "123 kwin_wayland_wrapper --virtual\n";
|
||||
assert_eq!(detect_compositor_process(output), Some("kwin_wayland"));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
#[test]
|
||||
fn contains_software_renderer_text_detects_llvmpipe() {
|
||||
assert!(contains_software_renderer_text(
|
||||
"OpenGL renderer string: llvmpipe (LLVM 18.1, 256 bits)"
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
#[test]
|
||||
fn is_software_driver_name_detects_swrast_variants() {
|
||||
assert!(is_software_driver_name("kms_swrast_dri.so"));
|
||||
assert!(is_software_driver_name("swrast_dri.so"));
|
||||
assert!(!is_software_driver_name("iris_dri.so"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_accepts_json_flag() {
|
||||
let parsed = parse_args_from([String::from("--json")]);
|
||||
assert_eq!(parsed, Ok(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_rejects_unknown_flag() {
|
||||
let parsed = parse_args_from([String::from("--bogus")]);
|
||||
assert_eq!(parsed, Err(String::from("unsupported argument: --bogus")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,497 @@
|
||||
//! Phase 3 desktop-session preflight checker.
|
||||
//! Validates compositor binary presence, D-Bus session bus, seatd socket,
|
||||
//! and WAYLAND_DISPLAY availability. Does NOT validate real KWin behavior
|
||||
//! (KWin recipe currently provides cmake stubs pending Qt6Quick/QML).
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
use std::{
|
||||
env,
|
||||
io::{Read, Write},
|
||||
os::unix::net::UnixStream,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
time::Duration,
|
||||
};
|
||||
use std::process;
|
||||
|
||||
const PROGRAM: &str = "redbear-phase3-kwin-check";
|
||||
const USAGE: &str = "Usage: redbear-phase3-kwin-check [--json]\n\n\
|
||||
Phase 3 desktop-session preflight check. Validates compositor binary\n\
|
||||
presence, D-Bus session bus reachability, seatd socket presence, active\n\
|
||||
WAYLAND_DISPLAY state, and a bounded wl_display roundtrip.\n\
|
||||
NOTE: Does NOT validate real KWin behavior (KWin is a cmake stub).";
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
const DEFAULT_RUNTIME_DIR: &str = "/run/user/1000";
|
||||
#[cfg(target_os = "redox")]
|
||||
const DBUS_SESSION_DESTINATION: &str = "org.freedesktop.DBus";
|
||||
|
||||
fn parse_args_from<I>(args: I) -> Result<bool, String>
|
||||
where
|
||||
I: IntoIterator<Item = String>,
|
||||
{
|
||||
let mut json_mode = false;
|
||||
|
||||
let mut args = args.into_iter();
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--json" => json_mode = true,
|
||||
"-h" | "--help" => {
|
||||
println!("{USAGE}");
|
||||
return Err(String::new());
|
||||
}
|
||||
_ => return Err(format!("unsupported argument: {arg}")),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(json_mode)
|
||||
}
|
||||
|
||||
fn parse_args() -> Result<bool, String> {
|
||||
parse_args_from(std::env::args().skip(1))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum CheckResult {
|
||||
Pass,
|
||||
Fail,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
impl CheckResult {
|
||||
fn label(self) -> &'static str {
|
||||
match self {
|
||||
CheckResult::Pass => "PASS",
|
||||
CheckResult::Fail => "FAIL",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
struct Check {
|
||||
name: String,
|
||||
result: CheckResult,
|
||||
detail: String,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
impl Check {
|
||||
fn pass(name: &str, detail: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
result: CheckResult::Pass,
|
||||
detail: detail.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn fail(name: &str, detail: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
result: CheckResult::Fail,
|
||||
detail: detail.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
struct Report {
|
||||
checks: Vec<Check>,
|
||||
json_mode: bool,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
impl Report {
|
||||
fn new(json_mode: bool) -> Self {
|
||||
Self {
|
||||
checks: Vec::new(),
|
||||
json_mode,
|
||||
}
|
||||
}
|
||||
|
||||
fn add(&mut self, check: Check) {
|
||||
self.checks.push(check);
|
||||
}
|
||||
|
||||
fn any_failed(&self) -> bool {
|
||||
self.checks.iter().any(|check| check.result == CheckResult::Fail)
|
||||
}
|
||||
|
||||
fn check_passed(&self, name: &str) -> bool {
|
||||
self.checks
|
||||
.iter()
|
||||
.find(|check| check.name == name)
|
||||
.is_some_and(|check| check.result == CheckResult::Pass)
|
||||
}
|
||||
|
||||
fn print(&self) {
|
||||
if self.json_mode {
|
||||
self.print_json();
|
||||
} else {
|
||||
self.print_human();
|
||||
}
|
||||
}
|
||||
|
||||
fn print_human(&self) {
|
||||
println!("=== Red Bear OS Phase 3 Desktop Session Preflight ===");
|
||||
for check in &self.checks {
|
||||
let icon = match check.result {
|
||||
CheckResult::Pass => "[PASS]",
|
||||
CheckResult::Fail => "[FAIL]",
|
||||
};
|
||||
println!("{icon} {}: {}", check.name, check.detail);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_json(&self) {
|
||||
#[derive(serde::Serialize)]
|
||||
struct JsonCheck {
|
||||
name: String,
|
||||
result: String,
|
||||
detail: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct JsonReport {
|
||||
overall_success: bool,
|
||||
compositor_binary: bool,
|
||||
dbus_session_bus_address: bool,
|
||||
dbus_send_session: bool,
|
||||
seatd_socket: bool,
|
||||
wayland_display_active: bool,
|
||||
wayland_roundtrip: bool,
|
||||
checks: Vec<JsonCheck>,
|
||||
}
|
||||
|
||||
let report = JsonReport {
|
||||
overall_success: !self.any_failed(),
|
||||
compositor_binary: self.check_passed("COMPOSITOR_BINARY"),
|
||||
dbus_session_bus_address: self.check_passed("DBUS_SESSION_BUS_ADDRESS"),
|
||||
dbus_send_session: self.check_passed("DBUS_SEND_SESSION"),
|
||||
seatd_socket: self.check_passed("SEATD_SOCKET"),
|
||||
wayland_display_active: self.check_passed("WAYLAND_DISPLAY_ACTIVE"),
|
||||
wayland_roundtrip: self.check_passed("WAYLAND_ROUNDTRIP"),
|
||||
checks: self
|
||||
.checks
|
||||
.iter()
|
||||
.map(|check| JsonCheck {
|
||||
name: check.name.clone(),
|
||||
result: check.result.label().to_string(),
|
||||
detail: check.detail.clone(),
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
if let Err(err) = serde_json::to_writer(std::io::stdout(), &report) {
|
||||
eprintln!("{PROGRAM}: failed to serialize JSON: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
#[derive(Debug, Clone)]
|
||||
struct WaylandEndpoint {
|
||||
path: PathBuf,
|
||||
display: String,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
struct WaylandClient {
|
||||
stream: UnixStream,
|
||||
next_id: u32,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
impl WaylandClient {
|
||||
fn connect(path: &Path) -> Result<Self, String> {
|
||||
let stream = UnixStream::connect(path)
|
||||
.map_err(|err| format!("failed to connect to {}: {err}", path.display()))?;
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_secs(2)))
|
||||
.map_err(|err| format!("failed to set read timeout on {}: {err}", path.display()))?;
|
||||
stream
|
||||
.set_write_timeout(Some(Duration::from_secs(2)))
|
||||
.map_err(|err| format!("failed to set write timeout on {}: {err}", path.display()))?;
|
||||
Ok(Self { stream, next_id: 2 })
|
||||
}
|
||||
|
||||
fn alloc_id(&mut self) -> u32 {
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
id
|
||||
}
|
||||
|
||||
fn send_message(&mut self, object_id: u32, opcode: u16, payload: &[u8]) -> Result<(), String> {
|
||||
let size = 8 + payload.len();
|
||||
let mut message = Vec::with_capacity(size);
|
||||
message.extend_from_slice(&object_id.to_le_bytes());
|
||||
let header = ((size as u32) << 16) | u32::from(opcode);
|
||||
message.extend_from_slice(&header.to_le_bytes());
|
||||
message.extend_from_slice(payload);
|
||||
self.stream
|
||||
.write_all(&message)
|
||||
.map_err(|err| format!("failed to write Wayland message: {err}"))
|
||||
}
|
||||
|
||||
fn read_message(&mut self) -> Result<(u32, u16, Vec<u8>), String> {
|
||||
let mut header = [0u8; 8];
|
||||
self.stream
|
||||
.read_exact(&mut header)
|
||||
.map_err(|err| format!("failed to read Wayland header: {err}"))?;
|
||||
|
||||
let object_id = u32::from_le_bytes([header[0], header[1], header[2], header[3]]);
|
||||
let size_opcode = u32::from_le_bytes([header[4], header[5], header[6], header[7]]);
|
||||
let size = ((size_opcode >> 16) & 0xFFFF) as usize;
|
||||
let opcode = (size_opcode & 0xFFFF) as u16;
|
||||
if size < 8 {
|
||||
return Err(format!("invalid Wayland message size {size}"));
|
||||
}
|
||||
|
||||
let payload_len = size - 8;
|
||||
let mut payload = vec![0u8; payload_len];
|
||||
if payload_len > 0 {
|
||||
self.stream
|
||||
.read_exact(&mut payload)
|
||||
.map_err(|err| format!("failed to read Wayland payload: {err}"))?;
|
||||
}
|
||||
|
||||
Ok((object_id, opcode, payload))
|
||||
}
|
||||
|
||||
fn sync(&mut self) -> Result<u32, String> {
|
||||
let callback_id = self.alloc_id();
|
||||
self.send_message(1, 0, &callback_id.to_le_bytes())?;
|
||||
Ok(callback_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn env_value(name: &str) -> Option<String> {
|
||||
env::var(name).ok().filter(|value| !value.trim().is_empty())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn run_command(program: &str, args: &[&str], label: &str) -> Result<String, String> {
|
||||
let output = Command::new(program)
|
||||
.args(args)
|
||||
.output()
|
||||
.map_err(|err| format!("failed to run {label}: {err}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let detail = if !stderr.trim().is_empty() {
|
||||
stderr.trim().to_string()
|
||||
} else if !stdout.trim().is_empty() {
|
||||
stdout.trim().to_string()
|
||||
} else {
|
||||
String::from("no output")
|
||||
};
|
||||
return Err(format!("{label} exited with status {}: {detail}", output.status));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn resolve_wayland_endpoint() -> Result<WaylandEndpoint, String> {
|
||||
let display = env_value("WAYLAND_DISPLAY")
|
||||
.ok_or_else(|| String::from("WAYLAND_DISPLAY is not set"))?;
|
||||
let runtime_dir = env_value("XDG_RUNTIME_DIR").unwrap_or_else(|| DEFAULT_RUNTIME_DIR.to_string());
|
||||
let path = PathBuf::from(runtime_dir).join(&display);
|
||||
if path.exists() {
|
||||
Ok(WaylandEndpoint { path, display })
|
||||
} else {
|
||||
Err(format!("WAYLAND_DISPLAY is set but socket is missing at {}", path.display()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn require_one_path<'a>(paths: &'a [&'a str]) -> Result<&'a str, String> {
|
||||
for path in paths {
|
||||
if Path::new(path).exists() {
|
||||
return Ok(*path);
|
||||
}
|
||||
}
|
||||
Err(format!("missing any of: {}", paths.join(", ")))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn check_dbus_session_bus() -> (Check, Check) {
|
||||
match env_value("DBUS_SESSION_BUS_ADDRESS") {
|
||||
Some(address) => {
|
||||
let address_check = Check::pass("DBUS_SESSION_BUS_ADDRESS", address);
|
||||
|
||||
if !Path::new("/usr/bin/dbus-send").exists() {
|
||||
return (
|
||||
address_check,
|
||||
Check::fail("DBUS_SEND_SESSION", "missing /usr/bin/dbus-send"),
|
||||
);
|
||||
}
|
||||
|
||||
match run_command(
|
||||
"dbus-send",
|
||||
&[
|
||||
"--session",
|
||||
&format!("--dest={DBUS_SESSION_DESTINATION}"),
|
||||
"--type=method_call",
|
||||
"--print-reply",
|
||||
"/org/freedesktop/DBus",
|
||||
"org.freedesktop.DBus.ListNames",
|
||||
],
|
||||
"dbus-send --session ListNames",
|
||||
) {
|
||||
Ok(output) if !output.trim().is_empty() => (
|
||||
address_check,
|
||||
Check::pass(
|
||||
"DBUS_SEND_SESSION",
|
||||
"dbus-send --session returned a non-empty bus name list",
|
||||
),
|
||||
),
|
||||
Ok(_) => (
|
||||
address_check,
|
||||
Check::fail(
|
||||
"DBUS_SEND_SESSION",
|
||||
"dbus-send --session returned empty output",
|
||||
),
|
||||
),
|
||||
Err(err) => (address_check, Check::fail("DBUS_SEND_SESSION", err)),
|
||||
}
|
||||
}
|
||||
None => (
|
||||
Check::fail(
|
||||
"DBUS_SESSION_BUS_ADDRESS",
|
||||
"DBUS_SESSION_BUS_ADDRESS is not set",
|
||||
),
|
||||
Check::fail(
|
||||
"DBUS_SEND_SESSION",
|
||||
"cannot validate dbus-send without DBUS_SESSION_BUS_ADDRESS",
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn verify_wayland_roundtrip(endpoint: &WaylandEndpoint) -> Result<String, String> {
|
||||
let mut client = WaylandClient::connect(&endpoint.path)?;
|
||||
let callback_id = client.sync()?;
|
||||
|
||||
for _ in 0..8 {
|
||||
let (object_id, opcode, _) = client.read_message()?;
|
||||
if object_id == callback_id && opcode == 0 {
|
||||
return Ok(format!(
|
||||
"{} completed wl_display.sync roundtrip on {}",
|
||||
endpoint.display,
|
||||
endpoint.path.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"{} did not emit callback.done within bounded read window",
|
||||
endpoint.path.display()
|
||||
))
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
let json_mode = parse_args()?;
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
{
|
||||
let _ = json_mode;
|
||||
println!("{PROGRAM}: desktop session preflight requires Redox runtime");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
{
|
||||
let mut report = Report::new(json_mode);
|
||||
|
||||
report.add(match require_one_path(&["/usr/bin/kwin_wayland", "/usr/bin/redbear-compositor"]) {
|
||||
Ok(path) => Check::pass("COMPOSITOR_BINARY", path),
|
||||
Err(err) => Check::fail("COMPOSITOR_BINARY", err),
|
||||
});
|
||||
|
||||
let (dbus_address_check, dbus_send_check) = check_dbus_session_bus();
|
||||
report.add(dbus_address_check);
|
||||
report.add(dbus_send_check);
|
||||
|
||||
report.add(if Path::new("/run/seatd.sock").exists() {
|
||||
Check::pass("SEATD_SOCKET", "/run/seatd.sock")
|
||||
} else {
|
||||
Check::fail("SEATD_SOCKET", "missing /run/seatd.sock")
|
||||
});
|
||||
|
||||
match resolve_wayland_endpoint() {
|
||||
Ok(endpoint) => {
|
||||
report.add(Check::pass(
|
||||
"WAYLAND_DISPLAY_ACTIVE",
|
||||
format!("{} ({})", endpoint.path.display(), endpoint.display),
|
||||
));
|
||||
report.add(match verify_wayland_roundtrip(&endpoint) {
|
||||
Ok(detail) => Check::pass("WAYLAND_ROUNDTRIP", detail),
|
||||
Err(err) => Check::fail("WAYLAND_ROUNDTRIP", err),
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
report.add(Check::fail("WAYLAND_DISPLAY_ACTIVE", err));
|
||||
report.add(Check::fail(
|
||||
"WAYLAND_ROUNDTRIP",
|
||||
"cannot attempt wl_display roundtrip without an active WAYLAND_DISPLAY socket",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
report.print();
|
||||
|
||||
if report.any_failed() {
|
||||
return Err(String::from("one or more Phase 3 preflight checks failed"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = run() {
|
||||
if err.is_empty() {
|
||||
process::exit(0);
|
||||
}
|
||||
eprintln!("{PROGRAM}: {err}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
#[test]
|
||||
fn require_one_path_returns_first_present_path() {
|
||||
let existing = require_one_path(&["/", "/definitely/missing"]);
|
||||
assert_eq!(existing, Ok("/"));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
#[test]
|
||||
fn resolve_wayland_endpoint_requires_display() {
|
||||
let result = {
|
||||
let display = None::<String>;
|
||||
display.ok_or_else(|| String::from("WAYLAND_DISPLAY is not set"))
|
||||
};
|
||||
assert_eq!(result, Err(String::from("WAYLAND_DISPLAY is not set")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_accepts_json_flag() {
|
||||
let parsed = parse_args_from([String::from("--json")]);
|
||||
assert_eq!(parsed, Ok(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_rejects_unknown_flag() {
|
||||
let parsed = parse_args_from([String::from("--bogus")]);
|
||||
assert_eq!(parsed, Err(String::from("unsupported argument: --bogus")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
// Phase 4 KDE Plasma preflight check.
|
||||
// Validates KF6 library presence, plasma binaries, and session entry points.
|
||||
// Does NOT validate real KDE Plasma session behavior (blocked on Qt6Quick/QML + real KWin).
|
||||
|
||||
use std::process;
|
||||
|
||||
const PROGRAM: &str = "redbear-phase4-kde-check";
|
||||
const USAGE: &str = "Usage: redbear-phase4-kde-check [--json]\n\n\
|
||||
Phase 4 KDE Plasma preflight check. Validates KF6 library and plasma binary\n\
|
||||
presence. Does NOT validate real KDE session behavior (gated on Qt6Quick/QML).";
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum CheckResult { Pass, Fail, Skip }
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
impl CheckResult {
|
||||
fn label(self) -> &'static str {
|
||||
match self { Self::Pass => "PASS", Self::Fail => "FAIL", Self::Skip => "SKIP" }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
struct Check { name: String, result: CheckResult, detail: String }
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
impl Check {
|
||||
fn pass(name: &str, detail: &str) -> Self {
|
||||
Check { name: name.to_string(), result: CheckResult::Pass, detail: detail.to_string() }
|
||||
}
|
||||
fn fail(name: &str, detail: &str) -> Self {
|
||||
Check { name: name.to_string(), result: CheckResult::Fail, detail: detail.to_string() }
|
||||
}
|
||||
fn skip(name: &str, detail: &str) -> Self {
|
||||
Check { name: name.to_string(), result: CheckResult::Skip, detail: detail.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
struct Report { checks: Vec<Check>, json_mode: bool }
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
impl Report {
|
||||
fn new(json_mode: bool) -> Self { Report { checks: Vec::new(), json_mode } }
|
||||
fn add(&mut self, check: Check) { self.checks.push(check); }
|
||||
fn any_failed(&self) -> bool { self.checks.iter().any(|c| c.result == CheckResult::Fail) }
|
||||
|
||||
fn print(&self) {
|
||||
if self.json_mode { self.print_json(); } else { self.print_human(); }
|
||||
}
|
||||
|
||||
fn print_human(&self) {
|
||||
for check in &self.checks {
|
||||
let icon = match check.result {
|
||||
CheckResult::Pass => "[PASS]", CheckResult::Fail => "[FAIL]", CheckResult::Skip => "[SKIP]",
|
||||
};
|
||||
println!("{icon} {}: {}", check.name, check.detail);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_json(&self) {
|
||||
#[derive(serde::Serialize)]
|
||||
struct JsonCheck { name: String, result: String, detail: String }
|
||||
#[derive(serde::Serialize)]
|
||||
struct JsonReport {
|
||||
kf6_libs_present: bool, plasma_binaries_present: bool,
|
||||
session_entry: bool, kirigami_available: bool, checks: Vec<JsonCheck>,
|
||||
}
|
||||
let kf6_libs = self.checks.iter().find(|c| c.name == "KF6_LIBRARIES").map_or(false, |c| c.result == CheckResult::Pass);
|
||||
let plasma_bins = self.checks.iter().find(|c| c.name == "PLASMA_BINARIES").map_or(false, |c| c.result == CheckResult::Pass);
|
||||
let session_entry = self.checks.iter().find(|c| c.name == "SESSION_ENTRY").map_or(false, |c| c.result == CheckResult::Pass);
|
||||
let kirigami = self.checks.iter().find(|c| c.name == "KIRIGAMI_STATUS").map_or(false, |c| c.result == CheckResult::Pass);
|
||||
let checks: Vec<JsonCheck> = self.checks.iter().map(|c| JsonCheck {
|
||||
name: c.name.clone(), result: c.result.label().to_string(), detail: c.detail.clone(),
|
||||
}).collect();
|
||||
if let Err(err) = serde_json::to_writer(std::io::stdout(), &JsonReport { kf6_libs_present: kf6_libs, plasma_binaries_present: plasma_bins, session_entry, kirigami_available: kirigami, checks }) {
|
||||
eprintln!("{PROGRAM}: failed to serialize JSON: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn parse_args() -> Result<bool, String> {
|
||||
let mut json_mode = false;
|
||||
for arg in std::env::args().skip(1) {
|
||||
match arg.as_str() {
|
||||
"--json" => json_mode = true,
|
||||
"-h" | "--help" => { println!("{USAGE}"); return Err(String::new()); }
|
||||
_ => return Err(format!("unsupported argument: {arg}")),
|
||||
}
|
||||
}
|
||||
Ok(json_mode)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn check_kf6_libraries() -> Check {
|
||||
let key_libs = [
|
||||
"/usr/lib/libKF6CoreAddons.so", "/usr/lib/libKF6ConfigCore.so",
|
||||
"/usr/lib/libKF6I18n.so", "/usr/lib/libKF6WindowSystem.so",
|
||||
"/usr/lib/libKF6Notifications.so", "/usr/lib/libKF6Service.so",
|
||||
"/usr/lib/libKF6WaylandClient.so",
|
||||
];
|
||||
let mut found = 0usize;
|
||||
let mut missing = Vec::new();
|
||||
for lib in key_libs {
|
||||
if std::path::Path::new(lib).exists() {
|
||||
found += 1;
|
||||
} else {
|
||||
missing.push(lib);
|
||||
}
|
||||
}
|
||||
if found >= 6 {
|
||||
let preview: Vec<_> = missing.iter().take(3).map(|s| s.rsplit('/').next().unwrap_or(s)).collect();
|
||||
if missing.is_empty() {
|
||||
Check::pass("KF6_LIBRARIES", &format!("{}/{} key KF6 libs found", found, key_libs.len()))
|
||||
} else {
|
||||
Check::pass("KF6_LIBRARIES", &format!("{}/{} found, missing: {}", found, key_libs.len(), preview.join(", ")))
|
||||
}
|
||||
} else {
|
||||
Check::fail("KF6_LIBRARIES", &format!("only {}/{} key KF6 libs found", found, key_libs.len()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn check_plasma_binaries() -> Check {
|
||||
let bins = ["/usr/bin/plasmashell", "/usr/bin/systemsettings", "/usr/bin/kwin_wayland_wrapper"];
|
||||
let mut found = 0usize;
|
||||
for bin in bins {
|
||||
if std::path::Path::new(bin).exists() { found += 1; }
|
||||
}
|
||||
if found >= 2 {
|
||||
Check::pass("PLASMA_BINARIES", &format!("{}/{} plasma binaries present", found, bins.len()))
|
||||
} else if found == 1 {
|
||||
Check::fail("PLASMA_BINARIES", &format!("only {}/{} plasma binaries present", found, bins.len()))
|
||||
} else {
|
||||
Check::fail("PLASMA_BINARIES", "no plasma binaries found")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn check_session_entry() -> Check {
|
||||
let entries = ["/usr/bin/startplasma-wayland", "/usr/lib/plasma-session"];
|
||||
for e in entries {
|
||||
if std::path::Path::new(e).exists() {
|
||||
return Check::pass("SESSION_ENTRY", e);
|
||||
}
|
||||
}
|
||||
Check::fail("SESSION_ENTRY", "no KDE session entry point found")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn check_kirigami_status() -> Check {
|
||||
let kirigami_lib = "/usr/lib/libKF6Kirigami.so";
|
||||
if std::path::Path::new(kirigami_lib).exists() {
|
||||
Check::pass("KIRIGAMI_STATUS", "kirigami library present")
|
||||
} else {
|
||||
Check::skip("KIRIGAMI_STATUS", "kirigami not available (QML stub, requires Qt6Quick)")
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
{
|
||||
if std::env::args().any(|a| a == "-h" || a == "--help") { println!("{USAGE}"); return Err(String::new()); }
|
||||
println!("{PROGRAM}: KDE Plasma check requires Redox runtime");
|
||||
return Ok(());
|
||||
}
|
||||
#[cfg(target_os = "redox")]
|
||||
{
|
||||
let json_mode = parse_args()?;
|
||||
let mut report = Report::new(json_mode);
|
||||
report.add(check_kf6_libraries());
|
||||
report.add(check_plasma_binaries());
|
||||
report.add(check_session_entry());
|
||||
report.add(check_kirigami_status());
|
||||
report.print();
|
||||
if report.any_failed() { return Err("one or more Phase 4 checks failed".to_string()); }
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = run() {
|
||||
if err.is_empty() { process::exit(0); }
|
||||
eprintln!("{PROGRAM}: {err}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
use std::{env, path::{Path, PathBuf}};
|
||||
use std::process::{self, Command};
|
||||
use std::{
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use redbear_hwutils::parse_args;
|
||||
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
// Phase 5 Hardware GPU preflight check.
|
||||
// Validates DRM device presence, GPU firmware, and rendering infrastructure.
|
||||
// Does NOT validate real hardware GPU rendering (requires hardware + CS ioctl).
|
||||
|
||||
use std::process;
|
||||
|
||||
const PROGRAM: &str = "redbear-phase5-gpu-check";
|
||||
const USAGE: &str = "Usage: redbear-phase5-gpu-check [--json]\n\n\
|
||||
Phase 5 hardware GPU preflight check. Validates DRM device registration,\n\
|
||||
GPU firmware, and Mesa rendering infrastructure. Hardware validation\n\
|
||||
requires real AMD/Intel GPU + command submission (CS ioctl).";
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum CheckResult { Pass, Fail, Skip }
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
impl CheckResult {
|
||||
fn label(self) -> &'static str {
|
||||
match self { Self::Pass => "PASS", Self::Fail => "FAIL", Self::Skip => "SKIP" }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
struct Check { name: String, result: CheckResult, detail: String }
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
impl Check {
|
||||
fn pass(name: &str, detail: &str) -> Self {
|
||||
Check { name: name.to_string(), result: CheckResult::Pass, detail: detail.to_string() }
|
||||
}
|
||||
fn fail(name: &str, detail: &str) -> Self {
|
||||
Check { name: name.to_string(), result: CheckResult::Fail, detail: detail.to_string() }
|
||||
}
|
||||
fn skip(name: &str, detail: &str) -> Self {
|
||||
Check { name: name.to_string(), result: CheckResult::Skip, detail: detail.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
struct Report { checks: Vec<Check>, json_mode: bool }
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
impl Report {
|
||||
fn new(json_mode: bool) -> Self { Report { checks: Vec::new(), json_mode } }
|
||||
fn add(&mut self, check: Check) { self.checks.push(check); }
|
||||
fn any_failed(&self) -> bool { self.checks.iter().any(|c| c.result == CheckResult::Fail) }
|
||||
|
||||
fn print(&self) {
|
||||
if self.json_mode { self.print_json(); } else { self.print_human(); }
|
||||
}
|
||||
|
||||
fn print_human(&self) {
|
||||
for check in &self.checks {
|
||||
let icon = match check.result {
|
||||
CheckResult::Pass => "[PASS]", CheckResult::Fail => "[FAIL]", CheckResult::Skip => "[SKIP]",
|
||||
};
|
||||
println!("{icon} {}: {}", check.name, check.detail);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_json(&self) {
|
||||
#[derive(serde::Serialize)]
|
||||
struct JsonCheck { name: String, result: String, detail: String }
|
||||
#[derive(serde::Serialize)]
|
||||
struct JsonReport {
|
||||
drm_device: bool, gpu_firmware: bool, mesa_dri: bool,
|
||||
display_modes: bool, checks: Vec<JsonCheck>,
|
||||
}
|
||||
let drm = self.checks.iter().find(|c| c.name == "DRM_DEVICE").map_or(false, |c| c.result == CheckResult::Pass);
|
||||
let firmware = self.checks.iter().find(|c| c.name == "GPU_FIRMWARE").map_or(false, |c| c.result == CheckResult::Pass);
|
||||
let mesa = self.checks.iter().find(|c| c.name == "MESA_DRI").map_or(false, |c| c.result == CheckResult::Pass);
|
||||
let modes = self.checks.iter().find(|c| c.name == "DISPLAY_MODES").map_or(false, |c| c.result == CheckResult::Pass);
|
||||
let checks: Vec<JsonCheck> = self.checks.iter().map(|c| JsonCheck {
|
||||
name: c.name.clone(), result: c.result.label().to_string(), detail: c.detail.clone(),
|
||||
}).collect();
|
||||
if let Err(err) = serde_json::to_writer(std::io::stdout(), &JsonReport { drm_device: drm, gpu_firmware: firmware, mesa_dri: mesa, display_modes: modes, checks }) {
|
||||
eprintln!("{PROGRAM}: failed to serialize JSON: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn parse_args() -> Result<bool, String> {
|
||||
let mut json_mode = false;
|
||||
for arg in std::env::args().skip(1) {
|
||||
match arg.as_str() {
|
||||
"--json" => json_mode = true,
|
||||
"-h" | "--help" => { println!("{USAGE}"); return Err(String::new()); }
|
||||
_ => return Err(format!("unsupported argument: {arg}")),
|
||||
}
|
||||
}
|
||||
Ok(json_mode)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn check_drm_device() -> Check {
|
||||
let paths = ["/scheme/drm/card0", "/dev/dri/card0"];
|
||||
for p in paths {
|
||||
if std::path::Path::new(p).exists() {
|
||||
return Check::pass("DRM_DEVICE", p);
|
||||
}
|
||||
}
|
||||
Check::fail("DRM_DEVICE", "no DRM device found at /scheme/drm/card0 or /dev/dri/card0")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn check_gpu_firmware() -> Check {
|
||||
let firmware_dirs = ["/lib/firmware/amdgpu", "/lib/firmware/i915"];
|
||||
let mut found = false;
|
||||
for dir in firmware_dirs {
|
||||
if let Ok(entries) = std::fs::read_dir(dir) {
|
||||
let count = entries.filter_map(|e| e.ok()).count();
|
||||
if count > 0 {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if found {
|
||||
Check::pass("GPU_FIRMWARE", "GPU firmware blobs present")
|
||||
} else {
|
||||
Check::skip("GPU_FIRMWARE", "no GPU firmware found (may need fetch-firmware.sh)")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn check_mesa_dri_hardware() -> Check {
|
||||
let hw_drivers = ["/usr/lib/dri/radeonsi_dri.so", "/usr/lib/dri/iris_dri.so"];
|
||||
let mut found = Vec::new();
|
||||
for d in hw_drivers {
|
||||
if std::path::Path::new(d).exists() { found.push(d); }
|
||||
}
|
||||
if !found.is_empty() {
|
||||
let names: Vec<_> = found.iter().map(|s| s.rsplit('/').next().unwrap_or(s)).collect();
|
||||
Check::pass("MESA_DRI", &format!("{} hardware DRI driver(s): {}", found.len(), names.join(", ")))
|
||||
} else {
|
||||
Check::fail("MESA_DRI", "no hardware DRI drivers found (llvmpipe software only)")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn check_display_modes() -> Check {
|
||||
let connector_dir = "/scheme/drm/card0/connectors";
|
||||
match std::fs::read_dir(connector_dir) {
|
||||
Ok(entries) => {
|
||||
let count = entries.filter_map(|e| e.ok()).count();
|
||||
if count > 0 {
|
||||
Check::pass("DISPLAY_MODES", &format!("{} connector(s) found", count))
|
||||
} else {
|
||||
Check::fail("DISPLAY_MODES", "no connectors found")
|
||||
}
|
||||
}
|
||||
Err(_) => Check::skip("DISPLAY_MODES", "cannot enumerate connectors (may need hardware GPU)")
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
{
|
||||
if std::env::args().any(|a| a == "-h" || a == "--help") { println!("{USAGE}"); return Err(String::new()); }
|
||||
println!("{PROGRAM}: GPU check requires Redox runtime");
|
||||
return Ok(());
|
||||
}
|
||||
#[cfg(target_os = "redox")]
|
||||
{
|
||||
let json_mode = parse_args()?;
|
||||
let mut report = Report::new(json_mode);
|
||||
report.add(check_drm_device());
|
||||
report.add(check_gpu_firmware());
|
||||
report.add(check_mesa_dri_hardware());
|
||||
report.add(check_display_modes());
|
||||
report.print();
|
||||
if report.any_failed() { return Err("one or more Phase 5 checks failed".to_string()); }
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = run() {
|
||||
if err.is_empty() { process::exit(0); }
|
||||
eprintln!("{PROGRAM}: {err}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
@@ -298,7 +298,11 @@ fn validate_upower(list_names_output: &str) -> Result<(), String> {
|
||||
println!("UPOWER_RUNTIME_BATTERIES={}", runtime.battery_ids.len());
|
||||
println!(
|
||||
"UPOWER_POWER_SURFACE={}",
|
||||
if power_surface_available { "available" } else { "unavailable" }
|
||||
if power_surface_available {
|
||||
"available"
|
||||
} else {
|
||||
"unavailable"
|
||||
}
|
||||
);
|
||||
|
||||
let enumerate_output = run_command_with_retry(
|
||||
|
||||
@@ -5,8 +5,7 @@ use redbear_hwutils::parse_args;
|
||||
use serde_json::Value;
|
||||
|
||||
const PROGRAM: &str = "redbear-phase5-wifi-analyze";
|
||||
const USAGE: &str =
|
||||
"Usage: redbear-phase5-wifi-analyze <capture.json>\n\nSummarize a Wi-Fi capture bundle into likely blocker categories.";
|
||||
const USAGE: &str = "Usage: redbear-phase5-wifi-analyze <capture.json>\n\nSummarize a Wi-Fi capture bundle into likely blocker categories.";
|
||||
|
||||
fn read_text<'a>(value: &'a Value, path: &[&str]) -> &'a str {
|
||||
let mut current = value;
|
||||
|
||||
@@ -114,8 +114,12 @@ fn run() -> Result<(), String> {
|
||||
require_contains("redbear_info", &info, "wifi_disconnect_result")?;
|
||||
|
||||
println!("PASS: bounded Intel Wi-Fi runtime path exercised inside target runtime");
|
||||
println!("NOTE: the packaged runtime checker currently validates the bounded open-profile path by default; WPA2-PSK is implemented and host/unit-verified elsewhere in-repo but is not yet the default packaged runtime proof");
|
||||
println!("NOTE: this still does not prove real AP scan/auth/association, packet flow, DHCP success over Wi-Fi, or validated end-to-end connectivity");
|
||||
println!(
|
||||
"NOTE: the packaged runtime checker currently validates the bounded open-profile path by default; WPA2-PSK is implemented and host/unit-verified elsewhere in-repo but is not yet the default packaged runtime proof"
|
||||
);
|
||||
println!(
|
||||
"NOTE: this still does not prove real AP scan/auth/association, packet flow, DHCP success over Wi-Fi, or validated end-to-end connectivity"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@ use std::process;
|
||||
use redbear_hwutils::parse_args;
|
||||
|
||||
const PROGRAM: &str = "redbear-usb-check";
|
||||
const USAGE: &str =
|
||||
"Usage: redbear-usb-check\n\nCheck the USB stack inside a Red Bear guest.\n\nWalks the usb scheme tree and reports controller and device status.";
|
||||
const USAGE: &str = "Usage: redbear-usb-check\n\nCheck the USB stack inside a Red Bear guest.\n\nWalks the usb scheme tree and reports controller and device status.";
|
||||
|
||||
fn list_scheme_dir(path: &str) -> Vec<String> {
|
||||
match fs::read_dir(path) {
|
||||
|
||||
@@ -115,7 +115,9 @@ fn parse_pci_id_database(text: &str) -> PciIdDatabase {
|
||||
let Some(vendor_id) = current_vendor else {
|
||||
continue;
|
||||
};
|
||||
let mut parts = rest.splitn(2, char::is_whitespace).filter(|part| !part.is_empty());
|
||||
let mut parts = rest
|
||||
.splitn(2, char::is_whitespace)
|
||||
.filter(|part| !part.is_empty());
|
||||
let Some(device_hex) = parts.next() else {
|
||||
continue;
|
||||
};
|
||||
@@ -131,7 +133,9 @@ fn parse_pci_id_database(text: &str) -> PciIdDatabase {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut parts = line.splitn(2, char::is_whitespace).filter(|part| !part.is_empty());
|
||||
let mut parts = line
|
||||
.splitn(2, char::is_whitespace)
|
||||
.filter(|part| !part.is_empty());
|
||||
let Some(vendor_hex) = parts.next() else {
|
||||
continue;
|
||||
};
|
||||
@@ -142,7 +146,9 @@ fn parse_pci_id_database(text: &str) -> PciIdDatabase {
|
||||
continue;
|
||||
};
|
||||
current_vendor = Some(vendor_id);
|
||||
database.vendor_names.insert(vendor_id, name.trim().to_string());
|
||||
database
|
||||
.vendor_names
|
||||
.insert(vendor_id, name.trim().to_string());
|
||||
}
|
||||
|
||||
database
|
||||
@@ -237,7 +243,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn describe_usb_device_empty_manufacturer_filtered() {
|
||||
assert_eq!(describe_usb_device(Some(""), Some("USB Mouse")), "USB Mouse");
|
||||
assert_eq!(
|
||||
describe_usb_device(Some(""), Some("USB Mouse")),
|
||||
"USB Mouse"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -254,14 +263,22 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parse_args_help_flag_returns_err_empty() {
|
||||
let result = parse_args("prog", "usage text", vec!["prog".to_string(), "--help".to_string()]);
|
||||
let result = parse_args(
|
||||
"prog",
|
||||
"usage text",
|
||||
vec!["prog".to_string(), "--help".to_string()],
|
||||
);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_h_flag_returns_err_empty() {
|
||||
let result = parse_args("prog", "usage text", vec!["prog".to_string(), "-h".to_string()]);
|
||||
let result = parse_args(
|
||||
"prog",
|
||||
"usage text",
|
||||
vec!["prog".to_string(), "-h".to_string()],
|
||||
);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err(), "");
|
||||
}
|
||||
@@ -275,7 +292,10 @@ mod tests {
|
||||
);
|
||||
assert!(result.is_err());
|
||||
let msg = result.unwrap_err();
|
||||
assert!(msg.contains("unsupported arguments"), "expected 'unsupported arguments' in: {msg}");
|
||||
assert!(
|
||||
msg.contains("unsupported arguments"),
|
||||
"expected 'unsupported arguments' in: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
// --- original pci_id_database tests ---
|
||||
|
||||
Reference in New Issue
Block a user