Advance Red Bear runtime services and tools

This commit is contained in:
2026-04-20 18:37:35 +01:00
parent 2b3b592dab
commit f3e6b09811
24 changed files with 1362 additions and 357 deletions
@@ -8,4 +8,5 @@ name = "redbear-info"
path = "src/main.rs"
[dependencies]
redox-driver-sys = { path = "../../../../recipes/drivers/redox-driver-sys/source" }
toml = "0.8"
@@ -5,6 +5,8 @@ use std::path::PathBuf;
use std::process;
use std::time::{SystemTime, UNIX_EPOCH};
use redox_driver_sys::pci::{parse_device_info_from_config_space, InterruptSupport};
use redox_driver_sys::quirks::{lookup_pci_quirks, PciQuirkFlags};
use toml::Value;
#[cfg(test)]
@@ -92,12 +94,29 @@ struct NetworkReport {
struct HardwareReport {
pci_devices: usize,
pci_irq_none: usize,
pci_irq_legacy: usize,
pci_irq_msi: usize,
pci_irq_msix: usize,
pci_irq_forced_legacy: usize,
pci_irq_msix_disabled_by_quirk: usize,
pci_irq_msi_disabled_by_quirk: usize,
runtime_irq_reports: Vec<IrqRuntimeReport>,
usb_controllers: usize,
drm_cards: usize,
acpi_power_surface_present: bool,
rtl8125_present: bool,
virtio_net_present: bool,
}
struct IrqRuntimeReport {
driver: String,
pid: u32,
device: String,
mode: String,
reason: String,
}
struct QuirkFile {
name: String,
pci_quirks: Vec<QuirkEntry>,
@@ -333,6 +352,16 @@ const INTEGRATIONS: &[IntegrationCheck] = &[
note: "Functional when the firmware scheme is enumerable.",
functional_probe: Some(probe_firmware_scheme),
},
IntegrationCheck {
name: "redbear-upower",
category: "Power",
description: "Bounded UPower-compatible power reporting daemon",
artifact_path: Some("/usr/bin/redbear-upower"),
control_path: Some("/scheme/acpi/power"),
test_hint: "redbear-phase5-network-check",
note: "Binary presence proves the daemon is installed; a live /scheme/acpi/power surface proves bounded ACPI-backed power reporting is actually available.",
functional_probe: Some(probe_acpi_power_surface),
},
IntegrationCheck {
name: "iommu",
category: "System",
@@ -855,6 +884,14 @@ fn collect_hardware(runtime: &Runtime, network: &NetworkReport) -> HardwareRepor
.iter()
.filter(|entry| entry.contains("--") && entry.contains('.'))
.count();
let mut pci_irq_none = 0;
let mut pci_irq_legacy = 0;
let mut pci_irq_msi = 0;
let mut pci_irq_msix = 0;
let mut pci_irq_forced_legacy = 0;
let mut pci_irq_msix_disabled_by_quirk = 0;
let mut pci_irq_msi_disabled_by_quirk = 0;
let mut rtl8125_present_from_pci = false;
let usb_controllers = runtime
.read_dir_names("/scheme")
@@ -869,19 +906,42 @@ fn collect_hardware(runtime: &Runtime, network: &NetworkReport) -> HardwareRepor
.into_iter()
.filter(|name| name.starts_with("card"))
.count();
let runtime_irq_reports = collect_irq_runtime_reports(runtime);
let acpi_power_surface_present = runtime.exists("/scheme/acpi/power");
let rtl8125_present = pci_entries.into_iter().any(|entry| {
for entry in &pci_entries {
let config_path = format!("/scheme/pci/{entry}/config");
let Some(bytes) = read_prefix_bytes(runtime, &config_path, 4) else {
return false;
let Some(bytes) = read_prefix_bytes(runtime, &config_path, 64) else {
continue;
};
if bytes.len() < 4 {
return false;
if bytes.len() < 64 {
continue;
}
let vendor = u16::from_le_bytes([bytes[0], bytes[1]]);
let device = u16::from_le_bytes([bytes[2], bytes[3]]);
vendor == RTL8125_VENDOR_ID && device == RTL8125_DEVICE_ID
}) || network
if let Some(location) = parse_scheme_pci_location(entry) {
if let Some(info) = parse_device_info_from_config_space(location, &bytes) {
match info.interrupt_support() {
InterruptSupport::None => pci_irq_none += 1,
InterruptSupport::LegacyOnly => pci_irq_legacy += 1,
InterruptSupport::Msi => pci_irq_msi += 1,
InterruptSupport::MsiX => pci_irq_msix += 1,
}
let quirk_flags = lookup_pci_quirks(&info);
if quirk_flags.contains(PciQuirkFlags::FORCE_LEGACY_IRQ) {
pci_irq_forced_legacy += 1;
}
if quirk_flags.contains(PciQuirkFlags::NO_MSIX) {
pci_irq_msix_disabled_by_quirk += 1;
}
if quirk_flags.contains(PciQuirkFlags::NO_MSI) {
pci_irq_msi_disabled_by_quirk += 1;
}
rtl8125_present_from_pci |=
info.vendor_id == RTL8125_VENDOR_ID && info.device_id == RTL8125_DEVICE_ID;
}
}
}
let rtl8125_present = rtl8125_present_from_pci || network
.network_schemes
.iter()
.any(|name| name.contains("rtl8125"));
@@ -909,13 +969,99 @@ fn collect_hardware(runtime: &Runtime, network: &NetworkReport) -> HardwareRepor
HardwareReport {
pci_devices,
pci_irq_none,
pci_irq_legacy,
pci_irq_msi,
pci_irq_msix,
pci_irq_forced_legacy,
pci_irq_msix_disabled_by_quirk,
pci_irq_msi_disabled_by_quirk,
runtime_irq_reports,
usb_controllers,
drm_cards,
acpi_power_surface_present,
rtl8125_present,
virtio_net_present,
}
}
fn parse_scheme_pci_location(entry: &str) -> Option<redox_driver_sys::pci::PciLocation> {
let (segment, rest) = entry.split_once("--")?;
let (bus, rest) = rest.split_once("--")?;
let (device, function) = rest.split_once('.')?;
Some(redox_driver_sys::pci::PciLocation {
segment: u16::from_str_radix(segment, 16).ok()?,
bus: u8::from_str_radix(bus, 16).ok()?,
device: u8::from_str_radix(device, 16).ok()?,
function: function.parse().ok()?,
})
}
fn collect_irq_runtime_reports(runtime: &Runtime) -> Vec<IrqRuntimeReport> {
let mut reports = Vec::new();
let mut seen = std::collections::BTreeSet::new();
for dir in [
"/tmp/redbear-irq-report",
"/tmp/run/redbear-irq-report",
"/run/redbear-irq-report",
"/var/run/redbear-irq-report",
"/scheme/initfs/tmp/redbear-irq-report",
"/scheme/initfs/tmp/run/redbear-irq-report",
"/scheme/initfs/run/redbear-irq-report",
"/scheme/initfs/var/run/redbear-irq-report",
] {
let entries = runtime.read_dir_names(dir).unwrap_or_default();
for name in entries.into_iter().filter(|name| name.ends_with(".env")) {
let path = format!("{dir}/{name}");
if !seen.insert(path.clone()) {
continue;
}
let Some(content) = runtime.read_to_string(&path) else {
continue;
};
let mut driver = None;
let mut pid = None;
let mut device = None;
let mut mode = None;
let mut reason = None;
for line in content.lines() {
let Some((key, value)) = line.split_once('=') else {
continue;
};
match key.trim() {
"driver" => driver = Some(value.trim().to_string()),
"pid" => pid = value.trim().parse::<u32>().ok(),
"device" => device = Some(value.trim().to_string()),
"mode" => mode = Some(value.trim().to_string()),
"reason" => reason = Some(value.trim().to_string()),
_ => {}
}
}
if let (Some(driver), Some(pid), Some(device), Some(mode), Some(reason)) =
(driver, pid, device, mode, reason)
{
if !runtime.exists(&format!("/proc/{pid}")) {
continue;
}
reports.push(IrqRuntimeReport {
driver,
pid,
device,
mode,
reason,
});
}
}
}
reports.sort_by(|left, right| left.driver.cmp(&right.driver).then(left.device.cmp(&right.device)));
reports
}
fn collect_quirks(runtime: &Runtime) -> QuirksReport {
let mut files_loaded = Vec::new();
let mut load_errors = Vec::new();
@@ -1359,8 +1505,38 @@ fn print_table(report: &Report<'_>, verbose: bool) {
print_section_header("Hardware");
println!(" PCI devices: {}", report.hardware.pci_devices);
println!(
" PCI IRQ support: none={} legacy={} msi={} msix={}",
report.hardware.pci_irq_none,
report.hardware.pci_irq_legacy,
report.hardware.pci_irq_msi,
report.hardware.pci_irq_msix,
);
println!(
" PCI IRQ quirk pressure: force_legacy={} no_msix={} no_msi={}",
report.hardware.pci_irq_forced_legacy,
report.hardware.pci_irq_msix_disabled_by_quirk,
report.hardware.pci_irq_msi_disabled_by_quirk,
);
if !report.hardware.runtime_irq_reports.is_empty() {
println!(" PCI IRQ runtime modes:");
for item in &report.hardware.runtime_irq_reports {
println!(
" {} pid={} {} mode={} reason={}",
item.driver, item.pid, item.device, item.mode, item.reason
);
}
}
println!(" USB controllers: {}", report.hardware.usb_controllers);
println!(" DRM cards: {}", report.hardware.drm_cards);
println!(
" ACPI power surface: {}",
if report.hardware.acpi_power_surface_present {
"present"
} else {
"unavailable"
}
);
println!(
" RTL8125 device seen: {}",
if report.hardware.rtl8125_present {
@@ -1758,6 +1934,70 @@ fn print_json(report: &Report<'_>) {
true,
4,
);
push_json_number_field(
&mut out,
"pci_irq_none",
report.hardware.pci_irq_none,
true,
4,
);
push_json_number_field(
&mut out,
"pci_irq_legacy",
report.hardware.pci_irq_legacy,
true,
4,
);
push_json_number_field(
&mut out,
"pci_irq_msi",
report.hardware.pci_irq_msi,
true,
4,
);
push_json_number_field(
&mut out,
"pci_irq_msix",
report.hardware.pci_irq_msix,
true,
4,
);
push_json_number_field(
&mut out,
"pci_irq_forced_legacy",
report.hardware.pci_irq_forced_legacy,
true,
4,
);
push_json_number_field(
&mut out,
"pci_irq_msix_disabled_by_quirk",
report.hardware.pci_irq_msix_disabled_by_quirk,
true,
4,
);
push_json_number_field(
&mut out,
"pci_irq_msi_disabled_by_quirk",
report.hardware.pci_irq_msi_disabled_by_quirk,
true,
4,
);
out.push_str(" \"runtime_irq_reports\": [\n");
for (index, item) in report.hardware.runtime_irq_reports.iter().enumerate() {
out.push_str(" {\n");
push_json_string_field(&mut out, "driver", &item.driver, true, 8);
push_json_number_field(&mut out, "pid", item.pid as usize, true, 8);
push_json_string_field(&mut out, "device", &item.device, true, 8);
push_json_string_field(&mut out, "mode", &item.mode, true, 8);
push_json_string_field(&mut out, "reason", &item.reason, false, 8);
out.push_str(" }");
if index + 1 != report.hardware.runtime_irq_reports.len() {
out.push(',');
}
out.push('\n');
}
out.push_str(" ],\n");
push_json_number_field(
&mut out,
"usb_controllers",
@@ -1766,6 +2006,13 @@ fn print_json(report: &Report<'_>) {
4,
);
push_json_number_field(&mut out, "drm_cards", report.hardware.drm_cards, true, 4);
push_json_bool_field(
&mut out,
"acpi_power_surface_present",
report.hardware.acpi_power_surface_present,
true,
4,
);
push_json_bool_field(
&mut out,
"rtl8125_present",
@@ -2160,6 +2407,23 @@ fn probe_iommu_scheme(
probe_named_scheme(runtime, "iommu")
}
fn probe_acpi_power_surface(
runtime: &Runtime,
_network: &NetworkReport,
_hardware: &HardwareReport,
_check: &IntegrationCheck,
) -> Option<String> {
let adapters = runtime.read_dir_names("/scheme/acpi/power/adapters");
let batteries = runtime.read_dir_names("/scheme/acpi/power/batteries");
runtime.exists("/scheme/acpi/power").then(|| {
format!(
"acpi power surface visible (adapters={}, batteries={})",
adapters.as_ref().map(|items| items.len()).unwrap_or(0),
batteries.as_ref().map(|items| items.len()).unwrap_or(0)
)
})
}
fn probe_serio_surface(
runtime: &Runtime,
_network: &NetworkReport,
@@ -2927,16 +3191,12 @@ mod tests {
fn rtl8125_hardware_detection_parses_pci_config() {
let root = temp_root();
create_dir(&root, "/scheme/pci/0000--02--00.0");
let config = [
(RTL8125_VENDOR_ID & 0xff) as u8,
(RTL8125_VENDOR_ID >> 8) as u8,
(RTL8125_DEVICE_ID & 0xff) as u8,
(RTL8125_DEVICE_ID >> 8) as u8,
0,
0,
0,
0,
];
let mut config = [0u8; 64];
config[0x00] = (RTL8125_VENDOR_ID & 0xff) as u8;
config[0x01] = (RTL8125_VENDOR_ID >> 8) as u8;
config[0x02] = (RTL8125_DEVICE_ID & 0xff) as u8;
config[0x03] = (RTL8125_DEVICE_ID >> 8) as u8;
config[0x0e] = 0x00;
let path = root.join("scheme/pci/0000--02--00.0/config");
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap();
@@ -2954,16 +3214,12 @@ mod tests {
fn virtio_net_hardware_detection_parses_pci_config() {
let root = temp_root();
create_dir(&root, "/scheme/pci/0000--00--03.0");
let config = [
(VIRTIO_NET_VENDOR_ID & 0xff) as u8,
(VIRTIO_NET_VENDOR_ID >> 8) as u8,
(VIRTIO_NET_DEVICE_ID & 0xff) as u8,
(VIRTIO_NET_DEVICE_ID >> 8) as u8,
0,
0,
0,
0,
];
let mut config = [0u8; 64];
config[0x00] = (VIRTIO_NET_VENDOR_ID & 0xff) as u8;
config[0x01] = (VIRTIO_NET_VENDOR_ID >> 8) as u8;
config[0x02] = (VIRTIO_NET_DEVICE_ID & 0xff) as u8;
config[0x03] = (VIRTIO_NET_DEVICE_ID >> 8) as u8;
config[0x0e] = 0x00;
let path = root.join("scheme/pci/0000--00--03.0/config");
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap();
@@ -2977,6 +3233,127 @@ mod tests {
fs::remove_dir_all(root).unwrap();
}
#[test]
fn hardware_report_counts_pci_interrupt_support_modes() {
let root = temp_root();
create_dir(&root, "/scheme/pci/0000--00--01.0");
create_dir(&root, "/scheme/pci/0000--00--02.0");
create_dir(&root, "/scheme/pci/0000--00--03.0");
let mut legacy = [0u8; 68];
legacy[0x00] = 0x34;
legacy[0x01] = 0x12;
legacy[0x02] = 0x78;
legacy[0x03] = 0x56;
legacy[0x06] = 0x10; // capabilities present
legacy[0x0e] = 0x00;
legacy[0x34] = 0x40;
legacy[0x3c] = 11;
legacy[0x40] = 0x01; // power capability only
legacy[0x41] = 0x00;
let mut msi = legacy;
msi[0x02] = 0x79;
msi[0x40] = 0x05; // MSI capability
let mut msix = legacy;
msix[0x02] = 0x7a;
msix[0x40] = 0x11; // MSI-X capability
fs::write(root.join("scheme/pci/0000--00--01.0/config"), legacy).unwrap();
fs::write(root.join("scheme/pci/0000--00--02.0/config"), msi).unwrap();
fs::write(root.join("scheme/pci/0000--00--03.0/config"), msix).unwrap();
let network = collect_network(&Runtime::from_root(root.clone()));
let hardware = collect_hardware(&Runtime::from_root(root.clone()), &network);
assert_eq!(hardware.pci_devices, 3);
assert_eq!(hardware.pci_irq_none, 0);
assert!(hardware.pci_irq_legacy >= 1);
assert_eq!(
hardware.pci_irq_legacy + hardware.pci_irq_msi + hardware.pci_irq_msix,
hardware.pci_devices
);
assert_eq!(
hardware.pci_irq_forced_legacy
+ hardware.pci_irq_msix_disabled_by_quirk
+ hardware.pci_irq_msi_disabled_by_quirk,
0
);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn hardware_report_detects_acpi_power_surface() {
let root = temp_root();
create_dir(&root, "/scheme/acpi/power");
let network = collect_network(&Runtime::from_root(root.clone()));
let hardware = collect_hardware(&Runtime::from_root(root.clone()), &network);
assert!(hardware.acpi_power_surface_present);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn collect_irq_runtime_reports_reads_driver_mode_files() {
let root = temp_root();
create_dir(&root, "/proc/123");
write_file(
&root,
"/tmp/redbear-irq-report/xhcid.env",
"driver=xhcid\npid=123\ndevice=0000:00:14.0\nmode=msi_or_msix\nreason=driver_selected_interrupt_delivery\n",
);
let reports = collect_irq_runtime_reports(&Runtime::from_root(root.clone()));
assert_eq!(reports.len(), 1);
assert_eq!(reports[0].driver, "xhcid");
assert_eq!(reports[0].pid, 123);
assert_eq!(reports[0].mode, "msi_or_msix");
assert_eq!(reports[0].reason, "driver_selected_interrupt_delivery");
fs::remove_dir_all(root).unwrap();
}
#[test]
fn collect_irq_runtime_reports_ignores_stale_pid_entries() {
let root = temp_root();
write_file(
&root,
"/tmp/redbear-irq-report/xhcid.env",
"driver=xhcid\npid=999\ndevice=0000:00:14.0\nmode=msi_or_msix\nreason=driver_selected_interrupt_delivery\n",
);
let reports = collect_irq_runtime_reports(&Runtime::from_root(root.clone()));
assert!(reports.is_empty());
fs::remove_dir_all(root).unwrap();
}
#[test]
fn redbear_upower_integration_is_present_without_live_power_surface() {
let root = temp_root();
write_file(&root, "/usr/bin/redbear-upower", "");
let report = collect_report(&Runtime::from_root(root.clone()));
assert_eq!(integration_state(&report, "redbear-upower"), ProbeState::Present);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn redbear_upower_integration_is_functional_with_live_power_surface() {
let root = temp_root();
write_file(&root, "/usr/bin/redbear-upower", "");
create_dir(&root, "/scheme/acpi/power/adapters/AC");
create_dir(&root, "/scheme/acpi/power/batteries");
let report = collect_report(&Runtime::from_root(root.clone()));
assert_eq!(integration_state(&report, "redbear-upower"), ProbeState::Functional);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn json_output_contains_network_and_integration_state() {
let root = temp_root();
@@ -2997,6 +3374,7 @@ mod tests {
let report = collect_report(&Runtime::from_root(root.clone()));
assert!(!report.hardware.virtio_net_present);
assert!(!report.hardware.acpi_power_surface_present);
let mut output = String::new();
output.push_str("{");
push_json_string_field(
@@ -3031,6 +3409,12 @@ mod tests {
.iter()
.any(|item| item.check.name == "redbear-btctl")
);
assert!(
report
.integrations
.iter()
.any(|item| item.check.name == "redbear-upower")
);
assert!(
report
.integrations