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:
@@ -8,10 +8,11 @@ DYNAMIC_INIT
|
||||
|
||||
mkdir -p "${COOKBOOK_STAGE}/usr/lib"
|
||||
|
||||
cargo build --lib --target "${TARGET}"
|
||||
unset CARGO_TARGET_DIR
|
||||
cargo build --manifest-path "${COOKBOOK_SOURCE}/Cargo.toml" --lib --target "${TARGET}" --release
|
||||
|
||||
cp "${COOKBOOK_SOURCE}/target/${TARGET}/debug/libredox_driver_sys.a" \
|
||||
cp "${COOKBOOK_SOURCE}/target/${TARGET}/release/libredox_driver_sys.a" \
|
||||
"${COOKBOOK_STAGE}/usr/lib/libredox_driver_sys.a"
|
||||
cp "${COOKBOOK_SOURCE}/target/${TARGET}/debug/libredox_driver_sys.rlib" \
|
||||
cp "${COOKBOOK_SOURCE}/target/${TARGET}/release/libredox_driver_sys.rlib" \
|
||||
"${COOKBOOK_STAGE}/usr/lib/libredox_driver_sys.rlib"
|
||||
"""
|
||||
|
||||
@@ -8,6 +8,7 @@ export SEATD_SOCK=/run/seatd.sock
|
||||
export QT_PLUGIN_PATH="${QT_PLUGIN_PATH:-/usr/plugins}"
|
||||
export QT_QPA_PLATFORM_PLUGIN_PATH="${QT_QPA_PLATFORM_PLUGIN_PATH:-/usr/plugins/platforms}"
|
||||
export QML2_IMPORT_PATH="${QML2_IMPORT_PATH:-/usr/qml}"
|
||||
export QT_WAYLAND_SHELL_INTEGRATION="${QT_WAYLAND_SHELL_INTEGRATION:-xdg-shell}"
|
||||
export XCURSOR_THEME="${XCURSOR_THEME:-Pop}"
|
||||
export XKB_CONFIG_ROOT="${XKB_CONFIG_ROOT:-/usr/share/X11/xkb}"
|
||||
|
||||
@@ -22,7 +23,12 @@ if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ] && command -v dbus-launch >/dev/null 2
|
||||
fi
|
||||
|
||||
if ! command -v kwin_wayland_wrapper >/dev/null 2>&1; then
|
||||
echo "redbear-greeter-compositor: kwin_wayland_wrapper not found in PATH" >&2
|
||||
# Fall back to redbear-compositor (simpler Rust compositor)
|
||||
if command -v /usr/bin/redbear-compositor >/dev/null 2>&1 || command -v redbear-compositor >/dev/null 2>&1; then
|
||||
echo "redbear-greeter-compositor: kwin_wayland_wrapper not found, using redbear-compositor" >&2
|
||||
exec /usr/bin/redbear-compositor
|
||||
fi
|
||||
echo "redbear-greeter-compositor: kwin_wayland_wrapper not found, and redbear-compositor not found either" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -39,9 +45,9 @@ if [ -z "${KWIN_DRM_DEVICES:-}" ]; then
|
||||
fi
|
||||
|
||||
if [ -n "${KWIN_DRM_DEVICES:-}" ]; then
|
||||
echo "redbear-greeter-compositor: using DRM KWin backend (KWIN_DRM_DEVICES=${KWIN_DRM_DEVICES})" >&2
|
||||
echo "redbear-greeter-compositor: using DRM compositor backend (KWIN_DRM_DEVICES=${KWIN_DRM_DEVICES})" >&2
|
||||
exec kwin_wayland_wrapper --drm
|
||||
else
|
||||
echo "redbear-greeter-compositor: using virtual KWin backend (set KWIN_DRM_DEVICES to enable DRM)" >&2
|
||||
echo "redbear-greeter-compositor: using virtual compositor backend (set KWIN_DRM_DEVICES to enable DRM)" >&2
|
||||
exec kwin_wayland_wrapper --virtual
|
||||
fi
|
||||
|
||||
@@ -18,3 +18,11 @@ template = "cargo"
|
||||
"/usr/bin/redbear-phase5-wifi-run" = "redbear-phase5-wifi-run"
|
||||
"/usr/bin/redbear-phase5-wifi-analyze" = "redbear-phase5-wifi-analyze"
|
||||
"/usr/bin/redbear-phase5-wifi-link-check" = "redbear-phase5-wifi-link-check"
|
||||
"/usr/bin/redbear-phase1-evdev-check" = "redbear-phase1-evdev-check"
|
||||
"/usr/bin/redbear-phase1-udev-check" = "redbear-phase1-udev-check"
|
||||
"/usr/bin/redbear-phase1-firmware-check" = "redbear-phase1-firmware-check"
|
||||
"/usr/bin/redbear-phase1-drm-check" = "redbear-phase1-drm-check"
|
||||
"/usr/bin/redbear-phase2-wayland-check" = "redbear-phase2-wayland-check"
|
||||
"/usr/bin/redbear-phase3-kwin-check" = "redbear-phase3-kwin-check"
|
||||
"/usr/bin/redbear-phase4-kde-check" = "redbear-phase4-kde-check"
|
||||
"/usr/bin/redbear-phase5-gpu-check" = "redbear-phase5-gpu-check"
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
@@ -24,12 +24,13 @@ const VIRTIO_NET_VENDOR_ID: u16 = 0x1af4;
|
||||
const VIRTIO_NET_DEVICE_ID: u16 = 0x1000;
|
||||
const BLUETOOTH_STATUS_FRESHNESS_SECS: u64 = 90;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum OutputMode {
|
||||
Table,
|
||||
Json,
|
||||
Test,
|
||||
Quirks,
|
||||
Probe,
|
||||
Help,
|
||||
}
|
||||
|
||||
@@ -476,6 +477,26 @@ fn run() -> Result<(), String> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if options.mode == OutputMode::Probe {
|
||||
let result = Phase1ProbeResult {
|
||||
evdev_active: probe_evdev_active(),
|
||||
udev_active: probe_udev_active(),
|
||||
firmware_active: probe_firmware_active(),
|
||||
drm_active: probe_drm_active(),
|
||||
time_active: probe_time_active(),
|
||||
};
|
||||
print_probe(&result);
|
||||
let all_present = result.evdev_active
|
||||
&& result.udev_active
|
||||
&& result.firmware_active
|
||||
&& result.drm_active
|
||||
&& result.time_active;
|
||||
if all_present {
|
||||
return Ok(());
|
||||
}
|
||||
return Err("some Phase 1 services are not present".to_string());
|
||||
}
|
||||
|
||||
let report = collect_report(&runtime);
|
||||
|
||||
match options.mode {
|
||||
@@ -483,6 +504,7 @@ fn run() -> Result<(), String> {
|
||||
OutputMode::Json => print_json(&report),
|
||||
OutputMode::Test => print_tests(&report, options.verbose),
|
||||
OutputMode::Quirks => {}
|
||||
OutputMode::Probe => {}
|
||||
OutputMode::Help => {}
|
||||
}
|
||||
|
||||
@@ -1093,6 +1115,122 @@ fn collect_quirks(runtime: &Runtime) -> QuirksReport {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Phase1ProbeResult {
|
||||
evdev_active: bool,
|
||||
udev_active: bool,
|
||||
firmware_active: bool,
|
||||
drm_active: bool,
|
||||
time_active: bool,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn probe_evdev_active() -> bool {
|
||||
std::fs::read_dir("/scheme/")
|
||||
.map(|mut entries| {
|
||||
entries.any(|entry| {
|
||||
entry.map_or(false, |entry| {
|
||||
entry.file_name().to_string_lossy().starts_with("evdev")
|
||||
})
|
||||
})
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
fn probe_evdev_active() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn probe_udev_active() -> bool {
|
||||
std::fs::read_dir("/scheme/")
|
||||
.map(|mut entries| {
|
||||
entries.any(|entry| {
|
||||
entry.map_or(false, |entry| entry.file_name().to_string_lossy() == "udev")
|
||||
})
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
fn probe_udev_active() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn probe_firmware_active() -> bool {
|
||||
std::fs::read_dir("/scheme/")
|
||||
.map(|mut entries| {
|
||||
entries.any(|entry| {
|
||||
entry.map_or(false, |entry| entry.file_name().to_string_lossy() == "firmware")
|
||||
})
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
fn probe_firmware_active() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn probe_drm_active() -> bool {
|
||||
std::fs::read_dir("/scheme/")
|
||||
.map(|mut entries| {
|
||||
entries.any(|entry| {
|
||||
entry.map_or(false, |entry| entry.file_name().to_string_lossy() == "drm")
|
||||
})
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
fn probe_drm_active() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn probe_time_active() -> bool {
|
||||
std::path::Path::new("/scheme/time").exists()
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
fn probe_time_active() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn print_probe(result: &Phase1ProbeResult) {
|
||||
let mark = |present: bool| if present { "✓ PRESENT" } else { "✗ ABSENT" };
|
||||
|
||||
println!("Phase 1 Service Probes:");
|
||||
println!(" evdevd {}", mark(result.evdev_active));
|
||||
println!(" udev-shim {}", mark(result.udev_active));
|
||||
println!(" firmware {}", mark(result.firmware_active));
|
||||
println!(" drm {}", mark(result.drm_active));
|
||||
println!(" time {}", mark(result.time_active));
|
||||
|
||||
let all = result.evdev_active
|
||||
&& result.udev_active
|
||||
&& result.firmware_active
|
||||
&& result.drm_active
|
||||
&& result.time_active;
|
||||
let most = result.evdev_active as u8
|
||||
+ result.udev_active as u8
|
||||
+ result.firmware_active as u8
|
||||
+ result.drm_active as u8
|
||||
+ result.time_active as u8;
|
||||
|
||||
println!();
|
||||
if all {
|
||||
println!("ALL PHASE 1 SERVICES PRESENT");
|
||||
} else if most >= 3 {
|
||||
println!("MOSTLY PRESENT, SOME GAPS ({}/5)", most);
|
||||
} else {
|
||||
println!("SIGNIFICANT GAPS REMAIN ({}/5)", most);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_quirk_toml(name: &str, content: &str) -> Result<QuirkFile, String> {
|
||||
let document: Value = content
|
||||
.parse()
|
||||
@@ -1286,6 +1424,9 @@ where
|
||||
if mode == OutputMode::Quirks {
|
||||
return Err("cannot combine --json with --quirks".to_string());
|
||||
}
|
||||
if mode == OutputMode::Probe {
|
||||
return Err("cannot combine --json with --probe".to_string());
|
||||
}
|
||||
mode = OutputMode::Json;
|
||||
}
|
||||
"--test" => {
|
||||
@@ -1295,6 +1436,9 @@ where
|
||||
if mode == OutputMode::Quirks {
|
||||
return Err("cannot combine --test with --quirks".to_string());
|
||||
}
|
||||
if mode == OutputMode::Probe {
|
||||
return Err("cannot combine --test with --probe".to_string());
|
||||
}
|
||||
mode = OutputMode::Test;
|
||||
}
|
||||
"--quirks" => {
|
||||
@@ -1304,8 +1448,23 @@ where
|
||||
if mode == OutputMode::Test {
|
||||
return Err("cannot combine --quirks with --test".to_string());
|
||||
}
|
||||
if mode == OutputMode::Probe {
|
||||
return Err("cannot combine --quirks with --probe".to_string());
|
||||
}
|
||||
mode = OutputMode::Quirks;
|
||||
}
|
||||
"--probe" => {
|
||||
if mode == OutputMode::Json {
|
||||
return Err("cannot combine --probe with --json".to_string());
|
||||
}
|
||||
if mode == OutputMode::Test {
|
||||
return Err("cannot combine --probe with --test".to_string());
|
||||
}
|
||||
if mode == OutputMode::Quirks {
|
||||
return Err("cannot combine --probe with --quirks".to_string());
|
||||
}
|
||||
mode = OutputMode::Probe;
|
||||
}
|
||||
"-h" | "--help" => mode = OutputMode::Help,
|
||||
_ => return Err(format!("unknown argument: {arg}")),
|
||||
}
|
||||
@@ -2071,14 +2230,14 @@ fn print_json(report: &Report<'_>) {
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
println!("Usage: redbear-info [--verbose|-v] [--json|--test|--quirks]");
|
||||
println!("Usage: redbear-info [--verbose|-v] [--json|--test|--quirks|--probe]");
|
||||
println!();
|
||||
println!("Passive runtime integration report for Red Bear OS.");
|
||||
println!();
|
||||
println!("This tool distinguishes:");
|
||||
println!(" present artifact or config exists");
|
||||
println!(" active live runtime surface exists");
|
||||
println!(" functional read-only runtime probe succeeded");
|
||||
println!(" functional read-only runtime probe succeeded (table/test output; --probe mode uses PRESENT/ABSENT)");
|
||||
println!();
|
||||
println!("Connected means the local networking stack has a configured address.");
|
||||
println!("It does not prove internet reachability.");
|
||||
@@ -2088,6 +2247,7 @@ fn print_help() {
|
||||
println!(" --json Print structured JSON");
|
||||
println!(" --test Print suggested diagnostic commands");
|
||||
println!(" --quirks Print configured hardware quirk data");
|
||||
println!(" --probe Probe Phase 1 service liveness (evdevd, udev-shim, firmware-loader, drm, time)");
|
||||
println!(" -h, --help Show this help message");
|
||||
}
|
||||
|
||||
@@ -3425,6 +3585,120 @@ mod tests {
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_accepts_probe_mode() {
|
||||
let options = parse_args([
|
||||
"redbear-info".to_string(),
|
||||
"--probe".to_string(),
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(options.mode, OutputMode::Probe));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_rejects_probe_with_other_output_modes() {
|
||||
// probe first, then --json: --json is the current arg, error puts current arg first
|
||||
assert_eq!(
|
||||
parse_args([
|
||||
"redbear-info".to_string(),
|
||||
"--probe".to_string(),
|
||||
"--json".to_string(),
|
||||
])
|
||||
.err(),
|
||||
Some("cannot combine --json with --probe".to_string())
|
||||
);
|
||||
// --test first, then --probe: --probe is the current arg
|
||||
assert_eq!(
|
||||
parse_args([
|
||||
"redbear-info".to_string(),
|
||||
"--test".to_string(),
|
||||
"--probe".to_string(),
|
||||
])
|
||||
.err(),
|
||||
Some("cannot combine --probe with --test".to_string())
|
||||
);
|
||||
// --quirks first, then --probe: --probe is the current arg
|
||||
assert_eq!(
|
||||
parse_args([
|
||||
"redbear-info".to_string(),
|
||||
"--quirks".to_string(),
|
||||
"--probe".to_string(),
|
||||
])
|
||||
.err(),
|
||||
Some("cannot combine --probe with --quirks".to_string())
|
||||
);
|
||||
// Reverse direction: --json/--test/--quirks after --probe
|
||||
assert_eq!(
|
||||
parse_args([
|
||||
"redbear-info".to_string(),
|
||||
"--json".to_string(),
|
||||
"--probe".to_string(),
|
||||
])
|
||||
.err(),
|
||||
Some("cannot combine --probe with --json".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args([
|
||||
"redbear-info".to_string(),
|
||||
"--probe".to_string(),
|
||||
"--test".to_string(),
|
||||
])
|
||||
.err(),
|
||||
Some("cannot combine --test with --probe".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn probe_functions_return_false_on_host() {
|
||||
assert!(!probe_evdev_active());
|
||||
assert!(!probe_udev_active());
|
||||
assert!(!probe_firmware_active());
|
||||
assert!(!probe_drm_active());
|
||||
assert!(!probe_time_active());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn print_probe_outputs_all_present() {
|
||||
let result = Phase1ProbeResult {
|
||||
evdev_active: true,
|
||||
udev_active: true,
|
||||
firmware_active: true,
|
||||
drm_active: true,
|
||||
time_active: true,
|
||||
};
|
||||
assert!(result.evdev_active);
|
||||
assert!(result.udev_active);
|
||||
assert!(result.firmware_active);
|
||||
assert!(result.drm_active);
|
||||
assert!(result.time_active);
|
||||
let all = result.evdev_active
|
||||
&& result.udev_active
|
||||
&& result.firmware_active
|
||||
&& result.drm_active
|
||||
&& result.time_active;
|
||||
assert!(all, "all five services should be present");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn print_probe_reports_gaps_count() {
|
||||
let result = Phase1ProbeResult {
|
||||
evdev_active: true,
|
||||
udev_active: true,
|
||||
firmware_active: false,
|
||||
drm_active: true,
|
||||
time_active: false,
|
||||
};
|
||||
let count = result.evdev_active as u8
|
||||
+ result.udev_active as u8
|
||||
+ result.firmware_active as u8
|
||||
+ result.drm_active as u8
|
||||
+ result.time_active as u8;
|
||||
assert_eq!(count, 3);
|
||||
assert!(!result.firmware_active);
|
||||
assert!(!result.time_active);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_accepts_quirks_mode() {
|
||||
let options = parse_args([
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
#TODO: Runtime proof requires executing these binaries inside a Red Bear OS Phase 1 validation target.
|
||||
|
||||
[source]
|
||||
path = "source"
|
||||
|
||||
[build]
|
||||
template = "custom"
|
||||
dependencies = ["relibc"]
|
||||
script = """
|
||||
DYNAMIC_INIT
|
||||
|
||||
RELIBC_STAGE="${COOKBOOK_ROOT}/recipes/core/relibc/target/${TARGET}/stage"
|
||||
if [ ! -d "${RELIBC_STAGE}/usr" ]; then
|
||||
RELIBC_STAGE="${COOKBOOK_ROOT}/recipes/core/relibc/target/${TARGET}/stage.tmp"
|
||||
fi
|
||||
|
||||
mkdir -p "${COOKBOOK_SYSROOT}"
|
||||
if [ -d "${RELIBC_STAGE}/usr" ]; then
|
||||
rsync -av "${RELIBC_STAGE}/usr/" "${COOKBOOK_SYSROOT}/"
|
||||
fi
|
||||
|
||||
TARGET_CC="${TARGET}-gcc"
|
||||
if ! command -v "${TARGET_CC}" >/dev/null 2>&1; then
|
||||
TARGET_CC="cc"
|
||||
fi
|
||||
|
||||
rsync -av "${COOKBOOK_SOURCE}/" ./
|
||||
make -j "${COOKBOOK_MAKE_JOBS}" \
|
||||
CC="${CC_WRAPPER} ${TARGET_CC}" \
|
||||
CPPFLAGS="-I${COOKBOOK_SYSROOT}/include" \
|
||||
CFLAGS="-O2 -Wall -Wextra -Werror" \
|
||||
LDFLAGS="--sysroot=${COOKBOOK_SYSROOT} -L${COOKBOOK_SYSROOT}/lib"
|
||||
|
||||
mkdir -p "${COOKBOOK_STAGE}/home/user/relibc-phase1-tests"
|
||||
cp -v test_signalfd_wayland test_timerfd_qt6 test_eventfd_qt6 test_shm_open_qt6 \
|
||||
test_sem_open_qt6 test_waitid_qt6 "${COOKBOOK_STAGE}/home/user/relibc-phase1-tests/"
|
||||
"""
|
||||
|
||||
[package]
|
||||
dependencies = ["gcc13"]
|
||||
@@ -0,0 +1,22 @@
|
||||
PROGRAMS = \
|
||||
test_signalfd_wayland \
|
||||
test_timerfd_qt6 \
|
||||
test_eventfd_qt6 \
|
||||
test_shm_open_qt6 \
|
||||
test_sem_open_qt6 \
|
||||
test_waitid_qt6
|
||||
|
||||
CC ?= cc
|
||||
CPPFLAGS ?=
|
||||
CFLAGS ?= -O2 -Wall -Wextra -Werror
|
||||
LDFLAGS ?=
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(PROGRAMS)
|
||||
|
||||
%: %.c
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $< -o $@ $(LDFLAGS)
|
||||
|
||||
clean:
|
||||
rm -f $(PROGRAMS)
|
||||
Binary file not shown.
@@ -0,0 +1,45 @@
|
||||
#define _GNU_SOURCE 1
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef __redox__
|
||||
#define EFD_CLOEXEC 0x80000
|
||||
#define EFD_NONBLOCK 0x800
|
||||
int eventfd(unsigned int initval, int flags);
|
||||
#else
|
||||
#include <sys/eventfd.h>
|
||||
#endif
|
||||
|
||||
static int fail_step(const char *step) {
|
||||
printf("FAIL eventfd: %s (errno=%d)\n", step, errno);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
uint64_t expected = 42;
|
||||
uint64_t observed = 0;
|
||||
int efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
|
||||
|
||||
if (efd < 0) return fail_step("eventfd");
|
||||
if (write(efd, &expected, sizeof(expected)) != (ssize_t)sizeof(expected)) return fail_step("write first");
|
||||
if (read(efd, &observed, sizeof(observed)) != (ssize_t)sizeof(observed)) return fail_step("read first");
|
||||
if (observed != expected) {
|
||||
printf("FAIL eventfd: first read=%llu\n", (unsigned long long)observed);
|
||||
return 1;
|
||||
}
|
||||
|
||||
expected = 7;
|
||||
if (write(efd, &expected, sizeof(expected)) != (ssize_t)sizeof(expected)) return fail_step("write second");
|
||||
if (read(efd, &observed, sizeof(observed)) != (ssize_t)sizeof(observed)) return fail_step("read second");
|
||||
if (observed != expected) {
|
||||
printf("FAIL eventfd: second read=%llu\n", (unsigned long long)observed);
|
||||
return 1;
|
||||
}
|
||||
if (close(efd) < 0) return fail_step("close");
|
||||
|
||||
printf("PASS eventfd\n");
|
||||
return 0;
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,90 @@
|
||||
#define _GNU_SOURCE 1
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <semaphore.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int fail_step(const char *step) {
|
||||
printf("FAIL sem_open: %s (errno=%d)\n", step, errno);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
static const char name[] = "/rb_test_sem";
|
||||
char go = 'G';
|
||||
char ready = 0;
|
||||
int child_value = -1;
|
||||
int parent_value = -1;
|
||||
int parent_to_child[2];
|
||||
int value = -1;
|
||||
int sync_pipe[2];
|
||||
int status;
|
||||
sem_t *sem;
|
||||
pid_t child;
|
||||
|
||||
errno = 0;
|
||||
if (sem_unlink(name) < 0 && errno != ENOENT) return fail_step("sem_unlink pre-clean");
|
||||
sem = sem_open(name, O_CREAT, 0666, 0);
|
||||
if (sem == SEM_FAILED) return fail_step("sem_open create");
|
||||
if (sem_getvalue(sem, &value) < 0 || value != 0) {
|
||||
printf("FAIL sem_open: initial value=%d\n", value);
|
||||
return 1;
|
||||
}
|
||||
if (pipe(sync_pipe) < 0) return fail_step("pipe");
|
||||
if (pipe(parent_to_child) < 0) return fail_step("pipe parent_to_child");
|
||||
|
||||
child = fork();
|
||||
if (child < 0) return fail_step("fork");
|
||||
if (child == 0) {
|
||||
ready = 'R';
|
||||
close(sync_pipe[0]);
|
||||
close(parent_to_child[1]);
|
||||
sem_t *child_sem = sem_open(name, 0);
|
||||
if (child_sem == SEM_FAILED) _Exit(1);
|
||||
if (write(sync_pipe[1], &ready, 1) != 1) _Exit(2);
|
||||
if (read(parent_to_child[0], &go, 1) != 1) _Exit(3);
|
||||
if (sem_wait(child_sem) < 0) _Exit(4);
|
||||
if (sem_getvalue(child_sem, &child_value) < 0 || child_value != 0) _Exit(5);
|
||||
if (sem_post(child_sem) < 0) _Exit(6);
|
||||
if (sem_getvalue(child_sem, &child_value) < 0 || child_value != 1) _Exit(7);
|
||||
if (sem_close(child_sem) < 0) _Exit(8);
|
||||
close(parent_to_child[0]);
|
||||
close(sync_pipe[1]);
|
||||
_Exit(0);
|
||||
}
|
||||
|
||||
close(sync_pipe[1]);
|
||||
close(parent_to_child[0]);
|
||||
if (read(sync_pipe[0], &ready, 1) != 1) return fail_step("child ready read");
|
||||
if (sem_post(sem) < 0) return fail_step("parent sem_post");
|
||||
if (sem_getvalue(sem, &parent_value) < 0 || parent_value != 1) {
|
||||
printf("FAIL sem_open: post value=%d\n", parent_value);
|
||||
return 1;
|
||||
}
|
||||
if (write(parent_to_child[1], &go, 1) != 1) return fail_step("release child");
|
||||
if (close(parent_to_child[1]) < 0) return fail_step("close parent_to_child");
|
||||
if (read(sync_pipe[0], &ready, 1) != 0) return fail_step("child completion pipe");
|
||||
if (sem_getvalue(sem, &parent_value) < 0 || parent_value != 1) {
|
||||
printf("FAIL sem_open: child post value=%d\n", parent_value);
|
||||
return 1;
|
||||
}
|
||||
close(sync_pipe[0]);
|
||||
if (sem_wait(sem) < 0) return fail_step("parent sem_wait");
|
||||
if (waitpid(child, &status, 0) < 0) return fail_step("waitpid");
|
||||
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
|
||||
printf("FAIL sem_open: child status=%d\n", status);
|
||||
return 1;
|
||||
}
|
||||
if (sem_getvalue(sem, &value) < 0 || value != 0) {
|
||||
printf("FAIL sem_open: final value=%d\n", value);
|
||||
return 1;
|
||||
}
|
||||
if (sem_close(sem) < 0 || sem_unlink(name) < 0) return fail_step("cleanup");
|
||||
|
||||
printf("PASS sem_open\n");
|
||||
return 0;
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,47 @@
|
||||
#define _GNU_SOURCE 1
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int fail_step(const char *step) {
|
||||
printf("FAIL shm_open: %s (errno=%d)\n", step, errno);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
static const char name[] = "/rb_test_shm";
|
||||
uint32_t *first;
|
||||
uint32_t *second;
|
||||
int fd;
|
||||
int second_fd;
|
||||
|
||||
errno = 0;
|
||||
if (shm_unlink(name) < 0 && errno != ENOENT) return fail_step("shm_unlink pre-clean");
|
||||
fd = shm_open(name, O_CREAT | O_RDWR, 0666);
|
||||
if (fd < 0) return fail_step("shm_open");
|
||||
if (ftruncate(fd, 4096) < 0) return fail_step("ftruncate");
|
||||
second_fd = shm_open(name, O_RDWR, 0666);
|
||||
if (second_fd < 0) return fail_step("shm_open reopen");
|
||||
|
||||
first = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
||||
if (first == MAP_FAILED) return fail_step("mmap first");
|
||||
second = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, second_fd, 0);
|
||||
if (second == MAP_FAILED) return fail_step("mmap second");
|
||||
*first = 0xDEADBEEFU;
|
||||
if (*second != 0xDEADBEEFU) {
|
||||
printf("FAIL shm_open: observed=0x%08X\n", *second);
|
||||
return 1;
|
||||
}
|
||||
if (munmap(second, 4096) < 0) return fail_step("munmap second");
|
||||
if (munmap(first, 4096) < 0) return fail_step("munmap first");
|
||||
if (close(second_fd) < 0) return fail_step("close second");
|
||||
if (close(fd) < 0) return fail_step("close");
|
||||
if (shm_unlink(name) < 0) return fail_step("shm_unlink");
|
||||
|
||||
printf("PASS shm_open\n");
|
||||
return 0;
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,91 @@
|
||||
#define _GNU_SOURCE 1
|
||||
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef __redox__
|
||||
struct signalfd_siginfo {
|
||||
uint32_t ssi_signo;
|
||||
int32_t ssi_errno;
|
||||
int32_t ssi_code;
|
||||
uint32_t ssi_pid;
|
||||
uint32_t ssi_uid;
|
||||
int32_t ssi_fd;
|
||||
uint32_t ssi_tid;
|
||||
uint32_t ssi_band;
|
||||
uint32_t ssi_overrun;
|
||||
uint32_t ssi_trapno;
|
||||
int32_t ssi_status;
|
||||
int32_t ssi_int;
|
||||
uint64_t ssi_ptr;
|
||||
uint64_t ssi_utime;
|
||||
uint64_t ssi_stime;
|
||||
uint64_t ssi_addr;
|
||||
uint16_t ssi_addr_lsb, __pad2;
|
||||
int32_t ssi_syscall;
|
||||
uint64_t ssi_call_addr;
|
||||
uint32_t ssi_arch;
|
||||
unsigned char __pad[28];
|
||||
};
|
||||
int signalfd(int fd, const sigset_t *mask, size_t masksize);
|
||||
_Static_assert(sizeof(struct signalfd_siginfo) == 128, "unexpected signalfd_siginfo size");
|
||||
#else
|
||||
#include <sys/signalfd.h>
|
||||
#endif
|
||||
|
||||
static int fail_step(const char *step) {
|
||||
printf("FAIL signalfd: %s (errno=%d)\n", step, errno);
|
||||
return 1;
|
||||
}
|
||||
|
||||
#ifdef __redox__
|
||||
#define RB_SIGNALFD(fd, mask) signalfd((fd), (mask), sizeof(*(mask)))
|
||||
#else
|
||||
#define RB_SIGNALFD(fd, mask) signalfd((fd), (mask), 0)
|
||||
#endif
|
||||
|
||||
int main(void) {
|
||||
sigset_t mask;
|
||||
sigset_t oldmask;
|
||||
struct signalfd_siginfo info;
|
||||
int sfd;
|
||||
int status;
|
||||
pid_t child;
|
||||
|
||||
if (sigemptyset(&mask) < 0 || sigaddset(&mask, SIGUSR1) < 0) return fail_step("sigset setup");
|
||||
if (sigprocmask(SIG_BLOCK, &mask, &oldmask) < 0) return fail_step("sigprocmask block");
|
||||
sfd = RB_SIGNALFD(-1, &mask);
|
||||
if (sfd < 0) return fail_step("signalfd");
|
||||
|
||||
child = fork();
|
||||
if (child < 0) return fail_step("fork");
|
||||
if (child == 0) {
|
||||
_Exit(kill(getppid(), SIGUSR1) < 0);
|
||||
}
|
||||
|
||||
if (read(sfd, &info, sizeof(info)) != (ssize_t)sizeof(info)) return fail_step("read");
|
||||
if (waitpid(child, &status, 0) < 0) return fail_step("waitpid");
|
||||
if (close(sfd) < 0) return fail_step("close");
|
||||
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) return fail_step("sigprocmask restore");
|
||||
|
||||
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
|
||||
printf("FAIL signalfd: child status=%d\n", status);
|
||||
return 1;
|
||||
}
|
||||
if (info.ssi_signo != (uint32_t)SIGUSR1) {
|
||||
printf("FAIL signalfd: ssi_signo=%u\n", info.ssi_signo);
|
||||
return 1;
|
||||
}
|
||||
if (info.ssi_code != SI_USER) {
|
||||
printf("FAIL signalfd: ssi_code=%d\n", info.ssi_code);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("PASS signalfd\n");
|
||||
return 0;
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,48 @@
|
||||
#define _GNU_SOURCE 1
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef __redox__
|
||||
#define TFD_NONBLOCK 0x800
|
||||
int timerfd_create(clockid_t clockid, int flags);
|
||||
int timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value);
|
||||
#else
|
||||
#include <sys/timerfd.h>
|
||||
#endif
|
||||
|
||||
static int fail_step(const char *step) {
|
||||
printf("FAIL timerfd: %s (errno=%d)\n", step, errno);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
const struct timespec pause = {.tv_sec = 0, .tv_nsec = 20000000};
|
||||
struct itimerspec spec = {{0, 0}, {0, 100000000}};
|
||||
uint64_t expirations = 0;
|
||||
int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
|
||||
|
||||
if (tfd < 0) return fail_step("timerfd_create");
|
||||
if (timerfd_settime(tfd, 0, &spec, NULL) < 0) return fail_step("timerfd_settime");
|
||||
|
||||
for (int i = 0; i < 50; ++i) {
|
||||
ssize_t n = read(tfd, &expirations, sizeof(expirations));
|
||||
if (n == (ssize_t)sizeof(expirations)) {
|
||||
if (expirations >= 1) {
|
||||
if (close(tfd) < 0) return fail_step("close");
|
||||
printf("PASS timerfd\n");
|
||||
return 0;
|
||||
}
|
||||
printf("FAIL timerfd: expirations=%llu\n", (unsigned long long)expirations);
|
||||
return 1;
|
||||
}
|
||||
if (n >= 0 || (errno != EAGAIN && errno != EWOULDBLOCK)) return fail_step("read");
|
||||
nanosleep(&pause, NULL);
|
||||
}
|
||||
|
||||
printf("FAIL timerfd: timeout waiting for expiration\n");
|
||||
return 1;
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,33 @@
|
||||
#define _GNU_SOURCE 1
|
||||
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int fail_step(const char *step) {
|
||||
printf("FAIL waitid: %s (errno=%d)\n", step, errno);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
siginfo_t info = {0};
|
||||
pid_t child = fork();
|
||||
|
||||
if (child < 0) return fail_step("fork");
|
||||
if (child == 0) _Exit(42);
|
||||
if (waitid(P_PID, child, &info, WEXITED) < 0) return fail_step("waitid");
|
||||
if (info.si_code != CLD_EXITED) {
|
||||
printf("FAIL waitid: si_code=%d\n", info.si_code);
|
||||
return 1;
|
||||
}
|
||||
if (info.si_status != 42) {
|
||||
printf("FAIL waitid: si_status=%d\n", info.si_status);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("PASS waitid\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
// Red Bear Wayland Compositor — a real Wayland display server for the Qt6 greeter UI.
|
||||
// Replaces the KWin stub that previously created a placeholder socket with no actual compositing.
|
||||
// Red Bear Wayland Compositor — bounded Wayland compositor proof scaffold.
|
||||
// Replaces the KWin stub that previously created a placeholder socket.
|
||||
//
|
||||
// Architecture: creates a Wayland Unix socket, speaks the core Wayland wire protocol,
|
||||
// accepts client SHM buffers, and composites them directly onto the vesad framebuffer.
|
||||
// Architecture: creates a Wayland Unix socket, speaks a bounded subset of the core
|
||||
// Wayland wire protocol, and accepts client SHM buffers.
|
||||
//
|
||||
// NOTE: This is a bounded proof scaffold, not a real compositor runtime proof.
|
||||
// Known limitations: framebuffer compositing uses private heap memory (not real
|
||||
// vesad), SHM fd passing uses payload bytes (not Unix SCM_RIGHTS), wire encoding
|
||||
// uses NUL-terminated strings (not padded Wayland format).
|
||||
//
|
||||
// Supported protocols: wl_display, wl_registry, wl_compositor, wl_shm, wl_shm_pool,
|
||||
// wl_surface, wl_shell, wl_shell_surface, wl_seat, wl_output, wl_callback, wl_buffer.
|
||||
@@ -20,6 +25,8 @@ fn map_framebuffer(_phys: usize, size: usize) -> Vec<u8> {
|
||||
|
||||
const WL_DISPLAY_SYNC: u16 = 0;
|
||||
const WL_DISPLAY_GET_REGISTRY: u16 = 1;
|
||||
// Protocol constant: reserved for future implementation.
|
||||
#[allow(dead_code)]
|
||||
const WL_DISPLAY_ERROR: u16 = 0;
|
||||
const WL_DISPLAY_DELETE_ID: u16 = 2;
|
||||
|
||||
@@ -27,12 +34,16 @@ const WL_REGISTRY_BIND: u16 = 0;
|
||||
const WL_REGISTRY_GLOBAL: u16 = 0;
|
||||
|
||||
const WL_COMPOSITOR_CREATE_SURFACE: u16 = 0;
|
||||
// Protocol constant: reserved for future implementation.
|
||||
#[allow(dead_code)]
|
||||
const WL_COMPOSITOR_CREATE_REGION: u16 = 1;
|
||||
|
||||
const WL_SHM_CREATE_POOL: u16 = 0;
|
||||
const WL_SHM_FORMAT: u16 = 0;
|
||||
|
||||
const WL_SHM_POOL_CREATE_BUFFER: u16 = 0;
|
||||
// Protocol constant: reserved for future implementation.
|
||||
#[allow(dead_code)]
|
||||
const WL_SHM_POOL_RESIZE: u16 = 1;
|
||||
|
||||
const WL_BUFFER_RELEASE: u16 = 0;
|
||||
@@ -40,14 +51,22 @@ const WL_BUFFER_RELEASE: u16 = 0;
|
||||
const WL_SURFACE_ATTACH: u16 = 0;
|
||||
const WL_SURFACE_DAMAGE: u16 = 1;
|
||||
const WL_SURFACE_COMMIT: u16 = 5;
|
||||
// Protocol constant: reserved for future implementation.
|
||||
#[allow(dead_code)]
|
||||
const WL_SURFACE_ENTER: u16 = 0;
|
||||
// Protocol constant: reserved for future implementation.
|
||||
#[allow(dead_code)]
|
||||
const WL_SURFACE_LEAVE: u16 = 1;
|
||||
|
||||
const WL_SHELL_GET_SHELL_SURFACE: u16 = 0;
|
||||
|
||||
const WL_SHELL_SURFACE_PONG: u16 = 0;
|
||||
const WL_SHELL_SURFACE_SET_TOPLEVEL: u16 = 2;
|
||||
// Protocol constant: reserved for future implementation.
|
||||
#[allow(dead_code)]
|
||||
const WL_SHELL_SURFACE_PING: u16 = 0;
|
||||
// Protocol constant: reserved for future implementation.
|
||||
#[allow(dead_code)]
|
||||
const WL_SHELL_SURFACE_CONFIGURE: u16 = 1;
|
||||
|
||||
const XDG_WM_BASE_DESTROY: u16 = 0;
|
||||
@@ -65,9 +84,17 @@ const WL_SEAT_GET_POINTER: u16 = 0;
|
||||
const WL_SEAT_GET_KEYBOARD: u16 = 1;
|
||||
const WL_SEAT_CAPABILITIES: u16 = 0;
|
||||
|
||||
// Protocol constant: reserved for future implementation.
|
||||
#[allow(dead_code)]
|
||||
const WL_KEYBOARD_KEYMAP: u16 = 0;
|
||||
// Protocol constant: reserved for future implementation.
|
||||
#[allow(dead_code)]
|
||||
const WL_KEYBOARD_ENTER: u16 = 1;
|
||||
// Protocol constant: reserved for future implementation.
|
||||
#[allow(dead_code)]
|
||||
const WL_KEYBOARD_LEAVE: u16 = 2;
|
||||
// Protocol constant: reserved for future implementation.
|
||||
#[allow(dead_code)]
|
||||
const WL_KEYBOARD_KEY: u16 = 3;
|
||||
|
||||
const WL_OUTPUT_GEOMETRY: u16 = 0;
|
||||
@@ -78,6 +105,23 @@ const WL_CALLBACK_DONE: u16 = 0;
|
||||
const WL_SHM_FORMAT_XRGB8888: u32 = 1;
|
||||
const WL_SHM_FORMAT_ARGB8888: u32 = 0;
|
||||
|
||||
const OBJECT_TYPE_WL_DISPLAY: u32 = 1;
|
||||
const OBJECT_TYPE_WL_REGISTRY: u32 = 2;
|
||||
const OBJECT_TYPE_WL_COMPOSITOR: u32 = 3;
|
||||
const OBJECT_TYPE_WL_SHM: u32 = 4;
|
||||
const OBJECT_TYPE_WL_SHELL: u32 = 5;
|
||||
const OBJECT_TYPE_WL_SEAT: u32 = 6;
|
||||
const OBJECT_TYPE_WL_OUTPUT: u32 = 7;
|
||||
const OBJECT_TYPE_XDG_WM_BASE: u32 = 8;
|
||||
const OBJECT_TYPE_WL_SURFACE: u32 = 9;
|
||||
const OBJECT_TYPE_WL_BUFFER: u32 = 10;
|
||||
const OBJECT_TYPE_WL_SHELL_SURFACE: u32 = 11;
|
||||
const OBJECT_TYPE_XDG_SURFACE: u32 = 12;
|
||||
const OBJECT_TYPE_XDG_TOPLEVEL: u32 = 13;
|
||||
const OBJECT_TYPE_WL_SHM_POOL: u32 = 14;
|
||||
const OBJECT_TYPE_WL_POINTER: u32 = 15;
|
||||
const OBJECT_TYPE_WL_KEYBOARD: u32 = 16;
|
||||
|
||||
struct Global {
|
||||
name: u32,
|
||||
interface: String,
|
||||
@@ -86,7 +130,7 @@ struct Global {
|
||||
|
||||
struct ShmPool {
|
||||
data: &'static mut [u8],
|
||||
size: usize,
|
||||
_size: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -96,7 +140,7 @@ struct Buffer {
|
||||
width: u32,
|
||||
height: u32,
|
||||
stride: u32,
|
||||
format: u32,
|
||||
_format: u32,
|
||||
}
|
||||
|
||||
struct Surface {
|
||||
@@ -104,8 +148,8 @@ struct Surface {
|
||||
committed_buffer_id: Option<u32>,
|
||||
x: u32,
|
||||
y: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
}
|
||||
|
||||
struct ClientState {
|
||||
@@ -113,7 +157,7 @@ struct ClientState {
|
||||
surfaces: HashMap<u32, Surface>,
|
||||
buffers: HashMap<u32, (u32, Buffer)>,
|
||||
shm_pools: HashMap<u32, ShmPool>,
|
||||
next_id: u32,
|
||||
_next_id: u32,
|
||||
}
|
||||
|
||||
pub struct Compositor {
|
||||
@@ -181,16 +225,15 @@ impl Compositor {
|
||||
);
|
||||
for stream in self.listener.incoming() {
|
||||
match stream {
|
||||
Ok(mut stream) => {
|
||||
Ok(stream) => {
|
||||
let client_id = self.alloc_id();
|
||||
eprintln!("redbear-compositor: client {} connected", client_id);
|
||||
self.send_globals(client_id, &mut stream);
|
||||
self.clients.lock().unwrap().insert(client_id, ClientState {
|
||||
objects: HashMap::new(),
|
||||
surfaces: HashMap::new(),
|
||||
buffers: HashMap::new(),
|
||||
shm_pools: HashMap::new(),
|
||||
next_id: 1,
|
||||
_next_id: 1,
|
||||
});
|
||||
self.handle_client(client_id, stream);
|
||||
}
|
||||
@@ -200,15 +243,14 @@ impl Compositor {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_globals(&self, _client_id: u32, stream: &mut UnixStream) {
|
||||
// Advertise each global interface to the client
|
||||
let display_id = 1u32; // wl_display id
|
||||
fn send_globals(&self, stream: &mut UnixStream, registry_id: u32) {
|
||||
// Advertise each global interface on the wl_registry object after get_registry.
|
||||
for global in &self.globals {
|
||||
let name = global.name;
|
||||
let iface = global.interface.as_bytes();
|
||||
let version = global.version;
|
||||
let mut msg = Vec::with_capacity(16 + iface.len() + 1);
|
||||
msg.extend_from_slice(&display_id.to_ne_bytes());
|
||||
msg.extend_from_slice(®istry_id.to_ne_bytes());
|
||||
let size = 8 + 4 + 4 + iface.len() as u16 + 1;
|
||||
let header = (size as u32) << 16 | WL_REGISTRY_GLOBAL as u32;
|
||||
msg.extend_from_slice(&header.to_ne_bytes());
|
||||
@@ -256,6 +298,7 @@ impl Compositor {
|
||||
let mut offset = 0;
|
||||
while offset + 8 <= data.len() {
|
||||
let object_id = u32::from_ne_bytes([data[offset], data[offset+1], data[offset+2], data[offset+3]]);
|
||||
// Wayland wire format: [object_id:u32][size:u16][opcode:u16]
|
||||
let size_opcode = u32::from_ne_bytes([data[offset+4], data[offset+5], data[offset+6], data[offset+7]]);
|
||||
let msg_size = ((size_opcode >> 16) & 0xFFFF) as usize;
|
||||
let opcode = (size_opcode & 0xFFFF) as u16;
|
||||
@@ -265,220 +308,329 @@ impl Compositor {
|
||||
}
|
||||
|
||||
let payload = &data[offset+8..offset+msg_size];
|
||||
let object_type = if object_id == 1 {
|
||||
OBJECT_TYPE_WL_DISPLAY
|
||||
} else {
|
||||
self.clients
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&client_id)
|
||||
.and_then(|client| client.objects.get(&object_id).copied())
|
||||
.unwrap_or(0)
|
||||
};
|
||||
|
||||
match opcode {
|
||||
WL_DISPLAY_SYNC => {
|
||||
let callback_id = if payload.len() >= 4 {
|
||||
u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]])
|
||||
} else { self.alloc_id() };
|
||||
self.send_callback_done(stream, callback_id, 0);
|
||||
}
|
||||
WL_DISPLAY_DELETE_ID => {
|
||||
if payload.len() >= 4 {
|
||||
let obj_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
|
||||
let mut clients = self.clients.lock().unwrap();
|
||||
if let Some(client) = clients.get_mut(&client_id) {
|
||||
client.objects.remove(&obj_id);
|
||||
client.surfaces.remove(&obj_id);
|
||||
client.buffers.remove(&obj_id);
|
||||
client.shm_pools.remove(&obj_id);
|
||||
match object_type {
|
||||
OBJECT_TYPE_WL_DISPLAY => match opcode {
|
||||
WL_DISPLAY_SYNC => {
|
||||
let callback_id = if payload.len() >= 4 {
|
||||
u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]])
|
||||
} else { self.alloc_id() };
|
||||
self.send_callback_done(stream, callback_id, 0);
|
||||
}
|
||||
WL_DISPLAY_DELETE_ID => {
|
||||
if payload.len() >= 4 {
|
||||
let obj_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
|
||||
let mut clients = self.clients.lock().unwrap();
|
||||
if let Some(client) = clients.get_mut(&client_id) {
|
||||
client.objects.remove(&obj_id);
|
||||
client.surfaces.remove(&obj_id);
|
||||
client.buffers.remove(&obj_id);
|
||||
client.shm_pools.remove(&obj_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
WL_DISPLAY_GET_REGISTRY => {
|
||||
if payload.len() >= 4 {
|
||||
let registry_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
|
||||
let mut clients = self.clients.lock().unwrap();
|
||||
if let Some(client) = clients.get_mut(&client_id) {
|
||||
client.objects.insert(registry_id, 2); // wl_registry
|
||||
WL_DISPLAY_GET_REGISTRY => {
|
||||
if payload.len() >= 4 {
|
||||
let registry_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
|
||||
let mut clients = self.clients.lock().unwrap();
|
||||
let mut send_globals = false;
|
||||
if let Some(client) = clients.get_mut(&client_id) {
|
||||
client.objects.insert(registry_id, OBJECT_TYPE_WL_REGISTRY);
|
||||
send_globals = true;
|
||||
}
|
||||
drop(clients);
|
||||
if send_globals {
|
||||
self.send_globals(stream, registry_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
WL_REGISTRY_BIND => {
|
||||
if payload.len() >= 12 {
|
||||
let name = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
|
||||
let iface_bytes = &payload[4..];
|
||||
let null_pos = iface_bytes.iter().position(|&b| b == 0).unwrap_or(iface_bytes.len());
|
||||
let iface = std::str::from_utf8(&iface_bytes[..null_pos]).unwrap_or("");
|
||||
let version_offset = 4 + null_pos + 1;
|
||||
let new_id = if payload.len() >= version_offset + 4 {
|
||||
u32::from_ne_bytes([payload[version_offset], payload[version_offset+1], payload[version_offset+2], payload[version_offset+3]])
|
||||
} else { continue; };
|
||||
_ => {
|
||||
eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id);
|
||||
}
|
||||
},
|
||||
OBJECT_TYPE_WL_REGISTRY => match opcode {
|
||||
WL_REGISTRY_BIND => {
|
||||
if payload.len() >= 12 {
|
||||
let _name = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
|
||||
let iface_bytes = &payload[4..];
|
||||
let null_pos = iface_bytes.iter().position(|&b| b == 0).unwrap_or(iface_bytes.len());
|
||||
let iface = std::str::from_utf8(&iface_bytes[..null_pos]).unwrap_or("");
|
||||
let version_offset = 4 + null_pos + 1;
|
||||
let new_id = if payload.len() >= version_offset + 4 {
|
||||
u32::from_ne_bytes([payload[version_offset], payload[version_offset+1], payload[version_offset+2], payload[version_offset+3]])
|
||||
} else { continue; };
|
||||
|
||||
let mut clients = self.clients.lock().unwrap();
|
||||
if let Some(client) = clients.get_mut(&client_id) {
|
||||
let type_id = match iface {
|
||||
"wl_compositor" => 3,
|
||||
"wl_shm" => 4,
|
||||
"wl_shell" => 5,
|
||||
"wl_seat" => 6,
|
||||
"wl_output" => 7,
|
||||
"xdg_wm_base" => 8,
|
||||
_ => 0,
|
||||
};
|
||||
client.objects.insert(new_id, type_id);
|
||||
if iface == "wl_shm" {
|
||||
self.send_shm_format(stream, new_id, WL_SHM_FORMAT_ARGB8888);
|
||||
self.send_shm_format(stream, new_id, WL_SHM_FORMAT_XRGB8888);
|
||||
}
|
||||
if iface == "wl_output" {
|
||||
self.send_output_info(stream, new_id);
|
||||
}
|
||||
if iface == "wl_seat" {
|
||||
self.send_seat_capabilities(stream, new_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
WL_COMPOSITOR_CREATE_SURFACE => {
|
||||
if payload.len() >= 4 {
|
||||
let surface_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
|
||||
let mut clients = self.clients.lock().unwrap();
|
||||
if let Some(client) = clients.get_mut(&client_id) {
|
||||
client.objects.insert(surface_id, 8);
|
||||
client.surfaces.insert(surface_id, Surface {
|
||||
buffer: None,
|
||||
committed_buffer_id: None,
|
||||
x: 0, y: 0,
|
||||
width: self.fb_width,
|
||||
height: self.fb_height,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
WL_SHM_CREATE_POOL => {
|
||||
if payload.len() >= 8 {
|
||||
let pool_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
|
||||
let fd_val = i32::from_ne_bytes([payload[4], payload[5], payload[6], payload[7]]);
|
||||
let size = if payload.len() >= 12 {
|
||||
u32::from_ne_bytes([payload[8], payload[9], payload[10], payload[11]]) as usize
|
||||
} else { 0 };
|
||||
if fd_val >= 0 && size > 0 {
|
||||
use std::os::fd::FromRawFd;
|
||||
use std::io::Seek;
|
||||
let mut file = unsafe { std::fs::File::from_raw_fd(fd_val) };
|
||||
let mut data = vec![0u8; size];
|
||||
if file.seek(std::io::SeekFrom::Start(0)).is_ok()
|
||||
&& file.read_exact(&mut data).is_ok()
|
||||
{
|
||||
let boxed = data.into_boxed_slice();
|
||||
let leaked = Box::leak(boxed);
|
||||
let mut clients = self.clients.lock().unwrap();
|
||||
if let Some(client) = clients.get_mut(&client_id) {
|
||||
client.shm_pools.insert(pool_id, ShmPool { data: leaked, size });
|
||||
let mut clients = self.clients.lock().unwrap();
|
||||
if let Some(client) = clients.get_mut(&client_id) {
|
||||
let type_id = match iface {
|
||||
"wl_compositor" => OBJECT_TYPE_WL_COMPOSITOR,
|
||||
"wl_shm" => OBJECT_TYPE_WL_SHM,
|
||||
"wl_shell" => OBJECT_TYPE_WL_SHELL,
|
||||
"wl_seat" => OBJECT_TYPE_WL_SEAT,
|
||||
"wl_output" => OBJECT_TYPE_WL_OUTPUT,
|
||||
"xdg_wm_base" => OBJECT_TYPE_XDG_WM_BASE,
|
||||
_ => 0,
|
||||
};
|
||||
client.objects.insert(new_id, type_id);
|
||||
if iface == "wl_shm" {
|
||||
self.send_shm_format(stream, new_id, WL_SHM_FORMAT_ARGB8888);
|
||||
self.send_shm_format(stream, new_id, WL_SHM_FORMAT_XRGB8888);
|
||||
}
|
||||
if iface == "wl_output" {
|
||||
self.send_output_info(stream, new_id);
|
||||
}
|
||||
if iface == "wl_seat" {
|
||||
self.send_seat_capabilities(stream, new_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
WL_SHM_POOL_CREATE_BUFFER => {
|
||||
if payload.len() >= 20 {
|
||||
let buffer_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
|
||||
let offset = u32::from_ne_bytes([payload[4], payload[5], payload[6], payload[7]]);
|
||||
let width = u32::from_ne_bytes([payload[8], payload[9], payload[10], payload[11]]);
|
||||
let height = u32::from_ne_bytes([payload[12], payload[13], payload[14], payload[15]]);
|
||||
let stride = u32::from_ne_bytes([payload[16], payload[17], payload[18], payload[19]]);
|
||||
let format = if payload.len() >= 24 {
|
||||
u32::from_ne_bytes([payload[20], payload[21], payload[22], payload[23]])
|
||||
} else { WL_SHM_FORMAT_ARGB8888 };
|
||||
|
||||
let mut clients = self.clients.lock().unwrap();
|
||||
if let Some(client) = clients.get_mut(&client_id) {
|
||||
client.objects.insert(buffer_id, 9); // wl_buffer
|
||||
client.buffers.insert(buffer_id, (object_id, Buffer {
|
||||
pool_id: object_id,
|
||||
offset, width, height, stride, format,
|
||||
}));
|
||||
}
|
||||
_ => {
|
||||
eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id);
|
||||
}
|
||||
}
|
||||
WL_SURFACE_ATTACH => {
|
||||
if payload.len() >= 12 {
|
||||
let buffer_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
|
||||
let _x = i32::from_ne_bytes([payload[4], payload[5], payload[6], payload[7]]);
|
||||
let _y = i32::from_ne_bytes([payload[8], payload[9], payload[10], payload[11]]);
|
||||
|
||||
let mut clients = self.clients.lock().unwrap();
|
||||
if let Some(client) = clients.get_mut(&client_id) {
|
||||
if let Some((pool_id, buffer)) = client.buffers.get(&buffer_id).cloned() {
|
||||
if let Some(surface) = client.surfaces.get_mut(&object_id) {
|
||||
surface.buffer = Some(Buffer {
|
||||
pool_id,
|
||||
..buffer
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
WL_SURFACE_COMMIT => {
|
||||
let (release_id, buffer_data) = {
|
||||
let mut clients = self.clients.lock().unwrap();
|
||||
if let Some(client) = clients.get_mut(&client_id) {
|
||||
if let Some(surface) = client.surfaces.get_mut(&object_id) {
|
||||
let old_buffer = surface.committed_buffer_id.take();
|
||||
surface.committed_buffer_id = surface.buffer.as_ref().map(|b| {
|
||||
client.buffers.iter()
|
||||
.find(|(_, (_, buf))| buf.offset == b.offset && buf.width == b.width)
|
||||
.map(|(id, _)| *id)
|
||||
.unwrap_or(0)
|
||||
},
|
||||
OBJECT_TYPE_WL_COMPOSITOR => match opcode {
|
||||
WL_COMPOSITOR_CREATE_SURFACE => {
|
||||
if payload.len() >= 4 {
|
||||
let surface_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
|
||||
let mut clients = self.clients.lock().unwrap();
|
||||
if let Some(client) = clients.get_mut(&client_id) {
|
||||
client.objects.insert(surface_id, OBJECT_TYPE_WL_SURFACE);
|
||||
client.surfaces.insert(surface_id, Surface {
|
||||
buffer: None,
|
||||
committed_buffer_id: None,
|
||||
x: 0, y: 0,
|
||||
_width: self.fb_width,
|
||||
_height: self.fb_height,
|
||||
});
|
||||
|
||||
if let Some(ref buffer) = surface.buffer {
|
||||
if let Some(pool) = client.shm_pools.get(&buffer.pool_id) {
|
||||
self.composite_buffer(pool, buffer, surface);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id);
|
||||
}
|
||||
},
|
||||
OBJECT_TYPE_WL_SHM => match opcode {
|
||||
WL_SHM_CREATE_POOL => {
|
||||
if payload.len() >= 8 {
|
||||
let pool_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
|
||||
let fd_val = i32::from_ne_bytes([payload[4], payload[5], payload[6], payload[7]]);
|
||||
let size = if payload.len() >= 12 {
|
||||
u32::from_ne_bytes([payload[8], payload[9], payload[10], payload[11]]) as usize
|
||||
} else { 0 };
|
||||
if fd_val >= 0 && size > 0 {
|
||||
use std::io::Seek;
|
||||
use std::os::fd::FromRawFd;
|
||||
|
||||
let mut file = unsafe { std::fs::File::from_raw_fd(fd_val) };
|
||||
let mut data = vec![0u8; size];
|
||||
if file.seek(std::io::SeekFrom::Start(0)).is_ok()
|
||||
&& file.read_exact(&mut data).is_ok()
|
||||
{
|
||||
let boxed = data.into_boxed_slice();
|
||||
let leaked = Box::leak(boxed);
|
||||
let mut clients = self.clients.lock().unwrap();
|
||||
if let Some(client) = clients.get_mut(&client_id) {
|
||||
client.objects.insert(pool_id, OBJECT_TYPE_WL_SHM_POOL);
|
||||
client.shm_pools.insert(pool_id, ShmPool { data: leaked, _size: size });
|
||||
}
|
||||
}
|
||||
(old_buffer, surface.buffer.is_some())
|
||||
} else { (None, false) }
|
||||
} else { (None, false) }
|
||||
};
|
||||
|
||||
if let Some(buf_id) = release_id {
|
||||
if buf_id != 0 {
|
||||
self.send_buffer_release(stream, buf_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
WL_SHELL_GET_SHELL_SURFACE | WL_SEAT_GET_KEYBOARD | WL_SEAT_GET_POINTER => {
|
||||
// Ack new object creation — just register the id
|
||||
if payload.len() >= 4 {
|
||||
let new_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
|
||||
_ => {
|
||||
eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id);
|
||||
}
|
||||
},
|
||||
OBJECT_TYPE_WL_SHM_POOL => match opcode {
|
||||
WL_SHM_POOL_CREATE_BUFFER => {
|
||||
if payload.len() >= 20 {
|
||||
let buffer_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
|
||||
let offset = u32::from_ne_bytes([payload[4], payload[5], payload[6], payload[7]]);
|
||||
let width = u32::from_ne_bytes([payload[8], payload[9], payload[10], payload[11]]);
|
||||
let height = u32::from_ne_bytes([payload[12], payload[13], payload[14], payload[15]]);
|
||||
let stride = u32::from_ne_bytes([payload[16], payload[17], payload[18], payload[19]]);
|
||||
let format = if payload.len() >= 24 {
|
||||
u32::from_ne_bytes([payload[20], payload[21], payload[22], payload[23]])
|
||||
} else { WL_SHM_FORMAT_ARGB8888 };
|
||||
|
||||
let mut clients = self.clients.lock().unwrap();
|
||||
if let Some(client) = clients.get_mut(&client_id) {
|
||||
client.objects.insert(buffer_id, OBJECT_TYPE_WL_BUFFER);
|
||||
client.buffers.insert(buffer_id, (object_id, Buffer {
|
||||
pool_id: object_id,
|
||||
offset,
|
||||
width,
|
||||
height,
|
||||
stride,
|
||||
_format: format,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id);
|
||||
}
|
||||
},
|
||||
OBJECT_TYPE_WL_SURFACE => match opcode {
|
||||
WL_SURFACE_ATTACH => {
|
||||
if payload.len() >= 12 {
|
||||
let buffer_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
|
||||
let _x = i32::from_ne_bytes([payload[4], payload[5], payload[6], payload[7]]);
|
||||
let _y = i32::from_ne_bytes([payload[8], payload[9], payload[10], payload[11]]);
|
||||
|
||||
let mut clients = self.clients.lock().unwrap();
|
||||
if let Some(client) = clients.get_mut(&client_id) {
|
||||
if let Some((pool_id, buffer)) = client.buffers.get(&buffer_id).cloned() {
|
||||
if let Some(surface) = client.surfaces.get_mut(&object_id) {
|
||||
surface.buffer = Some(Buffer {
|
||||
pool_id,
|
||||
..buffer
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
WL_SURFACE_COMMIT => {
|
||||
let release_id = {
|
||||
let mut clients = self.clients.lock().unwrap();
|
||||
if let Some(client) = clients.get_mut(&client_id) {
|
||||
if let Some(surface) = client.surfaces.get_mut(&object_id) {
|
||||
let old_buffer = surface.committed_buffer_id.take();
|
||||
surface.committed_buffer_id = surface.buffer.as_ref().map(|b| {
|
||||
client.buffers.iter()
|
||||
.find(|(_, (_, buf))| buf.offset == b.offset && buf.width == b.width)
|
||||
.map(|(id, _)| *id)
|
||||
.unwrap_or(0)
|
||||
});
|
||||
|
||||
if let Some(ref buffer) = surface.buffer {
|
||||
if let Some(pool) = client.shm_pools.get(&buffer.pool_id) {
|
||||
self.composite_buffer(pool, buffer, surface);
|
||||
}
|
||||
}
|
||||
old_buffer
|
||||
} else { None }
|
||||
} else { None }
|
||||
};
|
||||
|
||||
if let Some(buf_id) = release_id {
|
||||
if buf_id != 0 {
|
||||
self.send_buffer_release(stream, buf_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
WL_SURFACE_DAMAGE => {
|
||||
// No-op — we don't need damage tracking for a single-client greeter.
|
||||
}
|
||||
_ => {
|
||||
eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id);
|
||||
}
|
||||
},
|
||||
OBJECT_TYPE_WL_SHELL => match opcode {
|
||||
WL_SHELL_GET_SHELL_SURFACE => {
|
||||
if payload.len() >= 4 {
|
||||
let new_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
|
||||
let mut clients = self.clients.lock().unwrap();
|
||||
if let Some(client) = clients.get_mut(&client_id) {
|
||||
client.objects.insert(new_id, OBJECT_TYPE_WL_SHELL_SURFACE);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id);
|
||||
}
|
||||
},
|
||||
OBJECT_TYPE_WL_SHELL_SURFACE => match opcode {
|
||||
WL_SHELL_SURFACE_SET_TOPLEVEL | WL_SHELL_SURFACE_PONG => {
|
||||
// No-op — we don't need window management for a single-client greeter.
|
||||
}
|
||||
_ => {
|
||||
eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id);
|
||||
}
|
||||
},
|
||||
OBJECT_TYPE_WL_SEAT => match opcode {
|
||||
WL_SEAT_GET_POINTER | WL_SEAT_GET_KEYBOARD => {
|
||||
if payload.len() >= 4 {
|
||||
let new_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
|
||||
let object_type = match opcode {
|
||||
WL_SEAT_GET_POINTER => OBJECT_TYPE_WL_POINTER,
|
||||
WL_SEAT_GET_KEYBOARD => OBJECT_TYPE_WL_KEYBOARD,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let mut clients = self.clients.lock().unwrap();
|
||||
if let Some(client) = clients.get_mut(&client_id) {
|
||||
client.objects.insert(new_id, object_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id);
|
||||
}
|
||||
},
|
||||
OBJECT_TYPE_XDG_WM_BASE => match opcode {
|
||||
XDG_WM_BASE_GET_XDG_SURFACE => {
|
||||
if payload.len() >= 4 {
|
||||
let new_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
|
||||
let mut clients = self.clients.lock().unwrap();
|
||||
if let Some(client) = clients.get_mut(&client_id) {
|
||||
client.objects.insert(new_id, OBJECT_TYPE_XDG_SURFACE);
|
||||
}
|
||||
}
|
||||
}
|
||||
XDG_WM_BASE_DESTROY | XDG_WM_BASE_PONG => {
|
||||
// No-op — the greeter keeps the shell global alive for the client lifetime.
|
||||
}
|
||||
_ => {
|
||||
eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id);
|
||||
}
|
||||
},
|
||||
OBJECT_TYPE_XDG_SURFACE => match opcode {
|
||||
XDG_SURFACE_DESTROY => {
|
||||
let mut clients = self.clients.lock().unwrap();
|
||||
if let Some(client) = clients.get_mut(&client_id) {
|
||||
client.objects.insert(new_id, 10);
|
||||
client.objects.remove(&object_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
WL_SHELL_SURFACE_SET_TOPLEVEL | WL_SHELL_SURFACE_PONG | WL_SURFACE_DAMAGE => {
|
||||
// No-op — we don't need window management for a single-client greeter
|
||||
}
|
||||
XDG_WM_BASE_GET_XDG_SURFACE | XDG_WM_BASE_DESTROY | XDG_WM_BASE_PONG => {
|
||||
if payload.len() >= 4 {
|
||||
let new_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
|
||||
let mut clients = self.clients.lock().unwrap();
|
||||
if let Some(client) = clients.get_mut(&client_id) {
|
||||
client.objects.insert(new_id, 11);
|
||||
XDG_SURFACE_GET_TOPLEVEL => {
|
||||
if payload.len() >= 4 {
|
||||
let toplevel_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
|
||||
let mut clients = self.clients.lock().unwrap();
|
||||
if let Some(client) = clients.get_mut(&client_id) {
|
||||
client.objects.insert(toplevel_id, OBJECT_TYPE_XDG_TOPLEVEL);
|
||||
}
|
||||
drop(clients);
|
||||
self.send_xdg_surface_configure(stream, object_id);
|
||||
self.send_xdg_toplevel_configure(stream, toplevel_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
XDG_SURFACE_GET_TOPLEVEL | XDG_SURFACE_DESTROY => {
|
||||
if payload.len() >= 4 {
|
||||
let toplevel_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
|
||||
let mut clients = self.clients.lock().unwrap();
|
||||
if let Some(client) = clients.get_mut(&client_id) {
|
||||
client.objects.insert(toplevel_id, 12);
|
||||
}
|
||||
drop(clients);
|
||||
self.send_xdg_surface_configure(stream, object_id);
|
||||
self.send_xdg_toplevel_configure(stream, toplevel_id);
|
||||
XDG_SURFACE_ACK_CONFIGURE => {
|
||||
// Client acknowledged — ready for first commit.
|
||||
}
|
||||
}
|
||||
XDG_SURFACE_ACK_CONFIGURE => {
|
||||
// Client acknowledged — ready for first commit
|
||||
_ => {
|
||||
eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id);
|
||||
}
|
||||
},
|
||||
OBJECT_TYPE_WL_OUTPUT
|
||||
| OBJECT_TYPE_WL_BUFFER
|
||||
| OBJECT_TYPE_XDG_TOPLEVEL
|
||||
| OBJECT_TYPE_WL_POINTER
|
||||
| OBJECT_TYPE_WL_KEYBOARD => {
|
||||
eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id);
|
||||
}
|
||||
_ => {
|
||||
eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id);
|
||||
eprintln!("redbear-compositor: unhandled object {} opcode {}", object_id, opcode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -621,8 +773,6 @@ fn main() {
|
||||
let fb_phys = usize::from_str_radix(fb_phys_str.trim_start_matches("0x"), 16)
|
||||
.unwrap_or(0x80000000);
|
||||
|
||||
let fb_size = (fb_height as usize) * (fb_stride as usize);
|
||||
|
||||
eprintln!(
|
||||
"redbear-compositor: fb {}x{} stride {} phys 0x{:X}",
|
||||
fb_width, fb_height, fb_stride, fb_phys
|
||||
|
||||
@@ -109,13 +109,13 @@ fn test_compositor_globals() {
|
||||
let mut client = WaylandClient::connect(socket).expect("failed to connect");
|
||||
|
||||
// Get registry
|
||||
let registry = client.get_registry().expect("get_registry failed");
|
||||
let _registry = client.get_registry().expect("get_registry failed");
|
||||
|
||||
// Read global events
|
||||
let mut globals = Vec::new();
|
||||
for _ in 0..6 {
|
||||
match client.read_message() {
|
||||
Ok((obj_id, opcode, payload)) => {
|
||||
Ok((_obj_id, opcode, payload)) => {
|
||||
assert_eq!(opcode, 0); // wl_registry.global
|
||||
let name = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
|
||||
let iface_end = payload[4..].iter().position(|&b| b == 0).unwrap_or(0);
|
||||
|
||||
Reference in New Issue
Block a user