5c90afdad8
- redbear-info: -i launches ratatui dashboard (System/Hardware/Network/Integrations/Health tabs) - redbear-netctl: -i wires existing netctl-console ratatui TUI - Same -i switch convention as cub - Feature-gated behind 'tui' feature for both apps
4592 lines
145 KiB
Rust
4592 lines
145 KiB
Rust
use std::env;
|
|
use std::fs;
|
|
use std::io::Read;
|
|
use std::path::PathBuf;
|
|
use std::process;
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
use redox_driver_sys::pci::{InterruptSupport, parse_device_info_from_config_space};
|
|
use redox_driver_sys::quirks::{PciQuirkFlags, lookup_pci_quirks};
|
|
use serde_json::Value as JsonValue;
|
|
use toml::Value;
|
|
|
|
#[cfg(test)]
|
|
use std::path::Path;
|
|
|
|
#[cfg(feature = "tui")]
|
|
mod tui;
|
|
|
|
const RESET: &str = "\x1b[0m";
|
|
const GREEN: &str = "\x1b[32m";
|
|
const YELLOW: &str = "\x1b[33m";
|
|
const RED: &str = "\x1b[31m";
|
|
const BLUE: &str = "\x1b[34m";
|
|
const DIVIDER: &str = "═══════════════════════════════════════════════════════════════════";
|
|
const RTL8125_VENDOR_ID: u16 = 0x10ec;
|
|
const RTL8125_DEVICE_ID: u16 = 0x8125;
|
|
const VIRTIO_NET_VENDOR_ID: u16 = 0x1af4;
|
|
const VIRTIO_NET_DEVICE_ID: u16 = 0x1000;
|
|
const BLUETOOTH_STATUS_FRESHNESS_SECS: u64 = 90;
|
|
const BOOT_TIMELINE_PATH: &str = "/tmp/redbear-boot-timeline.json";
|
|
const DRIVER_PARAMS_ROOT: &str = "/tmp/redbear-driver-params";
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
enum OutputMode {
|
|
Table,
|
|
Json,
|
|
Test,
|
|
Quirks,
|
|
Probe,
|
|
Boot,
|
|
Device,
|
|
Health,
|
|
Help,
|
|
Tui,
|
|
}
|
|
|
|
struct Options {
|
|
mode: OutputMode,
|
|
verbose: bool,
|
|
device: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
enum ProbeState {
|
|
Absent,
|
|
Present,
|
|
Active,
|
|
Functional,
|
|
Unobservable,
|
|
}
|
|
|
|
struct Runtime {
|
|
root: Option<PathBuf>,
|
|
}
|
|
|
|
struct IdentityReport {
|
|
pretty_name: Option<String>,
|
|
version_id: Option<String>,
|
|
hostname: Option<String>,
|
|
}
|
|
|
|
struct NetworkReport {
|
|
state: ProbeState,
|
|
connected: bool,
|
|
interface: Option<String>,
|
|
mac: Option<String>,
|
|
address: Option<String>,
|
|
dns: Option<String>,
|
|
default_route: Option<String>,
|
|
active_profile: Option<String>,
|
|
network_schemes: Vec<String>,
|
|
wifi_control_state: ProbeState,
|
|
wifi_interfaces: Vec<String>,
|
|
wifi_firmware_status: Option<String>,
|
|
wifi_transport_status: Option<String>,
|
|
wifi_transport_init_status: Option<String>,
|
|
wifi_activation_status: Option<String>,
|
|
wifi_connect_result: Option<String>,
|
|
wifi_disconnect_result: Option<String>,
|
|
wifi_scan_results: Vec<String>,
|
|
claim_limit: &'static str,
|
|
bluetooth_transport_state: ProbeState,
|
|
bluetooth_control_state: ProbeState,
|
|
bluetooth_adapters: Vec<String>,
|
|
bluetooth_transport_status: Option<String>,
|
|
bluetooth_adapter_status: Option<String>,
|
|
bluetooth_scan_results: Vec<String>,
|
|
bluetooth_connection_state: Option<String>,
|
|
bluetooth_connect_result: Option<String>,
|
|
bluetooth_disconnect_result: Option<String>,
|
|
bluetooth_read_char_result: Option<String>,
|
|
bluetooth_bond_store_path: Option<String>,
|
|
bluetooth_bond_count: Option<usize>,
|
|
bluetooth_claim_limit: &'static str,
|
|
}
|
|
|
|
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>,
|
|
usb_quirks: Vec<UsbQuirkEntry>,
|
|
dmi_quirk_count: usize,
|
|
}
|
|
|
|
struct QuirkEntry {
|
|
vendor: String,
|
|
device: Option<String>,
|
|
class: Option<String>,
|
|
flags: Vec<String>,
|
|
description: Option<String>,
|
|
}
|
|
|
|
struct UsbQuirkEntry {
|
|
vendor: String,
|
|
product: Option<String>,
|
|
flags: Vec<String>,
|
|
}
|
|
|
|
struct QuirksReport {
|
|
files_loaded: Vec<QuirkFile>,
|
|
load_errors: Vec<String>,
|
|
}
|
|
|
|
struct IntegrationCheck {
|
|
name: &'static str,
|
|
category: &'static str,
|
|
description: &'static str,
|
|
artifact_path: Option<&'static str>,
|
|
control_path: Option<&'static str>,
|
|
test_hint: &'static str,
|
|
note: &'static str,
|
|
functional_probe:
|
|
Option<fn(&Runtime, &NetworkReport, &HardwareReport, &IntegrationCheck) -> Option<String>>,
|
|
}
|
|
|
|
struct IntegrationStatus<'a> {
|
|
check: &'a IntegrationCheck,
|
|
state: ProbeState,
|
|
artifact_present: Option<bool>,
|
|
control_present: Option<bool>,
|
|
evidence: Vec<String>,
|
|
claim_limit: &'static str,
|
|
}
|
|
|
|
struct Report<'a> {
|
|
identity: IdentityReport,
|
|
network: NetworkReport,
|
|
hardware: HardwareReport,
|
|
integrations: Vec<IntegrationStatus<'a>>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
struct BootTimelineEntry {
|
|
ts: u128,
|
|
event: BootTimelineEvent,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
enum BootTimelineEvent {
|
|
BusEnumerated {
|
|
bus: String,
|
|
count: usize,
|
|
},
|
|
Probe {
|
|
device: String,
|
|
driver: String,
|
|
status: BootProbeStatus,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
enum BootProbeStatus {
|
|
Bound,
|
|
Deferred,
|
|
Failed,
|
|
Skipped,
|
|
}
|
|
|
|
struct DeviceStatusReport {
|
|
selector: String,
|
|
status: Option<BootProbeStatus>,
|
|
vendor_id: u16,
|
|
device_id: u16,
|
|
class_code: u8,
|
|
class_name: &'static str,
|
|
irq_mode: String,
|
|
driver: Option<String>,
|
|
parameters: Vec<(String, String)>,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
enum HealthState {
|
|
Healthy,
|
|
Warning,
|
|
Critical,
|
|
}
|
|
|
|
struct HealthItem {
|
|
label: &'static str,
|
|
state: HealthState,
|
|
detail: String,
|
|
}
|
|
|
|
const INTEGRATIONS: &[IntegrationCheck] = &[
|
|
IntegrationCheck {
|
|
name: "redbear-info",
|
|
category: "Tool",
|
|
description: "Runtime integration status utility",
|
|
artifact_path: Some("/usr/bin/redbear-info"),
|
|
control_path: None,
|
|
test_hint: "redbear-info --json",
|
|
note: "Binary presence proves the tool is installed, not that every integration is healthy.",
|
|
functional_probe: None,
|
|
},
|
|
IntegrationCheck {
|
|
name: "lspci",
|
|
category: "Tool",
|
|
description: "Native PCI inventory command",
|
|
artifact_path: Some("/usr/bin/lspci"),
|
|
control_path: Some("/scheme/pci"),
|
|
test_hint: "lspci",
|
|
note: "Functional when the PCI scheme is enumerable.",
|
|
functional_probe: Some(probe_directory_readable),
|
|
},
|
|
IntegrationCheck {
|
|
name: "lsusb",
|
|
category: "Tool",
|
|
description: "Native USB inventory command",
|
|
artifact_path: Some("/usr/bin/lsusb"),
|
|
control_path: Some("/scheme"),
|
|
test_hint: "lsusb",
|
|
note: "Functional when at least one usb.* controller scheme is readable.",
|
|
functional_probe: Some(probe_usb_surface),
|
|
},
|
|
IntegrationCheck {
|
|
name: "netctl",
|
|
category: "Tool",
|
|
description: "Redox-native network profile manager",
|
|
artifact_path: Some("/usr/bin/netctl"),
|
|
control_path: Some("/etc/netctl"),
|
|
test_hint: "netctl status",
|
|
note: "Profiles and active profile tracking are readable; profile application remains a separate runtime action.",
|
|
functional_probe: Some(probe_netctl_surface),
|
|
},
|
|
IntegrationCheck {
|
|
name: "redbear-wifictl",
|
|
category: "Networking",
|
|
description: "Wi-Fi control daemon and scheme",
|
|
artifact_path: Some("/usr/bin/redbear-wifictl"),
|
|
control_path: Some("/scheme/wifictl"),
|
|
test_hint: "ls /scheme/wifictl/ && redbear-wifictl --connect wlan0 demo open && netctl start wifi-dhcp",
|
|
note: "Functional when the wifictl scheme is enumerable, reports interface state, and exposes the bounded connect path; this still does not prove real radio association or working Wi-Fi connectivity.",
|
|
functional_probe: Some(probe_wifictl_surface),
|
|
},
|
|
IntegrationCheck {
|
|
name: "redbear-btusb",
|
|
category: "Bluetooth",
|
|
description: "Bounded USB Bluetooth transport daemon",
|
|
artifact_path: Some("/usr/bin/redbear-btusb"),
|
|
control_path: Some("/var/run/redbear-btusb/status"),
|
|
test_hint: "redbear-btusb --probe && redbear-btusb --status",
|
|
note: "Active when the explicit-startup btusb status file is visible; this does not prove controller initialization, USB-class autospawn, or a real BLE workload.",
|
|
functional_probe: Some(probe_btusb_surface),
|
|
},
|
|
IntegrationCheck {
|
|
name: "redbear-btctl",
|
|
category: "Bluetooth",
|
|
description: "Bounded Bluetooth host/control daemon and scheme",
|
|
artifact_path: Some("/usr/bin/redbear-btctl"),
|
|
control_path: Some("/scheme/btctl"),
|
|
test_hint: "redbear-btctl --probe && redbear-btctl --status && redbear-btctl --scan && redbear-btctl --connect hci0 <bond-id> && redbear-btctl --read-char hci0 <bond-id> 0000180f-0000-1000-8000-00805f9b34fb 00002a19-0000-1000-8000-00805f9b34fb",
|
|
note: "Functional when the btctl scheme is enumerable and reports adapter state plus one experimental battery-sensor Battery Level read result; this still does not prove general device traffic, generic GATT, write/notify support, classic Bluetooth, or desktop integration.",
|
|
functional_probe: Some(probe_btctl_surface),
|
|
},
|
|
IntegrationCheck {
|
|
name: "redbear-iwlwifi",
|
|
category: "Drivers",
|
|
description: "Bounded Intel Wi-Fi driver-side package",
|
|
artifact_path: Some("/usr/lib/drivers/redbear-iwlwifi"),
|
|
control_path: Some("/scheme/pci"),
|
|
test_hint: "redbear-iwlwifi --probe && redbear-iwlwifi --connect demo open",
|
|
note: "Functional when the Intel Wi-Fi driver package is installed and PCI inventory is accessible; bounded scan/connect actions may succeed, but this still does not prove real radio association or working connectivity.",
|
|
functional_probe: Some(probe_pci_surface),
|
|
},
|
|
IntegrationCheck {
|
|
name: "redbear-netstat",
|
|
category: "Tool",
|
|
description: "Native Red Bear network status reporter",
|
|
artifact_path: Some("/usr/bin/redbear-netstat"),
|
|
control_path: Some("/scheme/netcfg"),
|
|
test_hint: "redbear-netstat",
|
|
note: "Functional when the netcfg scheme answers read-only interface, route, and resolver queries.",
|
|
functional_probe: Some(probe_smolnetd_surface),
|
|
},
|
|
IntegrationCheck {
|
|
name: "redbear-traceroute",
|
|
category: "Tool",
|
|
description: "Native UDP-based path tracing utility",
|
|
artifact_path: Some("/usr/bin/redbear-traceroute"),
|
|
control_path: Some("/scheme/icmp"),
|
|
test_hint: "redbear-traceroute 1.1.1.1",
|
|
note: "Binary presence proves installation; successful hop tracing still depends on the live ICMP and UDP path in the current runtime.",
|
|
functional_probe: Some(probe_icmp_surface),
|
|
},
|
|
IntegrationCheck {
|
|
name: "redbear-mtr",
|
|
category: "Tool",
|
|
description: "Native path measurement tool built on traceroute probes",
|
|
artifact_path: Some("/usr/bin/redbear-mtr"),
|
|
control_path: Some("/scheme/icmp"),
|
|
test_hint: "redbear-mtr 1.1.1.1",
|
|
note: "Binary presence proves installation; useful measurements still depend on the same live ICMP and UDP probe substrate as traceroute.",
|
|
functional_probe: Some(probe_icmp_surface),
|
|
},
|
|
IntegrationCheck {
|
|
name: "redbear-nmap",
|
|
category: "Tool",
|
|
description: "Bounded TCP connect-scan utility",
|
|
artifact_path: Some("/usr/bin/redbear-nmap"),
|
|
control_path: Some("/scheme/netcfg"),
|
|
test_hint: "redbear-nmap 127.0.0.1 22,80,443",
|
|
note: "Binary presence proves the scanner is installed; successful scans still depend on live networking and reachable targets.",
|
|
functional_probe: Some(probe_smolnetd_surface),
|
|
},
|
|
IntegrationCheck {
|
|
name: "pcid-spawner",
|
|
category: "Core",
|
|
description: "PCI driver autoload daemon",
|
|
artifact_path: Some("/usr/bin/pcid-spawner"),
|
|
control_path: Some("/scheme/pci"),
|
|
test_hint: "lspci",
|
|
note: "The PCI scheme proves discovery is live, but not which driver handled each device.",
|
|
functional_probe: Some(probe_directory_readable),
|
|
},
|
|
IntegrationCheck {
|
|
name: "smolnetd",
|
|
category: "Networking",
|
|
description: "Native TCP/IP stack daemon",
|
|
artifact_path: Some("/usr/bin/smolnetd"),
|
|
control_path: Some("/scheme/netcfg"),
|
|
test_hint: "redbear-info --verbose",
|
|
note: "Functional when the netcfg scheme answers read-only queries.",
|
|
functional_probe: Some(probe_smolnetd_surface),
|
|
},
|
|
IntegrationCheck {
|
|
name: "xhcid",
|
|
category: "USB",
|
|
description: "xHCI host-controller daemon",
|
|
artifact_path: Some("/usr/lib/drivers/xhcid"),
|
|
control_path: Some("/scheme"),
|
|
test_hint: "lsusb",
|
|
note: "Functional when at least one usb.* controller scheme is registered.",
|
|
functional_probe: Some(probe_usb_surface),
|
|
},
|
|
IntegrationCheck {
|
|
name: "dhcpd",
|
|
category: "Networking",
|
|
description: "DHCP client daemon",
|
|
artifact_path: Some("/usr/bin/dhcpd"),
|
|
control_path: None,
|
|
test_hint: "netctl start <dhcp-profile>",
|
|
note: "Binary presence is observable; passive probing cannot prove the DHCP client is currently driving configuration.",
|
|
functional_probe: None,
|
|
},
|
|
IntegrationCheck {
|
|
name: "ext4d",
|
|
category: "Filesystem",
|
|
description: "ext4 scheme daemon",
|
|
artifact_path: Some("/usr/bin/ext4d"),
|
|
control_path: Some("/scheme/ext4d"),
|
|
test_hint: "ls /scheme/ext4d/",
|
|
note: "Functional when the ext4 scheme directory can be enumerated.",
|
|
functional_probe: Some(probe_directory_readable),
|
|
},
|
|
IntegrationCheck {
|
|
name: "firmware-loader",
|
|
category: "System",
|
|
description: "Firmware indexing and serving daemon",
|
|
artifact_path: Some("/usr/bin/firmware-loader"),
|
|
control_path: Some("/scheme"),
|
|
test_hint: "ls /scheme/firmware/",
|
|
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",
|
|
description: "IOMMU DMA-remapping daemon",
|
|
artifact_path: Some("/usr/bin/iommu"),
|
|
control_path: Some("/scheme/iommu"),
|
|
test_hint: "redbear-phase-iommu-check",
|
|
note: "Functional when the iommu scheme is registered in /scheme.",
|
|
functional_probe: Some(probe_iommu_scheme),
|
|
},
|
|
IntegrationCheck {
|
|
name: "redbear-phase-ps2-check",
|
|
category: "Validation",
|
|
description: "Bounded PS/2 + serio runtime proof helper",
|
|
artifact_path: Some("/usr/bin/redbear-phase-ps2-check"),
|
|
control_path: Some("/scheme/serio/0"),
|
|
test_hint: "redbear-phase-ps2-check",
|
|
note: "Functional when the PS/2 proof helper is installed and both serio keyboard/mouse nodes are visible.",
|
|
functional_probe: Some(probe_serio_surface),
|
|
},
|
|
IntegrationCheck {
|
|
name: "redbear-phase-timer-check",
|
|
category: "Validation",
|
|
description: "Bounded monotonic timer runtime proof helper",
|
|
artifact_path: Some("/usr/bin/redbear-phase-timer-check"),
|
|
control_path: Some("/scheme/time/4"),
|
|
test_hint: "redbear-phase-timer-check",
|
|
note: "Functional when the monotonic time scheme node is visible for bounded runtime timer proof.",
|
|
functional_probe: Some(probe_time_surface),
|
|
},
|
|
IntegrationCheck {
|
|
name: "udev-shim",
|
|
category: "System",
|
|
description: "udev-compatible device enumeration shim",
|
|
artifact_path: Some("/usr/bin/udev-shim"),
|
|
control_path: Some("/scheme"),
|
|
test_hint: "ls /scheme/udev/",
|
|
note: "Functional when the udev scheme can be listed.",
|
|
functional_probe: Some(probe_udev_scheme),
|
|
},
|
|
IntegrationCheck {
|
|
name: "evdevd",
|
|
category: "Input",
|
|
description: "Event-device translation daemon",
|
|
artifact_path: Some("/usr/bin/evdevd"),
|
|
control_path: Some("/scheme"),
|
|
test_hint: "ls /scheme/evdev/",
|
|
note: "Functional when event nodes are enumerable through the evdev scheme.",
|
|
functional_probe: Some(probe_evdev_scheme),
|
|
},
|
|
IntegrationCheck {
|
|
name: "redox-drm",
|
|
category: "GPU",
|
|
description: "DRM/KMS scheme daemon",
|
|
artifact_path: None,
|
|
control_path: Some("/scheme/drm"),
|
|
test_hint: "ls /scheme/drm/",
|
|
note: "A live DRM scheme proves the daemon is running, not that hardware display is fully validated.",
|
|
functional_probe: Some(probe_directory_readable),
|
|
},
|
|
IntegrationCheck {
|
|
name: "amdgpu",
|
|
category: "GPU",
|
|
description: "AMD GPU userspace driver library",
|
|
artifact_path: Some("/usr/lib/redox/drivers/libamdgpu_dc_redox.so"),
|
|
control_path: None,
|
|
test_hint: "redbear-info --verbose",
|
|
note: "Library presence proves packaging; runtime GPU validation still depends on actual hardware and redox-drm activity.",
|
|
functional_probe: None,
|
|
},
|
|
IntegrationCheck {
|
|
name: "rtl8125-native-path",
|
|
category: "Networking",
|
|
description: "Native Realtek RTL8125 support through the rtl8168d autoload path",
|
|
artifact_path: Some("/usr/lib/drivers/rtl8168d"),
|
|
control_path: Some("/scheme/pci"),
|
|
test_hint: "redbear-info --verbose",
|
|
note: "This only becomes functional when 10ec:8125 hardware is present and a network.* scheme is live.",
|
|
functional_probe: Some(probe_rtl8125_path),
|
|
},
|
|
IntegrationCheck {
|
|
name: "virtio-net-vm-path",
|
|
category: "Networking",
|
|
description: "VirtIO network support for QEMU and other virtualized baselines",
|
|
artifact_path: Some("/usr/lib/drivers/virtio-netd"),
|
|
control_path: Some("/scheme/pci"),
|
|
test_hint: "redbear-info --verbose",
|
|
note: "This becomes functional when a VirtIO NIC (1af4:1000) is present and a network.* scheme is live.",
|
|
functional_probe: Some(probe_virtio_net_path),
|
|
},
|
|
];
|
|
|
|
fn main() {
|
|
if let Err(err) = run() {
|
|
eprintln!("redbear-info: {err}");
|
|
process::exit(1);
|
|
}
|
|
}
|
|
|
|
fn run() -> Result<(), String> {
|
|
let options = parse_args(env::args())?;
|
|
|
|
if options.mode == OutputMode::Tui {
|
|
#[cfg(feature = "tui")]
|
|
{
|
|
let runtime = Runtime::from_env();
|
|
return tui::run(&runtime).map_err(|e| format!("TUI error: {e}"));
|
|
}
|
|
#[cfg(not(feature = "tui"))]
|
|
return Err("TUI support not compiled (enable 'tui' feature)".to_string());
|
|
}
|
|
|
|
if options.mode == OutputMode::Help {
|
|
print_help();
|
|
return Ok(());
|
|
}
|
|
|
|
let runtime = Runtime::from_env();
|
|
|
|
if options.mode == OutputMode::Quirks {
|
|
let quirks = collect_quirks(&runtime);
|
|
print_quirks(&quirks, options.verbose);
|
|
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());
|
|
}
|
|
|
|
if options.mode == OutputMode::Boot {
|
|
let timeline = collect_boot_timeline(&runtime)?;
|
|
print_boot_timeline(&timeline);
|
|
return Ok(());
|
|
}
|
|
|
|
if options.mode == OutputMode::Device {
|
|
let requested = options
|
|
.device
|
|
.as_deref()
|
|
.ok_or_else(|| "missing device selector".to_string())?;
|
|
let report = collect_device_status(&runtime, requested)?;
|
|
print_device_status(&report);
|
|
return Ok(());
|
|
}
|
|
|
|
let report = collect_report(&runtime);
|
|
|
|
if options.mode == OutputMode::Health {
|
|
let health = collect_health_items(&runtime, &report);
|
|
print_health_dashboard(&health);
|
|
return Ok(());
|
|
}
|
|
|
|
match options.mode {
|
|
OutputMode::Table => print_table(&report, options.verbose),
|
|
OutputMode::Json => print_json(&report),
|
|
OutputMode::Test => print_tests(&report, options.verbose),
|
|
OutputMode::Quirks => {}
|
|
OutputMode::Probe => {}
|
|
OutputMode::Boot => {}
|
|
OutputMode::Device => {}
|
|
OutputMode::Health => {}
|
|
OutputMode::Help => {}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
impl Runtime {
|
|
fn from_env() -> Self {
|
|
Self {
|
|
root: env::var_os("REDBEAR_INFO_ROOT").map(PathBuf::from),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn from_root(root: PathBuf) -> Self {
|
|
Self { root: Some(root) }
|
|
}
|
|
|
|
fn resolve(&self, absolute: &str) -> PathBuf {
|
|
let trimmed = absolute.trim_start_matches('/');
|
|
match &self.root {
|
|
Some(root) => root.join(trimmed),
|
|
None => PathBuf::from(absolute),
|
|
}
|
|
}
|
|
|
|
fn exists(&self, absolute: &str) -> bool {
|
|
self.resolve(absolute).exists()
|
|
}
|
|
|
|
fn is_dir(&self, absolute: &str) -> bool {
|
|
self.resolve(absolute).is_dir()
|
|
}
|
|
|
|
fn read_to_string(&self, absolute: &str) -> Option<String> {
|
|
fs::read_to_string(self.resolve(absolute)).ok()
|
|
}
|
|
|
|
fn read_dir_names(&self, absolute: &str) -> Option<Vec<String>> {
|
|
let mut names = Vec::new();
|
|
for entry in fs::read_dir(self.resolve(absolute)).ok()? {
|
|
let entry = entry.ok()?;
|
|
let name = entry.file_name();
|
|
let name = name.to_str()?.to_string();
|
|
names.push(name);
|
|
}
|
|
names.sort();
|
|
Some(names)
|
|
}
|
|
}
|
|
|
|
fn collect_report<'a>(runtime: &Runtime) -> Report<'a> {
|
|
let identity = collect_identity(runtime);
|
|
let network = collect_network(runtime);
|
|
let hardware = collect_hardware(runtime, &network);
|
|
let integrations = INTEGRATIONS
|
|
.iter()
|
|
.map(|check| inspect_integration(runtime, &network, &hardware, check))
|
|
.collect();
|
|
|
|
Report {
|
|
identity,
|
|
network,
|
|
hardware,
|
|
integrations,
|
|
}
|
|
}
|
|
|
|
fn collect_identity(runtime: &Runtime) -> IdentityReport {
|
|
let os_release = runtime.read_to_string("/usr/lib/os-release");
|
|
IdentityReport {
|
|
pretty_name: os_release
|
|
.as_deref()
|
|
.and_then(|content| parse_os_release_value(content, "PRETTY_NAME")),
|
|
version_id: os_release
|
|
.as_deref()
|
|
.and_then(|content| parse_os_release_value(content, "VERSION_ID")),
|
|
hostname: runtime
|
|
.read_to_string("/etc/hostname")
|
|
.map(|value| value.trim().to_string())
|
|
.filter(|value| !value.is_empty()),
|
|
}
|
|
}
|
|
|
|
fn collect_network(runtime: &Runtime) -> NetworkReport {
|
|
let network_schemes = runtime
|
|
.read_dir_names("/scheme")
|
|
.unwrap_or_default()
|
|
.into_iter()
|
|
.filter(|name| name.starts_with("network."))
|
|
.collect::<Vec<_>>();
|
|
|
|
let dns = read_trimmed(runtime, "/scheme/netcfg/resolv/nameserver")
|
|
.or_else(|| read_trimmed(runtime, "/etc/net/dns"))
|
|
.filter(|value| !value.is_empty());
|
|
|
|
let default_route = read_trimmed(runtime, "/scheme/netcfg/route/list")
|
|
.and_then(|routes| parse_default_route(&routes));
|
|
|
|
let active_profile =
|
|
read_trimmed(runtime, "/etc/netctl/active").filter(|value| !value.is_empty());
|
|
|
|
let active_interface = active_profile
|
|
.as_deref()
|
|
.and_then(|profile| active_profile_interface(runtime, profile));
|
|
|
|
let preferred_interface = active_interface.clone().or_else(|| {
|
|
runtime
|
|
.exists("/scheme/netcfg/ifaces/eth0/mac")
|
|
.then_some("eth0".to_string())
|
|
});
|
|
|
|
let mac = preferred_interface.as_ref().and_then(|iface| {
|
|
read_trimmed(runtime, &format!("/scheme/netcfg/ifaces/{iface}/mac"))
|
|
.filter(|value| !matches!(value.as_str(), "Not configured" | "Device not found"))
|
|
});
|
|
|
|
let address = preferred_interface.as_ref().and_then(|iface| {
|
|
read_trimmed(runtime, &format!("/scheme/netcfg/ifaces/{iface}/addr/list"))
|
|
.filter(|value| !matches!(value.as_str(), "Not configured" | "Device not found"))
|
|
});
|
|
|
|
let wifi_interfaces = runtime
|
|
.read_dir_names("/scheme/wifictl/ifaces")
|
|
.or_else(|| {
|
|
runtime
|
|
.read_to_string("/scheme/wifictl/ifaces")
|
|
.map(|value| {
|
|
value
|
|
.lines()
|
|
.map(str::trim)
|
|
.filter(|line| !line.is_empty())
|
|
.map(str::to_string)
|
|
.collect::<Vec<_>>()
|
|
})
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let wifi_control_state = if runtime.exists("/scheme/wifictl") {
|
|
if wifi_interfaces.is_empty() {
|
|
ProbeState::Active
|
|
} else {
|
|
ProbeState::Functional
|
|
}
|
|
} else if runtime.exists("/usr/bin/redbear-wifictl") {
|
|
ProbeState::Present
|
|
} else {
|
|
ProbeState::Absent
|
|
};
|
|
|
|
let wifi_primary = wifi_interfaces.first().cloned();
|
|
let wifi_firmware_status = wifi_primary.as_ref().and_then(|iface| {
|
|
read_trimmed(
|
|
runtime,
|
|
&format!("/scheme/wifictl/ifaces/{iface}/firmware-status"),
|
|
)
|
|
});
|
|
let wifi_transport_status = wifi_primary.as_ref().and_then(|iface| {
|
|
read_trimmed(
|
|
runtime,
|
|
&format!("/scheme/wifictl/ifaces/{iface}/transport-status"),
|
|
)
|
|
});
|
|
let wifi_transport_init_status = wifi_primary.as_ref().and_then(|iface| {
|
|
read_trimmed(
|
|
runtime,
|
|
&format!("/scheme/wifictl/ifaces/{iface}/transport-init-status"),
|
|
)
|
|
});
|
|
let wifi_activation_status = wifi_primary.as_ref().and_then(|iface| {
|
|
read_trimmed(
|
|
runtime,
|
|
&format!("/scheme/wifictl/ifaces/{iface}/activation-status"),
|
|
)
|
|
});
|
|
let wifi_connect_result = wifi_primary.as_ref().and_then(|iface| {
|
|
read_trimmed(
|
|
runtime,
|
|
&format!("/scheme/wifictl/ifaces/{iface}/connect-result"),
|
|
)
|
|
});
|
|
let wifi_disconnect_result = wifi_primary.as_ref().and_then(|iface| {
|
|
read_trimmed(
|
|
runtime,
|
|
&format!("/scheme/wifictl/ifaces/{iface}/disconnect-result"),
|
|
)
|
|
});
|
|
let wifi_scan_results = wifi_primary
|
|
.as_ref()
|
|
.and_then(|iface| {
|
|
runtime
|
|
.read_to_string(&format!("/scheme/wifictl/ifaces/{iface}/scan-results"))
|
|
.map(|value| {
|
|
value
|
|
.lines()
|
|
.map(str::trim)
|
|
.filter(|line| !line.is_empty())
|
|
.map(str::to_string)
|
|
.collect::<Vec<_>>()
|
|
})
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let bluetooth_adapters = runtime
|
|
.read_dir_names("/scheme/btctl/adapters")
|
|
.or_else(|| {
|
|
runtime
|
|
.read_to_string("/scheme/btctl/adapters")
|
|
.map(|value| {
|
|
value
|
|
.lines()
|
|
.map(str::trim)
|
|
.filter(|line| !line.is_empty())
|
|
.map(str::to_string)
|
|
.collect::<Vec<_>>()
|
|
})
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let bluetooth_transport_runtime_visible =
|
|
read_trimmed(runtime, "/var/run/redbear-btusb/status")
|
|
.map(|_| bluetooth_status_is_fresh(runtime, "/var/run/redbear-btusb/status"))
|
|
.unwrap_or(false);
|
|
|
|
let bluetooth_transport_state = if bluetooth_transport_runtime_visible {
|
|
ProbeState::Active
|
|
} else if runtime.exists("/usr/bin/redbear-btusb") {
|
|
ProbeState::Present
|
|
} else {
|
|
ProbeState::Absent
|
|
};
|
|
|
|
let bluetooth_control_state = if runtime.exists("/scheme/btctl") {
|
|
if bluetooth_adapters.is_empty() {
|
|
ProbeState::Active
|
|
} else {
|
|
ProbeState::Functional
|
|
}
|
|
} else if runtime.exists("/usr/bin/redbear-btctl") {
|
|
ProbeState::Present
|
|
} else {
|
|
ProbeState::Absent
|
|
};
|
|
|
|
let bluetooth_primary = bluetooth_adapters.first().cloned();
|
|
let bluetooth_transport_status = if bluetooth_transport_runtime_visible {
|
|
read_compact(runtime, "/var/run/redbear-btusb/status")
|
|
} else {
|
|
bluetooth_primary
|
|
.as_ref()
|
|
.and_then(|adapter| {
|
|
read_compact(
|
|
runtime,
|
|
&format!("/scheme/btctl/adapters/{adapter}/transport-status"),
|
|
)
|
|
})
|
|
.or_else(|| {
|
|
runtime.exists("/usr/bin/redbear-btusb").then_some(
|
|
"transport=usb startup=explicit runtime_visibility=installed-only".to_string(),
|
|
)
|
|
})
|
|
};
|
|
let bluetooth_adapter_status = bluetooth_primary.as_ref().and_then(|adapter| {
|
|
read_compact(runtime, &format!("/scheme/btctl/adapters/{adapter}/status"))
|
|
});
|
|
let bluetooth_scan_results = bluetooth_primary
|
|
.as_ref()
|
|
.and_then(|adapter| {
|
|
runtime
|
|
.read_to_string(&format!("/scheme/btctl/adapters/{adapter}/scan-results"))
|
|
.map(|value| {
|
|
value
|
|
.lines()
|
|
.map(str::trim)
|
|
.filter(|line| !line.is_empty())
|
|
.map(str::to_string)
|
|
.collect::<Vec<_>>()
|
|
})
|
|
})
|
|
.unwrap_or_default();
|
|
let bluetooth_connection_state = bluetooth_primary.as_ref().and_then(|adapter| {
|
|
read_compact(
|
|
runtime,
|
|
&format!("/scheme/btctl/adapters/{adapter}/connection-state"),
|
|
)
|
|
});
|
|
let bluetooth_connect_result = bluetooth_primary.as_ref().and_then(|adapter| {
|
|
read_trimmed(
|
|
runtime,
|
|
&format!("/scheme/btctl/adapters/{adapter}/connect-result"),
|
|
)
|
|
});
|
|
let bluetooth_disconnect_result = bluetooth_primary.as_ref().and_then(|adapter| {
|
|
read_trimmed(
|
|
runtime,
|
|
&format!("/scheme/btctl/adapters/{adapter}/disconnect-result"),
|
|
)
|
|
});
|
|
let bluetooth_read_char_result = bluetooth_primary.as_ref().and_then(|adapter| {
|
|
read_trimmed(
|
|
runtime,
|
|
&format!("/scheme/btctl/adapters/{adapter}/read-char-result"),
|
|
)
|
|
});
|
|
let bluetooth_bond_store_path = bluetooth_primary.as_ref().and_then(|adapter| {
|
|
read_trimmed(
|
|
runtime,
|
|
&format!("/scheme/btctl/adapters/{adapter}/bond-store-path"),
|
|
)
|
|
.or_else(|| {
|
|
let path = format!("/var/lib/bluetooth/{adapter}/bonds");
|
|
runtime.exists(&path).then_some(path)
|
|
})
|
|
});
|
|
let bluetooth_bond_count = bluetooth_primary.as_ref().and_then(|adapter| {
|
|
read_compact(
|
|
runtime,
|
|
&format!("/scheme/btctl/adapters/{adapter}/bond-count"),
|
|
)
|
|
.and_then(|content| parse_compact_key_value(&content, "bond_count"))
|
|
.and_then(|value| value.parse::<usize>().ok())
|
|
.or_else(|| {
|
|
let path = format!("/var/lib/bluetooth/{adapter}/bonds");
|
|
runtime.read_dir_names(&path).map(|entries| entries.len())
|
|
})
|
|
});
|
|
|
|
let state = if runtime.exists("/scheme/netcfg") {
|
|
if address.is_some() {
|
|
ProbeState::Functional
|
|
} else {
|
|
ProbeState::Active
|
|
}
|
|
} else if runtime.exists("/usr/bin/smolnetd") {
|
|
ProbeState::Present
|
|
} else {
|
|
ProbeState::Absent
|
|
};
|
|
|
|
NetworkReport {
|
|
state,
|
|
connected: address.is_some(),
|
|
interface: preferred_interface,
|
|
mac,
|
|
address,
|
|
dns,
|
|
default_route,
|
|
active_profile,
|
|
network_schemes,
|
|
wifi_control_state,
|
|
wifi_interfaces,
|
|
wifi_firmware_status,
|
|
wifi_transport_status,
|
|
wifi_transport_init_status,
|
|
wifi_activation_status,
|
|
wifi_connect_result,
|
|
wifi_disconnect_result,
|
|
wifi_scan_results,
|
|
claim_limit: "Connected means the local stack exposes a configured address; this does not prove external reachability.",
|
|
bluetooth_transport_state,
|
|
bluetooth_control_state,
|
|
bluetooth_adapters,
|
|
bluetooth_transport_status,
|
|
bluetooth_adapter_status,
|
|
bluetooth_scan_results,
|
|
bluetooth_connection_state,
|
|
bluetooth_connect_result,
|
|
bluetooth_disconnect_result,
|
|
bluetooth_read_char_result,
|
|
bluetooth_bond_store_path,
|
|
bluetooth_bond_count,
|
|
bluetooth_claim_limit: "Runtime-visible Bluetooth control evidence means the explicit-startup btusb/btctl surfaces, stub bond files, bounded connect/disconnect metadata, and one experimental battery-sensor Battery Level read result can be observed; this 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.",
|
|
}
|
|
}
|
|
|
|
fn active_profile_interface(runtime: &Runtime, profile: &str) -> Option<String> {
|
|
let content = runtime.read_to_string(&format!("/etc/netctl/{profile}"))?;
|
|
content.lines().find_map(|line| {
|
|
let line = line.trim();
|
|
let (key, value) = line.split_once('=')?;
|
|
(key.trim() == "Interface").then(|| parse_profile_scalar(value.trim()))
|
|
})
|
|
}
|
|
|
|
fn parse_profile_scalar(value: &str) -> String {
|
|
value
|
|
.trim()
|
|
.trim_start_matches('(')
|
|
.trim_end_matches(')')
|
|
.trim()
|
|
.trim_matches('"')
|
|
.trim_matches('\'')
|
|
.to_string()
|
|
}
|
|
|
|
fn collect_hardware(runtime: &Runtime, network: &NetworkReport) -> HardwareReport {
|
|
let pci_entries = runtime.read_dir_names("/scheme/pci").unwrap_or_default();
|
|
let pci_devices = pci_entries
|
|
.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")
|
|
.unwrap_or_default()
|
|
.into_iter()
|
|
.filter(|name| name.starts_with("usb."))
|
|
.count();
|
|
|
|
let drm_cards = runtime
|
|
.read_dir_names("/scheme/drm")
|
|
.unwrap_or_default()
|
|
.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");
|
|
|
|
for entry in &pci_entries {
|
|
let config_path = format!("/scheme/pci/{entry}/config");
|
|
let Some(bytes) = read_prefix_bytes(runtime, &config_path, 64) else {
|
|
continue;
|
|
};
|
|
if bytes.len() < 64 {
|
|
continue;
|
|
}
|
|
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"));
|
|
|
|
let virtio_net_present = runtime
|
|
.read_dir_names("/scheme/pci")
|
|
.unwrap_or_default()
|
|
.into_iter()
|
|
.any(|entry| {
|
|
let config_path = format!("/scheme/pci/{entry}/config");
|
|
let Some(bytes) = read_prefix_bytes(runtime, &config_path, 4) else {
|
|
return false;
|
|
};
|
|
if bytes.len() < 4 {
|
|
return false;
|
|
}
|
|
let vendor = u16::from_le_bytes([bytes[0], bytes[1]]);
|
|
let device = u16::from_le_bytes([bytes[2], bytes[3]]);
|
|
vendor == VIRTIO_NET_VENDOR_ID && device == VIRTIO_NET_DEVICE_ID
|
|
})
|
|
|| network
|
|
.network_schemes
|
|
.iter()
|
|
.any(|name| name.contains("virtio") || name.contains("eth0"));
|
|
|
|
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();
|
|
|
|
let entries = match runtime.read_dir_names("/etc/quirks.d") {
|
|
Some(entries) => entries,
|
|
None => {
|
|
return QuirksReport {
|
|
files_loaded,
|
|
load_errors: vec!["quirks directory not found".to_string()],
|
|
};
|
|
}
|
|
};
|
|
|
|
for name in entries.into_iter().filter(|name| name.ends_with(".toml")) {
|
|
let path = format!("/etc/quirks.d/{name}");
|
|
match runtime.read_to_string(&path) {
|
|
Some(content) => match parse_quirk_toml(&name, &content) {
|
|
Ok(file_quirks) => files_loaded.push(file_quirks),
|
|
Err(err) => load_errors.push(format!("{name}: {err}")),
|
|
},
|
|
None => load_errors.push(format!("{name}: read error")),
|
|
}
|
|
}
|
|
|
|
QuirksReport {
|
|
files_loaded,
|
|
load_errors,
|
|
}
|
|
}
|
|
|
|
#[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()
|
|
.map_err(|err| format!("parse error: {err}"))?;
|
|
let table = document
|
|
.as_table()
|
|
.ok_or_else(|| "top-level document is not a table".to_string())?;
|
|
|
|
let mut file_quirks = QuirkFile {
|
|
name: name.to_string(),
|
|
pci_quirks: Vec::new(),
|
|
usb_quirks: Vec::new(),
|
|
dmi_quirk_count: 0,
|
|
};
|
|
|
|
if let Some(entries) = table.get("pci_quirk").and_then(Value::as_array) {
|
|
for entry in entries {
|
|
if let Some(quirk) = parse_pci_quirk(entry) {
|
|
file_quirks.pci_quirks.push(quirk);
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(entries) = table.get("usb_quirk").and_then(Value::as_array) {
|
|
for entry in entries {
|
|
if let Some(quirk) = parse_usb_quirk(entry) {
|
|
file_quirks.usb_quirks.push(quirk);
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(entries) = table.get("dmi_system_quirk").and_then(Value::as_array) {
|
|
file_quirks.dmi_quirk_count = entries.len();
|
|
}
|
|
|
|
Ok(file_quirks)
|
|
}
|
|
|
|
fn parse_pci_quirk(entry: &Value) -> Option<QuirkEntry> {
|
|
let table = entry.as_table()?;
|
|
let vendor = table.get("vendor").and_then(|value| format_hex(value, 4))?;
|
|
let device = table.get("device").and_then(|value| format_hex(value, 4));
|
|
let class = table.get("class").and_then(|value| format_hex(value, 6));
|
|
let flags = table.get("flags").and_then(parse_string_array)?;
|
|
let description = table
|
|
.get("description")
|
|
.and_then(Value::as_str)
|
|
.map(str::to_string);
|
|
|
|
Some(QuirkEntry {
|
|
vendor,
|
|
device,
|
|
class,
|
|
flags,
|
|
description,
|
|
})
|
|
}
|
|
|
|
fn parse_usb_quirk(entry: &Value) -> Option<UsbQuirkEntry> {
|
|
let table = entry.as_table()?;
|
|
let vendor = table.get("vendor").and_then(|value| format_hex(value, 4))?;
|
|
let product = table.get("product").and_then(|value| format_hex(value, 4));
|
|
let flags = table.get("flags").and_then(parse_string_array)?;
|
|
|
|
Some(UsbQuirkEntry {
|
|
vendor,
|
|
product,
|
|
flags,
|
|
})
|
|
}
|
|
|
|
fn format_hex(value: &Value, width: usize) -> Option<String> {
|
|
let raw = u64::try_from(value.as_integer()?).ok()?;
|
|
Some(format!("0x{raw:0width$X}", width = width))
|
|
}
|
|
|
|
fn parse_string_array(value: &Value) -> Option<Vec<String>> {
|
|
value.as_array().map(|items| {
|
|
items
|
|
.iter()
|
|
.filter_map(Value::as_str)
|
|
.map(str::to_string)
|
|
.collect()
|
|
})
|
|
}
|
|
|
|
fn inspect_integration<'a>(
|
|
runtime: &Runtime,
|
|
network: &NetworkReport,
|
|
hardware: &HardwareReport,
|
|
check: &'a IntegrationCheck,
|
|
) -> IntegrationStatus<'a> {
|
|
let artifact_present = check.artifact_path.map(|path| runtime.exists(path));
|
|
let control_present = check
|
|
.control_path
|
|
.map(|path| control_surface_present(runtime, check, path));
|
|
|
|
let mut evidence = Vec::new();
|
|
|
|
if let Some(path) = check.artifact_path {
|
|
evidence.push(format!(
|
|
"artifact {} {}",
|
|
path,
|
|
if artifact_present == Some(true) {
|
|
"present"
|
|
} else {
|
|
"missing"
|
|
}
|
|
));
|
|
}
|
|
if let Some(path) = check.control_path {
|
|
evidence.push(format!(
|
|
"control {} {}",
|
|
path,
|
|
if control_present == Some(true) {
|
|
"present"
|
|
} else {
|
|
"missing"
|
|
}
|
|
));
|
|
}
|
|
|
|
let state = if let Some(probe) = check.functional_probe {
|
|
match probe(runtime, network, hardware, check) {
|
|
Some(message) => {
|
|
evidence.push(message);
|
|
if artifact_present == Some(false) {
|
|
ProbeState::Active
|
|
} else {
|
|
ProbeState::Functional
|
|
}
|
|
}
|
|
None => {
|
|
if check.name == "redbear-btctl" && control_present == Some(true) {
|
|
ProbeState::Active
|
|
} else {
|
|
derive_state(artifact_present, control_present)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
derive_state(artifact_present, control_present)
|
|
};
|
|
|
|
IntegrationStatus {
|
|
check,
|
|
state,
|
|
artifact_present,
|
|
control_present,
|
|
evidence,
|
|
claim_limit: check.note,
|
|
}
|
|
}
|
|
|
|
fn derive_state(artifact_present: Option<bool>, control_present: Option<bool>) -> ProbeState {
|
|
if control_present == Some(true) && artifact_present != Some(false) {
|
|
ProbeState::Active
|
|
} else if artifact_present == Some(true) {
|
|
ProbeState::Present
|
|
} else if artifact_present.is_none() && control_present.is_none() {
|
|
ProbeState::Unobservable
|
|
} else {
|
|
ProbeState::Absent
|
|
}
|
|
}
|
|
|
|
fn control_surface_present(runtime: &Runtime, check: &IntegrationCheck, path: &str) -> bool {
|
|
if check.name == "redbear-btusb" {
|
|
runtime.exists(path) && bluetooth_status_is_fresh(runtime, path)
|
|
} else {
|
|
runtime.exists(path)
|
|
}
|
|
}
|
|
|
|
fn output_mode_flag(mode: OutputMode) -> &'static str {
|
|
match mode {
|
|
OutputMode::Table => "table output",
|
|
OutputMode::Json => "--json",
|
|
OutputMode::Test => "--test",
|
|
OutputMode::Quirks => "--quirks",
|
|
OutputMode::Probe => "--probe",
|
|
OutputMode::Boot => "--boot",
|
|
OutputMode::Device => "--device",
|
|
OutputMode::Health => "--health",
|
|
OutputMode::Help => "--help",
|
|
}
|
|
}
|
|
|
|
fn set_output_mode(
|
|
mode: &mut OutputMode,
|
|
next_mode: OutputMode,
|
|
next_flag: &str,
|
|
) -> Result<(), String> {
|
|
if matches!(*mode, OutputMode::Table | OutputMode::Help) {
|
|
*mode = next_mode;
|
|
Ok(())
|
|
} else {
|
|
Err(format!(
|
|
"cannot combine {next_flag} with {}",
|
|
output_mode_flag(*mode)
|
|
))
|
|
}
|
|
}
|
|
|
|
fn parse_args<I>(args: I) -> Result<Options, String>
|
|
where
|
|
I: IntoIterator<Item = String>,
|
|
{
|
|
let mut mode = OutputMode::Table;
|
|
let mut verbose = false;
|
|
|
|
let mut device = None;
|
|
let mut args = args.into_iter().skip(1).peekable();
|
|
|
|
while let Some(arg) = args.next() {
|
|
match arg.as_str() {
|
|
"-v" | "--verbose" => verbose = true,
|
|
"-i" | "--interactive" => set_output_mode(&mut mode, OutputMode::Tui, "-i")?,
|
|
"--json" => set_output_mode(&mut mode, OutputMode::Json, "--json")?,
|
|
"--test" => set_output_mode(&mut mode, OutputMode::Test, "--test")?,
|
|
"--quirks" => set_output_mode(&mut mode, OutputMode::Quirks, "--quirks")?,
|
|
"--probe" => set_output_mode(&mut mode, OutputMode::Probe, "--probe")?,
|
|
"--boot" => set_output_mode(&mut mode, OutputMode::Boot, "--boot")?,
|
|
"--health" => set_output_mode(&mut mode, OutputMode::Health, "--health")?,
|
|
"--device" => {
|
|
set_output_mode(&mut mode, OutputMode::Device, "--device")?;
|
|
let Some(value) = args.next() else {
|
|
return Err("--device requires a PCI address".to_string());
|
|
};
|
|
if value.starts_with('-') {
|
|
return Err("--device requires a PCI address".to_string());
|
|
}
|
|
device = Some(value);
|
|
}
|
|
"-h" | "--help" => mode = OutputMode::Help,
|
|
_ => return Err(format!("unknown argument: {arg}")),
|
|
}
|
|
}
|
|
|
|
Ok(Options {
|
|
mode,
|
|
verbose,
|
|
device,
|
|
})
|
|
}
|
|
|
|
fn print_table(report: &Report<'_>, verbose: bool) {
|
|
println!("Red Bear OS Runtime Integration Report");
|
|
println!("{DIVIDER}");
|
|
println!();
|
|
|
|
print_section_header("Identity");
|
|
println!(
|
|
" OS: {}",
|
|
display_or_unknown(report.identity.pretty_name.as_deref())
|
|
);
|
|
println!(
|
|
" Version: {}",
|
|
display_or_unknown(report.identity.version_id.as_deref())
|
|
);
|
|
println!(
|
|
" Hostname: {}",
|
|
display_or_unknown(report.identity.hostname.as_deref())
|
|
);
|
|
println!();
|
|
|
|
print_section_header("Networking");
|
|
println!(
|
|
" Stack: {} {}",
|
|
colorize(
|
|
state_marker(report.network.state),
|
|
state_color(report.network.state)
|
|
),
|
|
state_label(report.network.state)
|
|
);
|
|
println!(
|
|
" Connected: {}",
|
|
if report.network.connected {
|
|
"yes"
|
|
} else {
|
|
"no"
|
|
}
|
|
);
|
|
println!(
|
|
" Interface: {}",
|
|
display_or_unknown(report.network.interface.as_deref())
|
|
);
|
|
println!(
|
|
" MAC: {}",
|
|
display_or_unknown(report.network.mac.as_deref())
|
|
);
|
|
println!(
|
|
" Address: {}",
|
|
display_or_unknown(report.network.address.as_deref())
|
|
);
|
|
println!(
|
|
" DNS: {}",
|
|
display_or_unknown(report.network.dns.as_deref())
|
|
);
|
|
println!(
|
|
" Default route: {}",
|
|
display_or_unknown(report.network.default_route.as_deref())
|
|
);
|
|
println!(
|
|
" Active profile: {}",
|
|
display_or_unknown(report.network.active_profile.as_deref())
|
|
);
|
|
println!(
|
|
" Network schemes: {}",
|
|
if report.network.network_schemes.is_empty() {
|
|
"none".to_string()
|
|
} else {
|
|
report.network.network_schemes.join(", ")
|
|
}
|
|
);
|
|
println!(
|
|
" Wi-Fi control: {}{}{}",
|
|
state_marker(report.network.wifi_control_state),
|
|
state_color(report.network.wifi_control_state),
|
|
state_label(report.network.wifi_control_state)
|
|
);
|
|
println!(
|
|
" Wi-Fi interfaces: {}",
|
|
if report.network.wifi_interfaces.is_empty() {
|
|
"none".to_string()
|
|
} else {
|
|
report.network.wifi_interfaces.join(", ")
|
|
}
|
|
);
|
|
println!(
|
|
" Wi-Fi firmware: {}",
|
|
display_or_unknown(report.network.wifi_firmware_status.as_deref())
|
|
);
|
|
println!(
|
|
" Wi-Fi transport: {}",
|
|
display_or_unknown(report.network.wifi_transport_status.as_deref())
|
|
);
|
|
println!(
|
|
" Wi-Fi transport init: {}",
|
|
display_or_unknown(report.network.wifi_transport_init_status.as_deref())
|
|
);
|
|
println!(
|
|
" Wi-Fi activation: {}",
|
|
display_or_unknown(report.network.wifi_activation_status.as_deref())
|
|
);
|
|
println!(
|
|
" Wi-Fi connect result: {}",
|
|
display_or_unknown(report.network.wifi_connect_result.as_deref())
|
|
);
|
|
println!(
|
|
" Wi-Fi disconnect result: {}",
|
|
display_or_unknown(report.network.wifi_disconnect_result.as_deref())
|
|
);
|
|
println!(
|
|
" Wi-Fi scan results: {}",
|
|
if report.network.wifi_scan_results.is_empty() {
|
|
"none".to_string()
|
|
} else {
|
|
report.network.wifi_scan_results.join(", ")
|
|
}
|
|
);
|
|
println!(
|
|
" Bluetooth transport: {}{}{}",
|
|
state_marker(report.network.bluetooth_transport_state),
|
|
state_color(report.network.bluetooth_transport_state),
|
|
state_label(report.network.bluetooth_transport_state)
|
|
);
|
|
println!(
|
|
" Bluetooth control: {}{}{}",
|
|
state_marker(report.network.bluetooth_control_state),
|
|
state_color(report.network.bluetooth_control_state),
|
|
state_label(report.network.bluetooth_control_state)
|
|
);
|
|
println!(
|
|
" Bluetooth adapters: {}",
|
|
if report.network.bluetooth_adapters.is_empty() {
|
|
"none".to_string()
|
|
} else {
|
|
report.network.bluetooth_adapters.join(", ")
|
|
}
|
|
);
|
|
println!(
|
|
" Bluetooth status: {}",
|
|
display_or_unknown(report.network.bluetooth_adapter_status.as_deref())
|
|
);
|
|
println!(
|
|
" Bluetooth transport status: {}",
|
|
display_or_unknown(report.network.bluetooth_transport_status.as_deref())
|
|
);
|
|
println!(
|
|
" Bluetooth scan results: {}",
|
|
if report.network.bluetooth_scan_results.is_empty() {
|
|
"none".to_string()
|
|
} else {
|
|
report.network.bluetooth_scan_results.join(", ")
|
|
}
|
|
);
|
|
println!(
|
|
" Bluetooth connection state: {}",
|
|
display_or_unknown(report.network.bluetooth_connection_state.as_deref())
|
|
);
|
|
println!(
|
|
" Bluetooth connect result: {}",
|
|
display_or_unknown(report.network.bluetooth_connect_result.as_deref())
|
|
);
|
|
println!(
|
|
" Bluetooth disconnect result: {}",
|
|
display_or_unknown(report.network.bluetooth_disconnect_result.as_deref())
|
|
);
|
|
println!(
|
|
" Bluetooth experimental BLE read: {}",
|
|
display_or_unknown(report.network.bluetooth_read_char_result.as_deref())
|
|
);
|
|
println!(
|
|
" Bluetooth bond store: {}",
|
|
report
|
|
.network
|
|
.bluetooth_bond_store_path
|
|
.as_deref()
|
|
.unwrap_or("none")
|
|
);
|
|
println!(
|
|
" Bluetooth bond count: {}",
|
|
report
|
|
.network
|
|
.bluetooth_bond_count
|
|
.map(|count| count.to_string())
|
|
.unwrap_or_else(|| "unknown".to_string())
|
|
);
|
|
if verbose {
|
|
println!(" Network note: {}", report.network.claim_limit);
|
|
println!(" Bluetooth note: {}", report.network.bluetooth_claim_limit);
|
|
}
|
|
println!();
|
|
|
|
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 {
|
|
"yes"
|
|
} else {
|
|
"no"
|
|
}
|
|
);
|
|
println!(
|
|
" VirtIO NIC seen: {}",
|
|
if report.hardware.virtio_net_present {
|
|
"yes"
|
|
} else {
|
|
"no"
|
|
}
|
|
);
|
|
println!();
|
|
|
|
print_section_header("Integrations");
|
|
for integration in &report.integrations {
|
|
println!(
|
|
" {} {:<18} [{}] {}",
|
|
colorize(
|
|
state_marker(integration.state),
|
|
state_color(integration.state)
|
|
),
|
|
integration.check.name,
|
|
integration.check.category,
|
|
state_label(integration.state)
|
|
);
|
|
println!(" {}", integration.check.description);
|
|
println!(" Test: {}", integration.check.test_hint);
|
|
if verbose {
|
|
for line in &integration.evidence {
|
|
println!(" Evidence: {line}");
|
|
}
|
|
println!(" Claim limit: {}", integration.claim_limit);
|
|
}
|
|
println!();
|
|
}
|
|
|
|
println!("{DIVIDER}");
|
|
println!(
|
|
"functional={} active={} present={} absent={} total={}",
|
|
count_state(&report.integrations, ProbeState::Functional),
|
|
count_state(&report.integrations, ProbeState::Active),
|
|
count_state(&report.integrations, ProbeState::Present),
|
|
count_state(&report.integrations, ProbeState::Absent),
|
|
report.integrations.len()
|
|
);
|
|
}
|
|
|
|
fn print_quirks(report: &QuirksReport, verbose: bool) {
|
|
println!("Red Bear OS Hardware Quirks Configuration");
|
|
println!("{DIVIDER}");
|
|
println!();
|
|
|
|
if report.files_loaded.is_empty()
|
|
&& report
|
|
.load_errors
|
|
.iter()
|
|
.any(|error| error.contains("quirks directory not found"))
|
|
{
|
|
println!(" Quirks directory: {}not found{}", YELLOW, RESET);
|
|
return;
|
|
}
|
|
|
|
println!(" Files loaded: {}", report.files_loaded.len());
|
|
|
|
for file in &report.files_loaded {
|
|
println!();
|
|
print_section_header(&format!("Quirks: {}", file.name));
|
|
|
|
if file.pci_quirks.is_empty() && file.usb_quirks.is_empty() && file.dmi_quirk_count == 0 {
|
|
println!(" (no entries)");
|
|
continue;
|
|
}
|
|
|
|
println!(
|
|
" {:<4} {:<8} {:<18} {}",
|
|
"Type", "Vendor", "Match", "Flags"
|
|
);
|
|
for quirk in &file.pci_quirks {
|
|
let selector = quirk
|
|
.device
|
|
.as_deref()
|
|
.map(|device| format!("device={device}"))
|
|
.or_else(|| quirk.class.as_deref().map(|class| format!("class={class}")))
|
|
.unwrap_or_else(|| "match=unknown".to_string());
|
|
println!(
|
|
" {:<4} {:<8} {:<18} {}",
|
|
"PCI",
|
|
quirk.vendor,
|
|
selector,
|
|
quirk.flags.join(", ")
|
|
);
|
|
if verbose && let Some(description) = &quirk.description {
|
|
println!(" Description: {description}");
|
|
}
|
|
}
|
|
|
|
for quirk in &file.usb_quirks {
|
|
let selector = quirk
|
|
.product
|
|
.as_deref()
|
|
.map(|p| format!("product={p}"))
|
|
.unwrap_or_else(|| "match=any".to_string());
|
|
println!(
|
|
" {:<4} {:<8} {:<18} {}",
|
|
"USB",
|
|
quirk.vendor,
|
|
selector,
|
|
quirk.flags.join(", ")
|
|
);
|
|
}
|
|
|
|
if file.dmi_quirk_count > 0 {
|
|
println!(
|
|
" DMI: {} system rule(s) configured (runtime application uses acpid /scheme/acpi/dmi)",
|
|
file.dmi_quirk_count
|
|
);
|
|
}
|
|
}
|
|
|
|
for error in &report.load_errors {
|
|
println!(" {}Error: {}{}", RED, error, RESET);
|
|
}
|
|
}
|
|
|
|
fn print_tests(report: &Report<'_>, verbose: bool) {
|
|
println!("Red Bear OS Runtime Test Hints");
|
|
println!("{DIVIDER}");
|
|
println!();
|
|
println!(" redbear-info --json");
|
|
println!(" redbear-info --verbose");
|
|
println!(" redbear-info --boot");
|
|
println!(" redbear-info --health");
|
|
println!(" netctl status");
|
|
println!(" lspci");
|
|
println!(" lsusb");
|
|
println!();
|
|
|
|
for integration in report
|
|
.integrations
|
|
.iter()
|
|
.filter(|integration| integration.state != ProbeState::Absent)
|
|
{
|
|
println!(
|
|
" {:<18} {}",
|
|
integration.check.name, integration.check.test_hint
|
|
);
|
|
if verbose {
|
|
println!(" {}", integration.claim_limit);
|
|
}
|
|
}
|
|
|
|
println!();
|
|
println!("Network interpretation: {}", report.network.claim_limit);
|
|
}
|
|
|
|
fn print_json(report: &Report<'_>) {
|
|
let mut out = String::new();
|
|
out.push_str("{\n");
|
|
|
|
out.push_str(" \"identity\": {\n");
|
|
push_json_field(
|
|
&mut out,
|
|
"pretty_name",
|
|
report.identity.pretty_name.as_deref(),
|
|
true,
|
|
4,
|
|
);
|
|
push_json_field(
|
|
&mut out,
|
|
"version_id",
|
|
report.identity.version_id.as_deref(),
|
|
true,
|
|
4,
|
|
);
|
|
push_json_field(
|
|
&mut out,
|
|
"hostname",
|
|
report.identity.hostname.as_deref(),
|
|
false,
|
|
4,
|
|
);
|
|
out.push_str(" },\n");
|
|
|
|
out.push_str(" \"network\": {\n");
|
|
push_json_string_field(
|
|
&mut out,
|
|
"state",
|
|
state_label(report.network.state),
|
|
true,
|
|
4,
|
|
);
|
|
push_json_bool_field(&mut out, "connected", report.network.connected, true, 4);
|
|
push_json_field(
|
|
&mut out,
|
|
"interface",
|
|
report.network.interface.as_deref(),
|
|
true,
|
|
4,
|
|
);
|
|
push_json_field(&mut out, "mac", report.network.mac.as_deref(), true, 4);
|
|
push_json_field(
|
|
&mut out,
|
|
"address",
|
|
report.network.address.as_deref(),
|
|
true,
|
|
4,
|
|
);
|
|
push_json_field(&mut out, "dns", report.network.dns.as_deref(), true, 4);
|
|
push_json_field(
|
|
&mut out,
|
|
"default_route",
|
|
report.network.default_route.as_deref(),
|
|
true,
|
|
4,
|
|
);
|
|
push_json_field(
|
|
&mut out,
|
|
"active_profile",
|
|
report.network.active_profile.as_deref(),
|
|
true,
|
|
4,
|
|
);
|
|
push_json_string_array_field(
|
|
&mut out,
|
|
"network_schemes",
|
|
&report.network.network_schemes,
|
|
true,
|
|
4,
|
|
);
|
|
push_json_string_field(
|
|
&mut out,
|
|
"wifi_control_state",
|
|
state_label(report.network.wifi_control_state),
|
|
true,
|
|
4,
|
|
);
|
|
push_json_string_array_field(
|
|
&mut out,
|
|
"wifi_interfaces",
|
|
&report.network.wifi_interfaces,
|
|
true,
|
|
4,
|
|
);
|
|
push_json_field(
|
|
&mut out,
|
|
"wifi_firmware_status",
|
|
report.network.wifi_firmware_status.as_deref(),
|
|
true,
|
|
4,
|
|
);
|
|
push_json_field(
|
|
&mut out,
|
|
"wifi_transport_status",
|
|
report.network.wifi_transport_status.as_deref(),
|
|
true,
|
|
4,
|
|
);
|
|
push_json_field(
|
|
&mut out,
|
|
"wifi_transport_init_status",
|
|
report.network.wifi_transport_init_status.as_deref(),
|
|
true,
|
|
4,
|
|
);
|
|
push_json_field(
|
|
&mut out,
|
|
"wifi_activation_status",
|
|
report.network.wifi_activation_status.as_deref(),
|
|
true,
|
|
4,
|
|
);
|
|
push_json_field(
|
|
&mut out,
|
|
"wifi_connect_result",
|
|
report.network.wifi_connect_result.as_deref(),
|
|
true,
|
|
4,
|
|
);
|
|
push_json_field(
|
|
&mut out,
|
|
"wifi_disconnect_result",
|
|
report.network.wifi_disconnect_result.as_deref(),
|
|
true,
|
|
4,
|
|
);
|
|
push_json_string_array_field(
|
|
&mut out,
|
|
"wifi_scan_results",
|
|
&report.network.wifi_scan_results,
|
|
true,
|
|
4,
|
|
);
|
|
push_json_string_field(&mut out, "claim_limit", report.network.claim_limit, true, 4);
|
|
push_json_string_field(
|
|
&mut out,
|
|
"bluetooth_transport_state",
|
|
state_label(report.network.bluetooth_transport_state),
|
|
true,
|
|
4,
|
|
);
|
|
push_json_string_field(
|
|
&mut out,
|
|
"bluetooth_control_state",
|
|
state_label(report.network.bluetooth_control_state),
|
|
true,
|
|
4,
|
|
);
|
|
push_json_string_array_field(
|
|
&mut out,
|
|
"bluetooth_adapters",
|
|
&report.network.bluetooth_adapters,
|
|
true,
|
|
4,
|
|
);
|
|
push_json_field(
|
|
&mut out,
|
|
"bluetooth_transport_status",
|
|
report.network.bluetooth_transport_status.as_deref(),
|
|
true,
|
|
4,
|
|
);
|
|
push_json_field(
|
|
&mut out,
|
|
"bluetooth_adapter_status",
|
|
report.network.bluetooth_adapter_status.as_deref(),
|
|
true,
|
|
4,
|
|
);
|
|
push_json_string_array_field(
|
|
&mut out,
|
|
"bluetooth_scan_results",
|
|
&report.network.bluetooth_scan_results,
|
|
true,
|
|
4,
|
|
);
|
|
push_json_field(
|
|
&mut out,
|
|
"bluetooth_connection_state",
|
|
report.network.bluetooth_connection_state.as_deref(),
|
|
true,
|
|
4,
|
|
);
|
|
push_json_field(
|
|
&mut out,
|
|
"bluetooth_connect_result",
|
|
report.network.bluetooth_connect_result.as_deref(),
|
|
true,
|
|
4,
|
|
);
|
|
push_json_field(
|
|
&mut out,
|
|
"bluetooth_disconnect_result",
|
|
report.network.bluetooth_disconnect_result.as_deref(),
|
|
true,
|
|
4,
|
|
);
|
|
push_json_field(
|
|
&mut out,
|
|
"bluetooth_read_char_result",
|
|
report.network.bluetooth_read_char_result.as_deref(),
|
|
true,
|
|
4,
|
|
);
|
|
push_json_field(
|
|
&mut out,
|
|
"bluetooth_bond_store_path",
|
|
report.network.bluetooth_bond_store_path.as_deref(),
|
|
true,
|
|
4,
|
|
);
|
|
push_json_optional_number_field(
|
|
&mut out,
|
|
"bluetooth_bond_count",
|
|
report.network.bluetooth_bond_count,
|
|
true,
|
|
4,
|
|
);
|
|
push_json_string_field(
|
|
&mut out,
|
|
"bluetooth_claim_limit",
|
|
report.network.bluetooth_claim_limit,
|
|
false,
|
|
4,
|
|
);
|
|
out.push_str(" },\n");
|
|
|
|
out.push_str(" \"hardware\": {\n");
|
|
push_json_number_field(
|
|
&mut out,
|
|
"pci_devices",
|
|
report.hardware.pci_devices,
|
|
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",
|
|
report.hardware.usb_controllers,
|
|
true,
|
|
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",
|
|
report.hardware.rtl8125_present,
|
|
true,
|
|
4,
|
|
);
|
|
push_json_bool_field(
|
|
&mut out,
|
|
"virtio_net_present",
|
|
report.hardware.virtio_net_present,
|
|
false,
|
|
4,
|
|
);
|
|
out.push_str(" },\n");
|
|
|
|
out.push_str(" \"integrations\": [\n");
|
|
for (index, integration) in report.integrations.iter().enumerate() {
|
|
out.push_str(" {\n");
|
|
push_json_string_field(&mut out, "name", integration.check.name, true, 6);
|
|
push_json_string_field(&mut out, "category", integration.check.category, true, 6);
|
|
push_json_string_field(
|
|
&mut out,
|
|
"description",
|
|
integration.check.description,
|
|
true,
|
|
6,
|
|
);
|
|
push_json_string_field(&mut out, "state", state_label(integration.state), true, 6);
|
|
push_json_optional_bool_field(
|
|
&mut out,
|
|
"artifact_present",
|
|
integration.artifact_present,
|
|
true,
|
|
6,
|
|
);
|
|
push_json_optional_bool_field(
|
|
&mut out,
|
|
"control_present",
|
|
integration.control_present,
|
|
true,
|
|
6,
|
|
);
|
|
push_json_string_field(&mut out, "test_hint", integration.check.test_hint, true, 6);
|
|
push_json_string_array_field(&mut out, "evidence", &integration.evidence, true, 6);
|
|
push_json_string_field(&mut out, "claim_limit", integration.claim_limit, false, 6);
|
|
out.push_str(" }");
|
|
if index + 1 != report.integrations.len() {
|
|
out.push(',');
|
|
}
|
|
out.push('\n');
|
|
}
|
|
out.push_str(" ]\n");
|
|
out.push('}');
|
|
println!("{out}");
|
|
}
|
|
|
|
fn print_help() {
|
|
println!(
|
|
"Usage: redbear-info [--verbose|-v] [--json|--test|--quirks|--probe|--boot|--device <pci>|--health]"
|
|
);
|
|
println!();
|
|
println!("Passive runtime integration report for Red Bear OS.");
|
|
println!();
|
|
println!("This tool distinguishes:");
|
|
println!(" present artifact or config exists");
|
|
println!(" active live runtime surface exists");
|
|
println!(
|
|
" functional read-only runtime probe succeeded (table/test output; --probe mode uses PRESENT/ABSENT)"
|
|
);
|
|
println!();
|
|
println!("Connected means the local networking stack has a configured address.");
|
|
println!("It does not prove internet reachability.");
|
|
println!();
|
|
println!("Options:");
|
|
println!(" -v, --verbose Show evidence and claim limits");
|
|
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!(" --boot Show device-init boot timeline");
|
|
println!(" --device Show per-device runtime status (example: pci/00:02:0)");
|
|
println!(" --health Show subsystem health dashboard");
|
|
println!(" -h, --help Show this help message");
|
|
}
|
|
|
|
fn print_section_header(title: &str) {
|
|
println!("{}", colorize(title, BLUE));
|
|
}
|
|
|
|
fn state_marker(state: ProbeState) -> &'static str {
|
|
match state {
|
|
ProbeState::Functional => "●",
|
|
ProbeState::Active => "◉",
|
|
ProbeState::Present => "◌",
|
|
ProbeState::Absent => "○",
|
|
ProbeState::Unobservable => "?",
|
|
}
|
|
}
|
|
|
|
fn state_label(state: ProbeState) -> &'static str {
|
|
match state {
|
|
ProbeState::Functional => "functional",
|
|
ProbeState::Active => "active",
|
|
ProbeState::Present => "present",
|
|
ProbeState::Absent => "absent",
|
|
ProbeState::Unobservable => "unobservable",
|
|
}
|
|
}
|
|
|
|
fn state_color(state: ProbeState) -> &'static str {
|
|
match state {
|
|
ProbeState::Functional => GREEN,
|
|
ProbeState::Active => YELLOW,
|
|
ProbeState::Present => BLUE,
|
|
ProbeState::Absent => RED,
|
|
ProbeState::Unobservable => YELLOW,
|
|
}
|
|
}
|
|
|
|
fn colorize(text: &str, color: &str) -> String {
|
|
format!("{color}{text}{RESET}")
|
|
}
|
|
|
|
fn count_state(items: &[IntegrationStatus<'_>], state: ProbeState) -> usize {
|
|
items.iter().filter(|item| item.state == state).count()
|
|
}
|
|
|
|
fn display_or_unknown(value: Option<&str>) -> &str {
|
|
value.unwrap_or("unknown")
|
|
}
|
|
|
|
fn parse_os_release_value(contents: &str, key: &str) -> Option<String> {
|
|
contents.lines().find_map(|line| {
|
|
let (found_key, raw) = line.split_once('=')?;
|
|
if found_key == key {
|
|
Some(raw.trim().trim_matches('"').to_string())
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
}
|
|
|
|
fn read_trimmed(runtime: &Runtime, path: &str) -> Option<String> {
|
|
runtime
|
|
.read_to_string(path)
|
|
.map(|value| value.trim().to_string())
|
|
.filter(|value| !value.is_empty())
|
|
}
|
|
|
|
fn read_compact(runtime: &Runtime, path: &str) -> Option<String> {
|
|
runtime
|
|
.read_to_string(path)
|
|
.map(|value| {
|
|
value
|
|
.lines()
|
|
.map(str::trim)
|
|
.filter(|line| !line.is_empty())
|
|
.collect::<Vec<_>>()
|
|
.join(" ")
|
|
})
|
|
.filter(|value| !value.is_empty())
|
|
}
|
|
|
|
fn parse_compact_key_value(content: &str, key: &str) -> Option<String> {
|
|
let prefix = format!("{key}=");
|
|
content
|
|
.split_whitespace()
|
|
.find_map(|token| token.strip_prefix(&prefix).map(str::to_string))
|
|
}
|
|
|
|
fn current_epoch_seconds() -> u64 {
|
|
SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_secs()
|
|
}
|
|
|
|
fn bluetooth_status_is_fresh(runtime: &Runtime, path: &str) -> bool {
|
|
runtime
|
|
.read_to_string(path)
|
|
.and_then(|value| {
|
|
value.lines().find_map(|line| {
|
|
line.trim()
|
|
.strip_prefix("updated_at_epoch=")
|
|
.and_then(|raw| raw.parse::<u64>().ok())
|
|
})
|
|
})
|
|
.map(|timestamp| {
|
|
current_epoch_seconds().saturating_sub(timestamp) <= BLUETOOTH_STATUS_FRESHNESS_SECS
|
|
})
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
fn read_prefix_bytes(runtime: &Runtime, path: &str, max_len: usize) -> Option<Vec<u8>> {
|
|
let mut file = fs::File::open(runtime.resolve(path)).ok()?;
|
|
let mut bytes = vec![0_u8; max_len];
|
|
let read = file.read(&mut bytes).ok()?;
|
|
bytes.truncate(read);
|
|
Some(bytes)
|
|
}
|
|
|
|
fn read_all_bytes(runtime: &Runtime, path: &str) -> Option<Vec<u8>> {
|
|
fs::read(runtime.resolve(path)).ok()
|
|
}
|
|
|
|
impl BootProbeStatus {
|
|
fn from_str(value: &str) -> Option<Self> {
|
|
match value {
|
|
"bound" => Some(Self::Bound),
|
|
"deferred" => Some(Self::Deferred),
|
|
"failed" => Some(Self::Failed),
|
|
"skipped" => Some(Self::Skipped),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn as_str(self) -> &'static str {
|
|
match self {
|
|
Self::Bound => "bound",
|
|
Self::Deferred => "deferred",
|
|
Self::Failed => "failed",
|
|
Self::Skipped => "skipped",
|
|
}
|
|
}
|
|
}
|
|
|
|
fn collect_boot_timeline(runtime: &Runtime) -> Result<Vec<BootTimelineEntry>, String> {
|
|
let content = runtime
|
|
.read_to_string(BOOT_TIMELINE_PATH)
|
|
.ok_or_else(|| format!("boot timeline not found at {BOOT_TIMELINE_PATH}"))?;
|
|
|
|
let mut entries = Vec::new();
|
|
|
|
for (index, line) in content.lines().enumerate() {
|
|
let line = line.trim();
|
|
if line.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
let value: JsonValue = serde_json::from_str(line)
|
|
.map_err(|err| format!("invalid boot timeline entry {}: {err}", index + 1))?;
|
|
let ts = value
|
|
.get("ts")
|
|
.and_then(JsonValue::as_u64)
|
|
.map(u128::from)
|
|
.ok_or_else(|| format!("boot timeline entry {} is missing ts", index + 1))?;
|
|
let event_name = value
|
|
.get("event")
|
|
.and_then(JsonValue::as_str)
|
|
.ok_or_else(|| format!("boot timeline entry {} is missing event", index + 1))?;
|
|
|
|
let event = match event_name {
|
|
"bus_enumerated" => BootTimelineEvent::BusEnumerated {
|
|
bus: value
|
|
.get("bus")
|
|
.and_then(JsonValue::as_str)
|
|
.ok_or_else(|| format!("boot timeline entry {} is missing bus", index + 1))?
|
|
.to_string(),
|
|
count: value
|
|
.get("count")
|
|
.and_then(JsonValue::as_u64)
|
|
.map(|value| value as usize)
|
|
.ok_or_else(|| format!("boot timeline entry {} is missing count", index + 1))?,
|
|
},
|
|
"probe" => BootTimelineEvent::Probe {
|
|
device: value
|
|
.get("device")
|
|
.and_then(JsonValue::as_str)
|
|
.ok_or_else(|| format!("boot timeline entry {} is missing device", index + 1))?
|
|
.to_string(),
|
|
driver: value
|
|
.get("driver")
|
|
.and_then(JsonValue::as_str)
|
|
.ok_or_else(|| format!("boot timeline entry {} is missing driver", index + 1))?
|
|
.to_string(),
|
|
status: value
|
|
.get("status")
|
|
.and_then(JsonValue::as_str)
|
|
.and_then(BootProbeStatus::from_str)
|
|
.ok_or_else(|| {
|
|
format!("boot timeline entry {} is missing status", index + 1)
|
|
})?,
|
|
},
|
|
_ => continue,
|
|
};
|
|
|
|
entries.push(BootTimelineEntry { ts, event });
|
|
}
|
|
|
|
entries.sort_by_key(|entry| entry.ts);
|
|
Ok(entries)
|
|
}
|
|
|
|
fn print_boot_timeline(entries: &[BootTimelineEntry]) {
|
|
println!("Boot Timeline:");
|
|
|
|
if entries.is_empty() {
|
|
println!(" no timeline data recorded");
|
|
return;
|
|
}
|
|
|
|
let first_ts = entries.first().map(|entry| entry.ts).unwrap_or(0);
|
|
let mut bound = 0usize;
|
|
let mut deferred = 0usize;
|
|
let mut failed = 0usize;
|
|
|
|
for entry in entries {
|
|
let delta = entry.ts.saturating_sub(first_ts);
|
|
match &entry.event {
|
|
BootTimelineEvent::BusEnumerated { bus, count } => {
|
|
println!("[{delta:>4}ms] bus {bus} enumerated {count} device(s)");
|
|
}
|
|
BootTimelineEvent::Probe {
|
|
device,
|
|
driver,
|
|
status,
|
|
} => {
|
|
match status {
|
|
BootProbeStatus::Bound => bound += 1,
|
|
BootProbeStatus::Deferred => deferred += 1,
|
|
BootProbeStatus::Failed => failed += 1,
|
|
BootProbeStatus::Skipped => {}
|
|
}
|
|
println!(
|
|
"[{delta:>4}ms] probed {} -> {} ({})",
|
|
format_timeline_device(device),
|
|
driver,
|
|
status.as_str()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
println!("Total: {bound} bound, {deferred} deferred, {failed} failed");
|
|
}
|
|
|
|
fn collect_device_status(runtime: &Runtime, requested: &str) -> Result<DeviceStatusReport, String> {
|
|
let location = parse_requested_pci_location(requested)
|
|
.ok_or_else(|| format!("invalid PCI device selector: {requested}"))?;
|
|
let scheme_entry = format_scheme_pci_entry(&location);
|
|
let config_path = format!("/scheme/pci/{scheme_entry}/config");
|
|
let bytes = read_all_bytes(runtime, &config_path)
|
|
.ok_or_else(|| format!("device config not found at {config_path}"))?;
|
|
if bytes.len() < 64 {
|
|
return Err(format!("device config at {config_path} is too short"));
|
|
}
|
|
|
|
let info = parse_device_info_from_config_space(location, &bytes)
|
|
.ok_or_else(|| format!("failed to parse PCI config at {config_path}"))?;
|
|
let params = collect_driver_params(runtime, &scheme_entry);
|
|
let driver = params
|
|
.iter()
|
|
.find_map(|(key, value)| (key == "driver" && !value.is_empty()).then(|| value.clone()));
|
|
let parameters = params
|
|
.into_iter()
|
|
.filter(|(key, _)| key != "driver")
|
|
.collect::<Vec<_>>();
|
|
let runtime_device = format_runtime_pci_location(&location);
|
|
let irq_mode = collect_irq_runtime_reports(runtime)
|
|
.into_iter()
|
|
.find(|report| report.device == runtime_device)
|
|
.map(|report| format_irq_mode(&report.mode))
|
|
.unwrap_or_else(|| format_irq_mode(info.interrupt_support().as_str()));
|
|
let status = latest_boot_status_for_device(runtime, &location);
|
|
|
|
Ok(DeviceStatusReport {
|
|
selector: format_device_selector(&location),
|
|
status,
|
|
vendor_id: info.vendor_id,
|
|
device_id: info.device_id,
|
|
class_code: info.class_code,
|
|
class_name: pci_class_name(info.class_code),
|
|
irq_mode,
|
|
driver,
|
|
parameters,
|
|
})
|
|
}
|
|
|
|
fn print_device_status(report: &DeviceStatusReport) {
|
|
println!("Device: {}", report.selector);
|
|
println!(
|
|
" Status: {}",
|
|
report
|
|
.status
|
|
.map(BootProbeStatus::as_str)
|
|
.unwrap_or("unknown")
|
|
);
|
|
println!(
|
|
" Vendor: 0x{:04x} Device: 0x{:04x}",
|
|
report.vendor_id, report.device_id
|
|
);
|
|
println!(
|
|
" Class: 0x{:02x} ({})",
|
|
report.class_code, report.class_name
|
|
);
|
|
println!(" IRQ mode: {}", report.irq_mode);
|
|
println!(
|
|
" Driver: {}",
|
|
report.driver.as_deref().unwrap_or("unknown")
|
|
);
|
|
println!(
|
|
" Parameters: {}",
|
|
if report.parameters.is_empty() {
|
|
"none".to_string()
|
|
} else {
|
|
report
|
|
.parameters
|
|
.iter()
|
|
.map(|(key, value)| format!("{key}={value}"))
|
|
.collect::<Vec<_>>()
|
|
.join(" ")
|
|
}
|
|
);
|
|
}
|
|
|
|
fn collect_health_items(runtime: &Runtime, report: &Report<'_>) -> Vec<HealthItem> {
|
|
let mut items = Vec::new();
|
|
|
|
items.push(HealthItem {
|
|
label: "PCI scheme",
|
|
state: if runtime.is_dir("/scheme/pci") {
|
|
HealthState::Healthy
|
|
} else {
|
|
HealthState::Critical
|
|
},
|
|
detail: "/scheme/pci".to_string(),
|
|
});
|
|
|
|
let usb_scheme_present = runtime.is_dir("/scheme/usb") || report.hardware.usb_controllers > 0;
|
|
items.push(HealthItem {
|
|
label: "USB scheme",
|
|
state: if usb_scheme_present {
|
|
HealthState::Healthy
|
|
} else if runtime.exists("/usr/lib/drivers/xhcid") {
|
|
HealthState::Warning
|
|
} else {
|
|
HealthState::Critical
|
|
},
|
|
detail: if runtime.is_dir("/scheme/usb") {
|
|
"/scheme/usb".to_string()
|
|
} else if report.hardware.usb_controllers > 0 {
|
|
format!(
|
|
"{} usb.* controller scheme(s)",
|
|
report.hardware.usb_controllers
|
|
)
|
|
} else {
|
|
"no USB runtime surface".to_string()
|
|
},
|
|
});
|
|
|
|
items.push(HealthItem {
|
|
label: "ACPI scheme",
|
|
state: if runtime.is_dir("/scheme/acpi") {
|
|
HealthState::Healthy
|
|
} else {
|
|
HealthState::Critical
|
|
},
|
|
detail: "/scheme/acpi".to_string(),
|
|
});
|
|
|
|
items.push(HealthItem {
|
|
label: "DRM scheme",
|
|
state: if runtime.is_dir("/scheme/drm") && report.hardware.drm_cards > 0 {
|
|
HealthState::Healthy
|
|
} else if runtime.is_dir("/scheme/drm") {
|
|
HealthState::Warning
|
|
} else {
|
|
HealthState::Critical
|
|
},
|
|
detail: if report.hardware.drm_cards > 0 {
|
|
format!("/scheme/drm ({} card(s))", report.hardware.drm_cards)
|
|
} else {
|
|
"/scheme/drm".to_string()
|
|
},
|
|
});
|
|
|
|
items.push(HealthItem {
|
|
label: "Network",
|
|
state: if report.network.connected {
|
|
HealthState::Healthy
|
|
} else if matches!(
|
|
report.network.state,
|
|
ProbeState::Active | ProbeState::Functional
|
|
) {
|
|
HealthState::Warning
|
|
} else {
|
|
HealthState::Critical
|
|
},
|
|
detail: if report.network.connected {
|
|
match report.network.interface.as_deref() {
|
|
Some(interface) => format!("configured address on {interface}"),
|
|
None => "configured address present".to_string(),
|
|
}
|
|
} else if runtime.exists("/scheme/netcfg") {
|
|
"network stack visible, not configured".to_string()
|
|
} else {
|
|
"dhcpd/netcfg not active".to_string()
|
|
},
|
|
});
|
|
|
|
items.push(HealthItem {
|
|
label: "Wi-Fi",
|
|
state: if !report.network.wifi_interfaces.is_empty() {
|
|
HealthState::Healthy
|
|
} else if report.network.wifi_control_state != ProbeState::Absent {
|
|
HealthState::Warning
|
|
} else {
|
|
HealthState::Critical
|
|
},
|
|
detail: if !report.network.wifi_interfaces.is_empty() {
|
|
report.network.wifi_interfaces.join(", ")
|
|
} else if report.network.wifi_control_state != ProbeState::Absent {
|
|
"no adapter".to_string()
|
|
} else {
|
|
"not running".to_string()
|
|
},
|
|
});
|
|
|
|
items.push(HealthItem {
|
|
label: "Bluetooth",
|
|
state: if !report.network.bluetooth_adapters.is_empty() {
|
|
HealthState::Healthy
|
|
} else if report.network.bluetooth_transport_state != ProbeState::Absent
|
|
|| report.network.bluetooth_control_state != ProbeState::Absent
|
|
{
|
|
HealthState::Warning
|
|
} else {
|
|
HealthState::Critical
|
|
},
|
|
detail: if !report.network.bluetooth_adapters.is_empty() {
|
|
report.network.bluetooth_adapters.join(", ")
|
|
} else if report.network.bluetooth_transport_state != ProbeState::Absent
|
|
|| report.network.bluetooth_control_state != ProbeState::Absent
|
|
{
|
|
"no adapter".to_string()
|
|
} else {
|
|
"not running".to_string()
|
|
},
|
|
});
|
|
|
|
items
|
|
}
|
|
|
|
fn print_health_dashboard(items: &[HealthItem]) {
|
|
println!("Health Dashboard:");
|
|
for item in items {
|
|
println!(
|
|
"[{}] {:<12} ({})",
|
|
health_marker(item.state),
|
|
item.label,
|
|
item.detail
|
|
);
|
|
}
|
|
}
|
|
|
|
fn health_marker(state: HealthState) -> &'static str {
|
|
match state {
|
|
HealthState::Healthy => "✓",
|
|
HealthState::Warning => "⚠",
|
|
HealthState::Critical => "✗",
|
|
}
|
|
}
|
|
|
|
fn format_timeline_device(device: &str) -> String {
|
|
parse_requested_pci_location(device)
|
|
.map(|location| {
|
|
format!(
|
|
"{:02x}.{:02x}.{}",
|
|
location.bus, location.device, location.function
|
|
)
|
|
})
|
|
.unwrap_or_else(|| {
|
|
device
|
|
.trim_start_matches("pci/")
|
|
.replace(':', ".")
|
|
.replace("..", ".")
|
|
})
|
|
}
|
|
|
|
fn latest_boot_status_for_device(
|
|
runtime: &Runtime,
|
|
location: &redox_driver_sys::pci::PciLocation,
|
|
) -> Option<BootProbeStatus> {
|
|
collect_boot_timeline(runtime)
|
|
.ok()?
|
|
.iter()
|
|
.rev()
|
|
.find_map(|entry| match &entry.event {
|
|
BootTimelineEvent::Probe { device, status, .. }
|
|
if parse_requested_pci_location(device).as_ref() == Some(location) =>
|
|
{
|
|
Some(*status)
|
|
}
|
|
_ => None,
|
|
})
|
|
}
|
|
|
|
fn parse_requested_pci_location(value: &str) -> Option<redox_driver_sys::pci::PciLocation> {
|
|
let raw = value.trim().trim_start_matches("pci/");
|
|
|
|
if raw.contains("--") {
|
|
return parse_scheme_pci_location(raw);
|
|
}
|
|
|
|
if raw.matches(':').count() == 2 && raw.contains('.') {
|
|
let (segment, rest) = raw.split_once(':')?;
|
|
let (bus, rest) = rest.split_once(':')?;
|
|
let (device, function) = rest.split_once('.')?;
|
|
return Some(redox_driver_sys::pci::PciLocation {
|
|
segment: u16::from_str_radix(segment, 16).ok()?,
|
|
bus: u8::from_str_radix(bus, 16).ok()?,
|
|
device: u8::from_str_radix(device, 16).ok()?,
|
|
function: u8::from_str_radix(function, 16).ok()?,
|
|
});
|
|
}
|
|
|
|
let normalized = raw.replace('.', ":");
|
|
let mut parts = normalized.split(':');
|
|
let bus = u8::from_str_radix(parts.next()?, 16).ok()?;
|
|
let device = u8::from_str_radix(parts.next()?, 16).ok()?;
|
|
let function = u8::from_str_radix(parts.next()?, 16).ok()?;
|
|
if parts.next().is_some() {
|
|
return None;
|
|
}
|
|
|
|
Some(redox_driver_sys::pci::PciLocation {
|
|
segment: 0,
|
|
bus,
|
|
device,
|
|
function,
|
|
})
|
|
}
|
|
|
|
fn format_scheme_pci_entry(location: &redox_driver_sys::pci::PciLocation) -> String {
|
|
format!(
|
|
"{:04x}--{:02x}--{:02x}.{}",
|
|
location.segment, location.bus, location.device, location.function
|
|
)
|
|
}
|
|
|
|
fn format_runtime_pci_location(location: &redox_driver_sys::pci::PciLocation) -> String {
|
|
format!(
|
|
"{:04x}:{:02x}:{:02x}.{}",
|
|
location.segment, location.bus, location.device, location.function
|
|
)
|
|
}
|
|
|
|
fn format_device_selector(location: &redox_driver_sys::pci::PciLocation) -> String {
|
|
format!(
|
|
"pci/{:02x}:{:02x}:{}",
|
|
location.bus, location.device, location.function
|
|
)
|
|
}
|
|
|
|
fn collect_driver_params(runtime: &Runtime, scheme_entry: &str) -> Vec<(String, String)> {
|
|
let dir = format!("{DRIVER_PARAMS_ROOT}/{scheme_entry}");
|
|
runtime
|
|
.read_dir_names(&dir)
|
|
.unwrap_or_default()
|
|
.into_iter()
|
|
.filter_map(|name| {
|
|
read_trimmed(runtime, &format!("{dir}/{name}")).map(|value| (name, value))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn format_irq_mode(value: &str) -> String {
|
|
match value {
|
|
"msix" => "msi-x".to_string(),
|
|
"msi_or_msix" => "msi/msi-x".to_string(),
|
|
other => other.to_string(),
|
|
}
|
|
}
|
|
|
|
fn pci_class_name(class_code: u8) -> &'static str {
|
|
match class_code {
|
|
0x00 => "Unclassified",
|
|
0x01 => "Mass storage controller",
|
|
0x02 => "Network controller",
|
|
0x03 => "Display controller",
|
|
0x04 => "Multimedia controller",
|
|
0x05 => "Memory controller",
|
|
0x06 => "Bridge device",
|
|
0x07 => "Communication controller",
|
|
0x08 => "System peripheral",
|
|
0x09 => "Input device controller",
|
|
0x0a => "Docking station",
|
|
0x0b => "Processor",
|
|
0x0c => "Serial bus controller",
|
|
0x0d => "Wireless controller",
|
|
0x0e => "Intelligent controller",
|
|
0x0f => "Satellite communication controller",
|
|
0x10 => "Encryption controller",
|
|
0x11 => "Signal processing controller",
|
|
0x12 => "Processing accelerator",
|
|
0x13 => "Non-essential instrumentation",
|
|
_ => "Unknown class",
|
|
}
|
|
}
|
|
|
|
fn parse_default_route(routes: &str) -> Option<String> {
|
|
routes.lines().find_map(|line| {
|
|
let trimmed = line.trim();
|
|
if trimmed.starts_with("default via ") || trimmed.starts_with("0.0.0.0/0 via ") {
|
|
Some(trimmed.to_string())
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
}
|
|
|
|
fn probe_directory_readable(
|
|
runtime: &Runtime,
|
|
_network: &NetworkReport,
|
|
_hardware: &HardwareReport,
|
|
check: &IntegrationCheck,
|
|
) -> Option<String> {
|
|
let path = check.control_path?;
|
|
let entries = runtime.read_dir_names(path)?;
|
|
Some(format!(
|
|
"read-only probe succeeded on {path} ({} entrie(s))",
|
|
entries.len()
|
|
))
|
|
}
|
|
|
|
fn probe_usb_surface(
|
|
runtime: &Runtime,
|
|
_network: &NetworkReport,
|
|
hardware: &HardwareReport,
|
|
_check: &IntegrationCheck,
|
|
) -> Option<String> {
|
|
(hardware.usb_controllers > 0 || runtime.is_dir("/scheme")).then(|| {
|
|
format!(
|
|
"usb scheme scan sees {} controller(s)",
|
|
hardware.usb_controllers
|
|
)
|
|
})
|
|
}
|
|
|
|
fn probe_icmp_surface(
|
|
runtime: &Runtime,
|
|
_network: &NetworkReport,
|
|
_hardware: &HardwareReport,
|
|
_check: &IntegrationCheck,
|
|
) -> Option<String> {
|
|
runtime
|
|
.read_dir_names("/scheme")
|
|
.filter(|entries| entries.iter().any(|name| name == "icmp"))
|
|
.map(|_| "icmp scheme is present for probe/error reporting".to_string())
|
|
}
|
|
|
|
fn probe_netctl_surface(
|
|
runtime: &Runtime,
|
|
network: &NetworkReport,
|
|
_hardware: &HardwareReport,
|
|
_check: &IntegrationCheck,
|
|
) -> Option<String> {
|
|
let profiles = runtime.read_dir_names("/etc/netctl")?;
|
|
let profile_count = profiles
|
|
.iter()
|
|
.filter(|name| *name != "active" && !name.starts_with('.'))
|
|
.count();
|
|
Some(match &network.active_profile {
|
|
Some(active) => format!(
|
|
"{} profile(s) visible, active profile {}",
|
|
profile_count, active
|
|
),
|
|
None => format!(
|
|
"{} profile(s) visible, no active profile recorded",
|
|
profile_count
|
|
),
|
|
})
|
|
}
|
|
|
|
fn probe_smolnetd_surface(
|
|
runtime: &Runtime,
|
|
network: &NetworkReport,
|
|
_hardware: &HardwareReport,
|
|
_check: &IntegrationCheck,
|
|
) -> Option<String> {
|
|
let _ = runtime.read_dir_names("/scheme/netcfg")?;
|
|
Some(match &network.address {
|
|
Some(address) => format!("netcfg readable, active address {address}"),
|
|
None => "netcfg readable, no configured address".to_string(),
|
|
})
|
|
}
|
|
|
|
fn probe_named_scheme(runtime: &Runtime, scheme_name: &str) -> Option<String> {
|
|
let names = runtime.read_dir_names("/scheme")?;
|
|
names
|
|
.into_iter()
|
|
.any(|name| name == scheme_name)
|
|
.then(|| format!("scheme {scheme_name} is registered in /scheme"))
|
|
}
|
|
|
|
fn probe_firmware_scheme(
|
|
runtime: &Runtime,
|
|
_network: &NetworkReport,
|
|
_hardware: &HardwareReport,
|
|
_check: &IntegrationCheck,
|
|
) -> Option<String> {
|
|
probe_named_scheme(runtime, "firmware")
|
|
}
|
|
|
|
fn probe_udev_scheme(
|
|
runtime: &Runtime,
|
|
_network: &NetworkReport,
|
|
_hardware: &HardwareReport,
|
|
_check: &IntegrationCheck,
|
|
) -> Option<String> {
|
|
probe_named_scheme(runtime, "udev")
|
|
}
|
|
|
|
fn probe_evdev_scheme(
|
|
runtime: &Runtime,
|
|
_network: &NetworkReport,
|
|
_hardware: &HardwareReport,
|
|
_check: &IntegrationCheck,
|
|
) -> Option<String> {
|
|
probe_named_scheme(runtime, "evdev")
|
|
}
|
|
|
|
fn probe_wifictl_surface(
|
|
runtime: &Runtime,
|
|
_network: &NetworkReport,
|
|
_hardware: &HardwareReport,
|
|
_check: &IntegrationCheck,
|
|
) -> Option<String> {
|
|
let ifaces = runtime
|
|
.read_to_string("/scheme/wifictl/ifaces")
|
|
.or_else(|| {
|
|
runtime
|
|
.read_dir_names("/scheme/wifictl/ifaces")
|
|
.map(|entries| entries.join("\n"))
|
|
})?;
|
|
Some(format!(
|
|
"wifictl interface surface visible ({})",
|
|
ifaces.trim()
|
|
))
|
|
}
|
|
|
|
fn probe_btctl_surface(
|
|
runtime: &Runtime,
|
|
_network: &NetworkReport,
|
|
_hardware: &HardwareReport,
|
|
_check: &IntegrationCheck,
|
|
) -> Option<String> {
|
|
let adapters = runtime
|
|
.read_to_string("/scheme/btctl/adapters")
|
|
.or_else(|| {
|
|
runtime
|
|
.read_dir_names("/scheme/btctl/adapters")
|
|
.map(|entries| entries.join("\n"))
|
|
})?;
|
|
let primary_adapter = adapters
|
|
.lines()
|
|
.map(str::trim)
|
|
.find(|line| !line.is_empty())?;
|
|
let read_result = read_trimmed(
|
|
runtime,
|
|
&format!("/scheme/btctl/adapters/{primary_adapter}/read-char-result"),
|
|
)?;
|
|
read_result.starts_with("read_char_result=").then(|| {
|
|
format!(
|
|
"btctl adapter surface visible ({}) with bounded read result",
|
|
primary_adapter
|
|
)
|
|
})
|
|
}
|
|
|
|
fn probe_btusb_surface(
|
|
runtime: &Runtime,
|
|
_network: &NetworkReport,
|
|
_hardware: &HardwareReport,
|
|
check: &IntegrationCheck,
|
|
) -> Option<String> {
|
|
let path = check.control_path?;
|
|
bluetooth_status_is_fresh(runtime, path)
|
|
.then(|| format!("btusb status file is fresh at {path}"))
|
|
}
|
|
|
|
fn probe_pci_surface(
|
|
runtime: &Runtime,
|
|
_network: &NetworkReport,
|
|
_hardware: &HardwareReport,
|
|
_check: &IntegrationCheck,
|
|
) -> Option<String> {
|
|
let entries = runtime.read_dir_names("/scheme/pci")?;
|
|
Some(format!("pci surface visible ({} entries)", entries.len()))
|
|
}
|
|
|
|
fn probe_iommu_scheme(
|
|
runtime: &Runtime,
|
|
_network: &NetworkReport,
|
|
_hardware: &HardwareReport,
|
|
_check: &IntegrationCheck,
|
|
) -> Option<String> {
|
|
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,
|
|
_hardware: &HardwareReport,
|
|
_check: &IntegrationCheck,
|
|
) -> Option<String> {
|
|
(runtime.exists("/scheme/serio/0") && runtime.exists("/scheme/serio/1"))
|
|
.then(|| "serio keyboard and mouse nodes are visible for PS/2 proof".to_string())
|
|
}
|
|
|
|
fn probe_time_surface(
|
|
runtime: &Runtime,
|
|
_network: &NetworkReport,
|
|
_hardware: &HardwareReport,
|
|
_check: &IntegrationCheck,
|
|
) -> Option<String> {
|
|
runtime
|
|
.exists("/scheme/time/4")
|
|
.then(|| "monotonic time scheme node is visible for runtime proof".to_string())
|
|
}
|
|
|
|
fn probe_rtl8125_path(
|
|
_runtime: &Runtime,
|
|
network: &NetworkReport,
|
|
hardware: &HardwareReport,
|
|
_check: &IntegrationCheck,
|
|
) -> Option<String> {
|
|
if !hardware.rtl8125_present {
|
|
return None;
|
|
}
|
|
|
|
Some(
|
|
if network
|
|
.network_schemes
|
|
.iter()
|
|
.any(|name| name.contains("rtl8125"))
|
|
{
|
|
"RTL8125 PCI device seen and network.rtl8125 scheme visible".to_string()
|
|
} else if network.connected {
|
|
"RTL8125 PCI device seen and native network stack reports a configured address"
|
|
.to_string()
|
|
} else {
|
|
"RTL8125 PCI device seen through /scheme/pci".to_string()
|
|
},
|
|
)
|
|
}
|
|
|
|
fn probe_virtio_net_path(
|
|
_runtime: &Runtime,
|
|
network: &NetworkReport,
|
|
hardware: &HardwareReport,
|
|
_check: &IntegrationCheck,
|
|
) -> Option<String> {
|
|
if !hardware.virtio_net_present {
|
|
return None;
|
|
}
|
|
|
|
Some(
|
|
if network
|
|
.network_schemes
|
|
.iter()
|
|
.any(|name| name.contains("virtio") || name.contains("eth0"))
|
|
{
|
|
"VirtIO NIC seen and network scheme surface is visible".to_string()
|
|
} else if network.connected {
|
|
"VirtIO NIC seen and native network stack reports a configured address".to_string()
|
|
} else {
|
|
"VirtIO NIC seen through /scheme/pci".to_string()
|
|
},
|
|
)
|
|
}
|
|
|
|
fn push_json_string(output: &mut String, value: &str) {
|
|
output.push('"');
|
|
for ch in value.chars() {
|
|
match ch {
|
|
'"' => output.push_str("\\\""),
|
|
'\\' => output.push_str("\\\\"),
|
|
'\n' => output.push_str("\\n"),
|
|
'\r' => output.push_str("\\r"),
|
|
'\t' => output.push_str("\\t"),
|
|
_ => output.push(ch),
|
|
}
|
|
}
|
|
output.push('"');
|
|
}
|
|
|
|
fn push_json_indent(output: &mut String, indent: usize) {
|
|
for _ in 0..indent {
|
|
output.push(' ');
|
|
}
|
|
}
|
|
|
|
fn push_json_string_field(
|
|
output: &mut String,
|
|
key: &str,
|
|
value: &str,
|
|
trailing_comma: bool,
|
|
indent: usize,
|
|
) {
|
|
push_json_indent(output, indent);
|
|
push_json_string(output, key);
|
|
output.push_str(": ");
|
|
push_json_string(output, value);
|
|
if trailing_comma {
|
|
output.push(',');
|
|
}
|
|
output.push('\n');
|
|
}
|
|
|
|
fn push_json_field(
|
|
output: &mut String,
|
|
key: &str,
|
|
value: Option<&str>,
|
|
trailing_comma: bool,
|
|
indent: usize,
|
|
) {
|
|
push_json_indent(output, indent);
|
|
push_json_string(output, key);
|
|
output.push_str(": ");
|
|
match value {
|
|
Some(value) => push_json_string(output, value),
|
|
None => output.push_str("null"),
|
|
}
|
|
if trailing_comma {
|
|
output.push(',');
|
|
}
|
|
output.push('\n');
|
|
}
|
|
|
|
fn push_json_bool_field(
|
|
output: &mut String,
|
|
key: &str,
|
|
value: bool,
|
|
trailing_comma: bool,
|
|
indent: usize,
|
|
) {
|
|
push_json_indent(output, indent);
|
|
push_json_string(output, key);
|
|
output.push_str(": ");
|
|
output.push_str(if value { "true" } else { "false" });
|
|
if trailing_comma {
|
|
output.push(',');
|
|
}
|
|
output.push('\n');
|
|
}
|
|
|
|
fn push_json_optional_bool_field(
|
|
output: &mut String,
|
|
key: &str,
|
|
value: Option<bool>,
|
|
trailing_comma: bool,
|
|
indent: usize,
|
|
) {
|
|
push_json_indent(output, indent);
|
|
push_json_string(output, key);
|
|
output.push_str(": ");
|
|
match value {
|
|
Some(value) => output.push_str(if value { "true" } else { "false" }),
|
|
None => output.push_str("null"),
|
|
}
|
|
if trailing_comma {
|
|
output.push(',');
|
|
}
|
|
output.push('\n');
|
|
}
|
|
|
|
fn push_json_number_field(
|
|
output: &mut String,
|
|
key: &str,
|
|
value: usize,
|
|
trailing_comma: bool,
|
|
indent: usize,
|
|
) {
|
|
push_json_indent(output, indent);
|
|
push_json_string(output, key);
|
|
output.push_str(": ");
|
|
output.push_str(&value.to_string());
|
|
if trailing_comma {
|
|
output.push(',');
|
|
}
|
|
output.push('\n');
|
|
}
|
|
|
|
fn push_json_optional_number_field(
|
|
output: &mut String,
|
|
key: &str,
|
|
value: Option<usize>,
|
|
trailing_comma: bool,
|
|
indent: usize,
|
|
) {
|
|
push_json_indent(output, indent);
|
|
push_json_string(output, key);
|
|
output.push_str(": ");
|
|
match value {
|
|
Some(value) => output.push_str(&value.to_string()),
|
|
None => output.push_str("null"),
|
|
}
|
|
if trailing_comma {
|
|
output.push(',');
|
|
}
|
|
output.push('\n');
|
|
}
|
|
|
|
fn push_json_string_array_field(
|
|
output: &mut String,
|
|
key: &str,
|
|
values: &[String],
|
|
trailing_comma: bool,
|
|
indent: usize,
|
|
) {
|
|
push_json_indent(output, indent);
|
|
push_json_string(output, key);
|
|
output.push_str(": [");
|
|
for (index, value) in values.iter().enumerate() {
|
|
if index > 0 {
|
|
output.push_str(", ");
|
|
}
|
|
push_json_string(output, value);
|
|
}
|
|
output.push(']');
|
|
if trailing_comma {
|
|
output.push(',');
|
|
}
|
|
output.push('\n');
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn temp_root() -> PathBuf {
|
|
let unique = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_nanos();
|
|
let path = env::temp_dir().join(format!("redbear-info-test-{}-{}", process::id(), unique));
|
|
fs::create_dir_all(&path).unwrap();
|
|
path
|
|
}
|
|
|
|
fn write_file(root: &Path, path: &str, content: &str) {
|
|
let full = root.join(path.trim_start_matches('/'));
|
|
if let Some(parent) = full.parent() {
|
|
fs::create_dir_all(parent).unwrap();
|
|
}
|
|
fs::write(full, content).unwrap();
|
|
}
|
|
|
|
fn create_dir(root: &Path, path: &str) {
|
|
fs::create_dir_all(root.join(path.trim_start_matches('/'))).unwrap();
|
|
}
|
|
|
|
fn integration_state<'a>(report: &'a Report<'a>, name: &str) -> ProbeState {
|
|
report
|
|
.integrations
|
|
.iter()
|
|
.find(|integration| integration.check.name == name)
|
|
.map(|integration| integration.state)
|
|
.unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn network_report_uses_live_netcfg_surfaces() {
|
|
let root = temp_root();
|
|
write_file(
|
|
&root,
|
|
"/usr/lib/os-release",
|
|
"PRETTY_NAME=\"Red Bear OS\"\nVERSION_ID=\"0.1.0\"\n",
|
|
);
|
|
write_file(&root, "/etc/hostname", "redbear\n");
|
|
create_dir(&root, "/scheme/netcfg/ifaces/eth0/addr");
|
|
write_file(
|
|
&root,
|
|
"/scheme/netcfg/ifaces/eth0/addr/list",
|
|
"192.168.10.20/24\n",
|
|
);
|
|
write_file(
|
|
&root,
|
|
"/scheme/netcfg/ifaces/eth0/mac",
|
|
"02:00:00:00:00:01\n",
|
|
);
|
|
write_file(&root, "/scheme/netcfg/resolv/nameserver", "1.1.1.1\n");
|
|
write_file(
|
|
&root,
|
|
"/scheme/netcfg/route/list",
|
|
"default via 192.168.10.1\n",
|
|
);
|
|
create_dir(&root, "/scheme/network.eth0_rtl8125");
|
|
create_dir(&root, "/scheme/wifictl/ifaces/wlan0");
|
|
write_file(
|
|
&root,
|
|
"/scheme/wifictl/ifaces/wlan0/firmware-status",
|
|
"firmware=present family=intel-bz-arrow-lake prepared=yes\n",
|
|
);
|
|
write_file(
|
|
&root,
|
|
"/scheme/wifictl/ifaces/wlan0/transport-status",
|
|
"transport=pci memory_enabled=yes bus_master=yes bar0_present=yes irq_pin_present=yes\n",
|
|
);
|
|
write_file(
|
|
&root,
|
|
"/scheme/wifictl/ifaces/wlan0/transport-init-status",
|
|
"transport_init=stub\n",
|
|
);
|
|
write_file(
|
|
&root,
|
|
"/scheme/wifictl/ifaces/wlan0/activation-status",
|
|
"activation=stub\n",
|
|
);
|
|
write_file(
|
|
&root,
|
|
"/scheme/wifictl/ifaces/wlan0/scan-results",
|
|
"demo-ssid\ndemo-open\n",
|
|
);
|
|
create_dir(&root, "/scheme/btctl/adapters/hci0");
|
|
create_dir(&root, "/var/run/redbear-btusb");
|
|
write_file(
|
|
&root,
|
|
"/var/run/redbear-btusb/status",
|
|
&format!(
|
|
"transport=usb\nstartup=explicit\nupdated_at_epoch={}\nruntime_visibility=runtime-visible\n",
|
|
current_epoch_seconds()
|
|
),
|
|
);
|
|
write_file(
|
|
&root,
|
|
"/scheme/btctl/adapters/hci0/status",
|
|
"status=adapter-visible\ntransport_status=transport=usb startup=explicit runtime_visibility=runtime-visible\nscan_results_count=2\nconnected_bond_count=1\nbond_count=1\nbond_store_path=/var/lib/bluetooth/hci0/bonds\n",
|
|
);
|
|
write_file(
|
|
&root,
|
|
"/scheme/btctl/adapters/hci0/transport-status",
|
|
"transport=usb startup=explicit runtime_visibility=runtime-visible\n",
|
|
);
|
|
write_file(
|
|
&root,
|
|
"/scheme/btctl/adapters/hci0/bond-store-path",
|
|
"/var/lib/bluetooth/hci0/bonds\n",
|
|
);
|
|
write_file(
|
|
&root,
|
|
"/scheme/btctl/adapters/hci0/bond-count",
|
|
"bond_count=1\nbond_store_path=/var/lib/bluetooth/hci0/bonds\n",
|
|
);
|
|
write_file(
|
|
&root,
|
|
"/scheme/btctl/adapters/hci0/scan-results",
|
|
"demo-beacon\ndemo-sensor\n",
|
|
);
|
|
write_file(
|
|
&root,
|
|
"/scheme/btctl/adapters/hci0/connection-state",
|
|
"connection_state=stub-connected\nconnected_bond_count=1\nconnected_bond_ids=AA:BB:CC:DD:EE:FF\nnote=stub-control-only-no-real-link-layer-beyond-experimental-battery-sensor-battery-level-read\n",
|
|
);
|
|
write_file(
|
|
&root,
|
|
"/scheme/btctl/adapters/hci0/connect-result",
|
|
"connect_result=stub-connected bond_id=AA:BB:CC:DD:EE:FF state=connected\n",
|
|
);
|
|
write_file(
|
|
&root,
|
|
"/scheme/btctl/adapters/hci0/disconnect-result",
|
|
"disconnect_result=stub-disconnected bond_id=AA:BB:CC:DD:EE:FF state=disconnected\n",
|
|
);
|
|
write_file(
|
|
&root,
|
|
"/scheme/btctl/adapters/hci0/read-char-result",
|
|
"read_char_result=stub-value workload=battery-sensor-battery-level-read peripheral_class=ble-battery-sensor characteristic=battery-level bond_id=AA:BB:CC:DD:EE:FF service_uuid=0000180f-0000-1000-8000-00805f9b34fb char_uuid=00002a19-0000-1000-8000-00805f9b34fb access=read-only value_hex=57 value_percent=87\n",
|
|
);
|
|
create_dir(&root, "/var/lib/bluetooth/hci0/bonds");
|
|
write_file(
|
|
&root,
|
|
"/var/lib/bluetooth/hci0/bonds/aabbccddeeff.bond",
|
|
"bond_id=AA:BB:CC:DD:EE:FF\nalias=demo-sensor\ncreated_at_epoch=123\nsource=stub-cli\n",
|
|
);
|
|
create_dir(&root, "/etc/netctl");
|
|
write_file(&root, "/etc/netctl/active", "wired-static\n");
|
|
|
|
let report = collect_report(&Runtime::from_root(root.clone()));
|
|
assert_eq!(report.network.state, ProbeState::Functional);
|
|
assert!(report.network.connected);
|
|
assert_eq!(report.network.address.as_deref(), Some("192.168.10.20/24"));
|
|
assert_eq!(report.network.dns.as_deref(), Some("1.1.1.1"));
|
|
assert_eq!(
|
|
report.network.default_route.as_deref(),
|
|
Some("default via 192.168.10.1")
|
|
);
|
|
assert_eq!(
|
|
report.network.active_profile.as_deref(),
|
|
Some("wired-static")
|
|
);
|
|
assert!(
|
|
report
|
|
.network
|
|
.network_schemes
|
|
.iter()
|
|
.any(|name| name.contains("rtl8125"))
|
|
);
|
|
assert_eq!(report.network.wifi_control_state, ProbeState::Functional);
|
|
assert_eq!(report.network.wifi_interfaces, vec!["wlan0".to_string()]);
|
|
assert!(
|
|
report
|
|
.network
|
|
.wifi_firmware_status
|
|
.as_deref()
|
|
.unwrap()
|
|
.contains("intel-bz-arrow-lake")
|
|
);
|
|
assert!(
|
|
report
|
|
.network
|
|
.wifi_transport_status
|
|
.as_deref()
|
|
.unwrap()
|
|
.contains("memory_enabled=yes")
|
|
);
|
|
assert_eq!(
|
|
report.network.wifi_transport_init_status.as_deref(),
|
|
Some("transport_init=stub")
|
|
);
|
|
assert_eq!(
|
|
report.network.wifi_activation_status.as_deref(),
|
|
Some("activation=stub")
|
|
);
|
|
assert_eq!(
|
|
report.network.wifi_scan_results,
|
|
vec!["demo-ssid".to_string(), "demo-open".to_string()]
|
|
);
|
|
assert_eq!(report.network.bluetooth_transport_state, ProbeState::Active);
|
|
assert_eq!(
|
|
report.network.bluetooth_control_state,
|
|
ProbeState::Functional
|
|
);
|
|
assert_eq!(report.network.bluetooth_adapters, vec!["hci0".to_string()]);
|
|
assert!(
|
|
report
|
|
.network
|
|
.bluetooth_transport_status
|
|
.as_deref()
|
|
.unwrap()
|
|
.contains("runtime_visibility=runtime-visible")
|
|
);
|
|
assert!(
|
|
report
|
|
.network
|
|
.bluetooth_adapter_status
|
|
.as_deref()
|
|
.unwrap()
|
|
.contains("status=adapter-visible")
|
|
);
|
|
assert_eq!(
|
|
report.network.bluetooth_scan_results,
|
|
vec!["demo-beacon".to_string(), "demo-sensor".to_string()]
|
|
);
|
|
assert!(
|
|
report
|
|
.network
|
|
.bluetooth_connection_state
|
|
.as_deref()
|
|
.unwrap()
|
|
.contains("connected_bond_ids=AA:BB:CC:DD:EE:FF")
|
|
);
|
|
assert_eq!(
|
|
report.network.bluetooth_connect_result.as_deref(),
|
|
Some("connect_result=stub-connected bond_id=AA:BB:CC:DD:EE:FF state=connected")
|
|
);
|
|
assert_eq!(
|
|
report.network.bluetooth_disconnect_result.as_deref(),
|
|
Some(
|
|
"disconnect_result=stub-disconnected bond_id=AA:BB:CC:DD:EE:FF state=disconnected"
|
|
)
|
|
);
|
|
assert!(
|
|
report
|
|
.network
|
|
.bluetooth_read_char_result
|
|
.as_deref()
|
|
.unwrap()
|
|
.contains("read_char_result=stub-value")
|
|
);
|
|
assert!(
|
|
report
|
|
.network
|
|
.bluetooth_read_char_result
|
|
.as_deref()
|
|
.unwrap()
|
|
.contains("workload=battery-sensor-battery-level-read")
|
|
);
|
|
assert_eq!(
|
|
report.network.bluetooth_bond_store_path.as_deref(),
|
|
Some("/var/lib/bluetooth/hci0/bonds")
|
|
);
|
|
assert_eq!(report.network.bluetooth_bond_count, Some(1));
|
|
assert!(
|
|
report
|
|
.network
|
|
.bluetooth_claim_limit
|
|
.contains("real pairing")
|
|
);
|
|
assert!(
|
|
report
|
|
.network
|
|
.bluetooth_claim_limit
|
|
.contains("general device traffic")
|
|
);
|
|
assert!(
|
|
report
|
|
.network
|
|
.bluetooth_claim_limit
|
|
.contains("generic GATT")
|
|
);
|
|
assert!(
|
|
report
|
|
.network
|
|
.bluetooth_claim_limit
|
|
.contains("write support")
|
|
);
|
|
|
|
fs::remove_dir_all(root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn network_report_prefers_active_wifi_profile_interface() {
|
|
let root = temp_root();
|
|
create_dir(&root, "/scheme/netcfg/ifaces/wlan0/addr");
|
|
write_file(
|
|
&root,
|
|
"/scheme/netcfg/ifaces/wlan0/addr/list",
|
|
"10.0.0.44/24\n",
|
|
);
|
|
write_file(
|
|
&root,
|
|
"/scheme/netcfg/ifaces/wlan0/mac",
|
|
"02:00:00:00:00:44\n",
|
|
);
|
|
write_file(&root, "/scheme/netcfg/resolv/nameserver", "9.9.9.9\n");
|
|
write_file(&root, "/scheme/netcfg/route/list", "default via 10.0.0.1\n");
|
|
create_dir(&root, "/scheme/wifictl/ifaces/wlan0");
|
|
write_file(
|
|
&root,
|
|
"/scheme/wifictl/ifaces/wlan0/firmware-status",
|
|
"firmware=present\n",
|
|
);
|
|
write_file(
|
|
&root,
|
|
"/scheme/wifictl/ifaces/wlan0/transport-status",
|
|
"transport=active\n",
|
|
);
|
|
write_file(
|
|
&root,
|
|
"/scheme/wifictl/ifaces/wlan0/transport-init-status",
|
|
"transport_init=ok\n",
|
|
);
|
|
write_file(
|
|
&root,
|
|
"/scheme/wifictl/ifaces/wlan0/activation-status",
|
|
"activation=ok\n",
|
|
);
|
|
write_file(
|
|
&root,
|
|
"/scheme/wifictl/ifaces/wlan0/connect-result",
|
|
"connect_result=bounded-associated ssid=demo security=wpa2-psk\n",
|
|
);
|
|
write_file(
|
|
&root,
|
|
"/scheme/wifictl/ifaces/wlan0/disconnect-result",
|
|
"disconnect_result=bounded-disconnected\n",
|
|
);
|
|
create_dir(&root, "/etc/netctl");
|
|
write_file(&root, "/etc/netctl/active", "wifi-dhcp\n");
|
|
write_file(
|
|
&root,
|
|
"/etc/netctl/wifi-dhcp",
|
|
"Description='Wi-Fi'\nInterface=wlan0\nConnection=wifi\nSSID='demo'\nSecurity=wpa2-psk\nKey='secret'\nIP=dhcp\n",
|
|
);
|
|
|
|
let report = collect_report(&Runtime::from_root(root.clone()));
|
|
assert_eq!(report.network.active_profile.as_deref(), Some("wifi-dhcp"));
|
|
assert_eq!(report.network.interface.as_deref(), Some("wlan0"));
|
|
assert_eq!(report.network.address.as_deref(), Some("10.0.0.44/24"));
|
|
assert_eq!(report.network.mac.as_deref(), Some("02:00:00:00:00:44"));
|
|
assert_eq!(report.network.dns.as_deref(), Some("9.9.9.9"));
|
|
assert_eq!(
|
|
report.network.wifi_connect_result.as_deref(),
|
|
Some("connect_result=bounded-associated ssid=demo security=wpa2-psk")
|
|
);
|
|
assert_eq!(
|
|
report.network.wifi_disconnect_result.as_deref(),
|
|
Some("disconnect_result=bounded-disconnected")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn bluetooth_reporting_distinguishes_installed_from_runtime_visible() {
|
|
let root = temp_root();
|
|
write_file(&root, "/usr/bin/redbear-btusb", "");
|
|
write_file(&root, "/usr/bin/redbear-btctl", "");
|
|
|
|
let report = collect_report(&Runtime::from_root(root.clone()));
|
|
assert_eq!(
|
|
report.network.bluetooth_transport_state,
|
|
ProbeState::Present
|
|
);
|
|
assert_eq!(report.network.bluetooth_control_state, ProbeState::Present);
|
|
assert_eq!(report.network.bluetooth_connection_state, None);
|
|
assert_eq!(report.network.bluetooth_connect_result, None);
|
|
assert_eq!(report.network.bluetooth_disconnect_result, None);
|
|
assert_eq!(report.network.bluetooth_read_char_result, None);
|
|
assert_eq!(report.network.bluetooth_bond_store_path, None);
|
|
assert_eq!(report.network.bluetooth_bond_count, None);
|
|
assert_eq!(
|
|
integration_state(&report, "redbear-btusb"),
|
|
ProbeState::Present
|
|
);
|
|
assert_eq!(
|
|
integration_state(&report, "redbear-btctl"),
|
|
ProbeState::Present
|
|
);
|
|
|
|
fs::remove_dir_all(root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn bluetooth_bond_store_reporting_falls_back_to_filesystem_evidence() {
|
|
let root = temp_root();
|
|
write_file(&root, "/usr/bin/redbear-btctl", "");
|
|
create_dir(&root, "/scheme/btctl/adapters/hci0");
|
|
create_dir(&root, "/var/lib/bluetooth/hci0/bonds");
|
|
write_file(
|
|
&root,
|
|
"/var/lib/bluetooth/hci0/bonds/112233445566.bond",
|
|
"bond_id=11:22:33:44:55:66\ncreated_at_epoch=42\nsource=stub-cli\n",
|
|
);
|
|
|
|
let report = collect_report(&Runtime::from_root(root.clone()));
|
|
assert_eq!(
|
|
report.network.bluetooth_bond_store_path.as_deref(),
|
|
Some("/var/lib/bluetooth/hci0/bonds")
|
|
);
|
|
assert_eq!(report.network.bluetooth_bond_count, Some(1));
|
|
assert!(
|
|
report
|
|
.network
|
|
.bluetooth_claim_limit
|
|
.contains("stub bond files")
|
|
);
|
|
|
|
fs::remove_dir_all(root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn bluetooth_connection_reporting_reads_bounded_control_surfaces() {
|
|
let root = temp_root();
|
|
write_file(&root, "/usr/bin/redbear-btctl", "");
|
|
create_dir(&root, "/scheme/btctl/adapters/hci0");
|
|
write_file(
|
|
&root,
|
|
"/scheme/btctl/adapters/hci0/connection-state",
|
|
"connection_state=stub-connected\nconnected_bond_count=1\nconnected_bond_ids=AA:BB:CC:DD:EE:FF\nnote=stub-control-only-no-real-link-layer-beyond-experimental-battery-sensor-battery-level-read\n",
|
|
);
|
|
write_file(
|
|
&root,
|
|
"/scheme/btctl/adapters/hci0/connect-result",
|
|
"connect_result=stub-connected bond_id=AA:BB:CC:DD:EE:FF state=connected\n",
|
|
);
|
|
write_file(
|
|
&root,
|
|
"/scheme/btctl/adapters/hci0/disconnect-result",
|
|
"disconnect_result=stub-disconnected bond_id=AA:BB:CC:DD:EE:FF state=disconnected\n",
|
|
);
|
|
write_file(
|
|
&root,
|
|
"/scheme/btctl/adapters/hci0/read-char-result",
|
|
"read_char_result=stub-value workload=battery-sensor-battery-level-read peripheral_class=ble-battery-sensor characteristic=battery-level bond_id=AA:BB:CC:DD:EE:FF service_uuid=0000180f-0000-1000-8000-00805f9b34fb char_uuid=00002a19-0000-1000-8000-00805f9b34fb access=read-only value_hex=57 value_percent=87\n",
|
|
);
|
|
|
|
let report = collect_report(&Runtime::from_root(root.clone()));
|
|
assert!(
|
|
report
|
|
.network
|
|
.bluetooth_connection_state
|
|
.as_deref()
|
|
.unwrap()
|
|
.contains("connected_bond_ids=AA:BB:CC:DD:EE:FF")
|
|
);
|
|
assert_eq!(
|
|
report.network.bluetooth_connect_result.as_deref(),
|
|
Some("connect_result=stub-connected bond_id=AA:BB:CC:DD:EE:FF state=connected")
|
|
);
|
|
assert_eq!(
|
|
report.network.bluetooth_disconnect_result.as_deref(),
|
|
Some(
|
|
"disconnect_result=stub-disconnected bond_id=AA:BB:CC:DD:EE:FF state=disconnected"
|
|
)
|
|
);
|
|
assert!(
|
|
report
|
|
.network
|
|
.bluetooth_read_char_result
|
|
.as_deref()
|
|
.unwrap()
|
|
.contains("read_char_result=stub-value")
|
|
);
|
|
assert!(
|
|
report
|
|
.network
|
|
.bluetooth_read_char_result
|
|
.as_deref()
|
|
.unwrap()
|
|
.contains("peripheral_class=ble-battery-sensor")
|
|
);
|
|
assert_eq!(
|
|
integration_state(&report, "redbear-btctl"),
|
|
ProbeState::Functional
|
|
);
|
|
|
|
fs::remove_dir_all(root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn btctl_integration_requires_bounded_read_result_for_functional_state() {
|
|
let root = temp_root();
|
|
write_file(&root, "/usr/bin/redbear-btctl", "");
|
|
create_dir(&root, "/scheme/btctl/adapters/hci0");
|
|
|
|
let report = collect_report(&Runtime::from_root(root.clone()));
|
|
assert_eq!(
|
|
report.network.bluetooth_control_state,
|
|
ProbeState::Functional
|
|
);
|
|
assert_eq!(
|
|
integration_state(&report, "redbear-btctl"),
|
|
ProbeState::Active
|
|
);
|
|
|
|
fs::remove_dir_all(root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn stale_btusb_status_does_not_report_active_in_integrations() {
|
|
let root = temp_root();
|
|
write_file(&root, "/usr/bin/redbear-btusb", "");
|
|
write_file(
|
|
&root,
|
|
"/var/run/redbear-btusb/status",
|
|
"transport=usb\nstartup=explicit\nupdated_at_epoch=1\nruntime_visibility=runtime-visible\n",
|
|
);
|
|
|
|
let report = collect_report(&Runtime::from_root(root.clone()));
|
|
assert_eq!(
|
|
report.network.bluetooth_transport_state,
|
|
ProbeState::Present
|
|
);
|
|
assert_eq!(
|
|
integration_state(&report, "redbear-btusb"),
|
|
ProbeState::Present
|
|
);
|
|
|
|
fs::remove_dir_all(root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn rtl8125_hardware_detection_parses_pci_config() {
|
|
let root = temp_root();
|
|
create_dir(&root, "/scheme/pci/0000--02--00.0");
|
|
let mut config = [0u8; 68];
|
|
config[0x00] = (RTL8125_VENDOR_ID & 0xff) as u8;
|
|
config[0x01] = (RTL8125_VENDOR_ID >> 8) as u8;
|
|
config[0x02] = (RTL8125_DEVICE_ID & 0xff) as u8;
|
|
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();
|
|
}
|
|
fs::write(&path, config).unwrap();
|
|
|
|
let network = collect_network(&Runtime::from_root(root.clone()));
|
|
let hardware = collect_hardware(&Runtime::from_root(root.clone()), &network);
|
|
assert!(hardware.rtl8125_present);
|
|
|
|
fs::remove_dir_all(root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn virtio_net_hardware_detection_parses_pci_config() {
|
|
let root = temp_root();
|
|
create_dir(&root, "/scheme/pci/0000--00--03.0");
|
|
let mut config = [0u8; 68];
|
|
config[0x00] = (VIRTIO_NET_VENDOR_ID & 0xff) as u8;
|
|
config[0x01] = (VIRTIO_NET_VENDOR_ID >> 8) as u8;
|
|
config[0x02] = (VIRTIO_NET_DEVICE_ID & 0xff) as u8;
|
|
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();
|
|
}
|
|
fs::write(&path, config).unwrap();
|
|
|
|
let network = collect_network(&Runtime::from_root(root.clone()));
|
|
let hardware = collect_hardware(&Runtime::from_root(root.clone()), &network);
|
|
assert!(hardware.virtio_net_present);
|
|
|
|
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();
|
|
write_file(
|
|
&root,
|
|
"/usr/lib/os-release",
|
|
"PRETTY_NAME=\"Red Bear OS\"\nVERSION_ID=\"0.1.0\"\n",
|
|
);
|
|
write_file(&root, "/usr/bin/redbear-info", "");
|
|
write_file(&root, "/usr/bin/redbear-netstat", "");
|
|
write_file(&root, "/usr/bin/redbear-nmap", "");
|
|
create_dir(&root, "/scheme/netcfg");
|
|
write_file(
|
|
&root,
|
|
"/scheme/netcfg/ifaces/eth0/addr/list",
|
|
"Not configured\n",
|
|
);
|
|
|
|
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(
|
|
&mut output,
|
|
"state",
|
|
state_label(report.network.state),
|
|
false,
|
|
0,
|
|
);
|
|
assert!(output.contains("state"));
|
|
assert!(
|
|
report
|
|
.integrations
|
|
.iter()
|
|
.any(|item| item.check.name == "redbear-info")
|
|
);
|
|
assert!(
|
|
report
|
|
.integrations
|
|
.iter()
|
|
.any(|item| item.check.name == "redbear-netstat")
|
|
);
|
|
assert!(
|
|
report
|
|
.integrations
|
|
.iter()
|
|
.any(|item| item.check.name == "redbear-btusb")
|
|
);
|
|
assert!(
|
|
report
|
|
.integrations
|
|
.iter()
|
|
.any(|item| item.check.name == "redbear-btctl")
|
|
);
|
|
assert!(
|
|
report
|
|
.integrations
|
|
.iter()
|
|
.any(|item| item.check.name == "redbear-upower")
|
|
);
|
|
assert!(
|
|
report
|
|
.integrations
|
|
.iter()
|
|
.any(|item| item.check.name == "redbear-nmap")
|
|
);
|
|
|
|
fs::remove_dir_all(root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn collect_boot_timeline_reads_probe_events() {
|
|
let root = temp_root();
|
|
write_file(
|
|
&root,
|
|
BOOT_TIMELINE_PATH,
|
|
concat!(
|
|
"{\"ts\":1000,\"event\":\"bus_enumerated\",\"bus\":\"pci\",\"count\":5}\n",
|
|
"{\"ts\":1120,\"event\":\"probe\",\"device\":\"pci/00:02:0\",\"driver\":\"redox-drm\",\"status\":\"bound\"}\n",
|
|
"{\"ts\":1750,\"event\":\"probe\",\"device\":\"pci/00:19:0\",\"driver\":\"e1000d\",\"status\":\"deferred\"}\n"
|
|
),
|
|
);
|
|
|
|
let timeline = collect_boot_timeline(&Runtime::from_root(root.clone())).unwrap();
|
|
assert_eq!(timeline.len(), 3);
|
|
assert!(matches!(
|
|
timeline[0].event,
|
|
BootTimelineEvent::BusEnumerated { ref bus, count } if bus == "pci" && count == 5
|
|
));
|
|
assert!(matches!(
|
|
timeline[1].event,
|
|
BootTimelineEvent::Probe {
|
|
ref driver,
|
|
status: BootProbeStatus::Bound,
|
|
..
|
|
} if driver == "redox-drm"
|
|
));
|
|
assert_eq!(format_timeline_device("pci/00:02:0"), "00.02.0");
|
|
|
|
fs::remove_dir_all(root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn collect_device_status_reads_config_params_and_irq_mode() {
|
|
let root = temp_root();
|
|
create_dir(&root, "/scheme/pci/0000--00--02.0");
|
|
create_dir(&root, "/proc/123");
|
|
let mut config = [0u8; 68];
|
|
config[0x00] = 0x86;
|
|
config[0x01] = 0x80;
|
|
config[0x02] = 0x34;
|
|
config[0x03] = 0x12;
|
|
config[0x0b] = 0x03;
|
|
config[0x0a] = 0x00;
|
|
config[0x09] = 0x00;
|
|
config[0x0e] = 0x00;
|
|
config[0x06] = 0x10;
|
|
config[0x34] = 0x40;
|
|
config[0x40] = 0x11;
|
|
fs::write(root.join("scheme/pci/0000--00--02.0/config"), config).unwrap();
|
|
write_file(
|
|
&root,
|
|
"/tmp/redbear-driver-params/0000--00--02.0/driver",
|
|
"redox-drm\n",
|
|
);
|
|
write_file(
|
|
&root,
|
|
"/tmp/redbear-driver-params/0000--00--02.0/enabled",
|
|
"true\n",
|
|
);
|
|
write_file(
|
|
&root,
|
|
"/tmp/redbear-driver-params/0000--00--02.0/priority",
|
|
"60\n",
|
|
);
|
|
write_file(
|
|
&root,
|
|
"/tmp/redbear-irq-report/redox-drm.env",
|
|
"driver=redox-drm\npid=123\ndevice=0000:00:02.0\nmode=msix\nreason=driver_selected_interrupt_delivery\n",
|
|
);
|
|
write_file(
|
|
&root,
|
|
BOOT_TIMELINE_PATH,
|
|
concat!(
|
|
"{\"ts\":1000,\"event\":\"probe\",\"device\":\"pci/00:02:0\",\"driver\":\"redox-drm\",\"status\":\"deferred\"}\n",
|
|
"{\"ts\":1200,\"event\":\"probe\",\"device\":\"0000:00:02.0\",\"driver\":\"redox-drm\",\"status\":\"bound\"}\n"
|
|
),
|
|
);
|
|
|
|
let report =
|
|
collect_device_status(&Runtime::from_root(root.clone()), "pci/00:02:0").unwrap();
|
|
assert_eq!(report.selector, "pci/00:02:0");
|
|
assert_eq!(report.status, Some(BootProbeStatus::Bound));
|
|
assert_eq!(report.vendor_id, 0x8086);
|
|
assert_eq!(report.device_id, 0x1234);
|
|
assert_eq!(report.class_name, "Display controller");
|
|
assert_eq!(report.irq_mode, "msi-x");
|
|
assert_eq!(report.driver.as_deref(), Some("redox-drm"));
|
|
assert_eq!(
|
|
report.parameters,
|
|
vec![
|
|
("enabled".to_string(), "true".to_string()),
|
|
("priority".to_string(), "60".to_string())
|
|
]
|
|
);
|
|
|
|
fs::remove_dir_all(root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn collect_device_status_falls_back_to_config_irq_mode_without_runtime_report() {
|
|
let root = temp_root();
|
|
create_dir(&root, "/scheme/pci/0000--00--02.0");
|
|
let mut config = [0u8; 68];
|
|
config[0x00] = 0x86;
|
|
config[0x01] = 0x80;
|
|
config[0x02] = 0x34;
|
|
config[0x03] = 0x12;
|
|
config[0x0b] = 0x03;
|
|
config[0x0a] = 0x00;
|
|
config[0x09] = 0x00;
|
|
config[0x0e] = 0x00;
|
|
config[0x06] = 0x10;
|
|
config[0x34] = 0x40;
|
|
config[0x40] = 0x11;
|
|
fs::write(root.join("scheme/pci/0000--00--02.0/config"), config).unwrap();
|
|
|
|
let report =
|
|
collect_device_status(&Runtime::from_root(root.clone()), "pci/00:02:0").unwrap();
|
|
assert_eq!(report.irq_mode, "msi-x");
|
|
|
|
fs::remove_dir_all(root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn collect_health_items_reports_scheme_and_adapter_gaps() {
|
|
let root = temp_root();
|
|
create_dir(&root, "/scheme/pci");
|
|
create_dir(&root, "/scheme/acpi");
|
|
create_dir(&root, "/scheme/drm/card0");
|
|
create_dir(&root, "/scheme/usb");
|
|
create_dir(&root, "/scheme/netcfg");
|
|
create_dir(&root, "/scheme/wifictl");
|
|
|
|
let runtime = Runtime::from_root(root.clone());
|
|
let report = collect_report(&runtime);
|
|
let health = collect_health_items(&runtime, &report);
|
|
|
|
assert!(
|
|
health
|
|
.iter()
|
|
.any(|item| { item.label == "PCI scheme" && item.state == HealthState::Healthy })
|
|
);
|
|
assert!(health.iter().any(|item| {
|
|
item.label == "Wi-Fi"
|
|
&& item.state == HealthState::Warning
|
|
&& item.detail == "no adapter"
|
|
}));
|
|
assert!(health.iter().any(|item| {
|
|
item.label == "Bluetooth"
|
|
&& item.state == HealthState::Critical
|
|
&& item.detail == "not running"
|
|
}));
|
|
|
|
fs::remove_dir_all(root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn parse_args_accepts_boot_health_and_device_modes() {
|
|
let boot = parse_args(["redbear-info".to_string(), "--boot".to_string()]).unwrap();
|
|
assert!(matches!(boot.mode, OutputMode::Boot));
|
|
|
|
let health = parse_args(["redbear-info".to_string(), "--health".to_string()]).unwrap();
|
|
assert!(matches!(health.mode, OutputMode::Health));
|
|
|
|
let device = parse_args([
|
|
"redbear-info".to_string(),
|
|
"--device".to_string(),
|
|
"pci/00:02:0".to_string(),
|
|
])
|
|
.unwrap();
|
|
assert!(matches!(device.mode, OutputMode::Device));
|
|
assert_eq!(device.device.as_deref(), Some("pci/00:02:0"));
|
|
}
|
|
|
|
#[test]
|
|
fn parse_args_rejects_device_without_value_and_with_other_modes() {
|
|
assert_eq!(
|
|
parse_args(["redbear-info".to_string(), "--device".to_string()]).err(),
|
|
Some("--device requires a PCI address".to_string())
|
|
);
|
|
assert_eq!(
|
|
parse_args([
|
|
"redbear-info".to_string(),
|
|
"--json".to_string(),
|
|
"--device".to_string(),
|
|
"pci/00:02:0".to_string(),
|
|
])
|
|
.err(),
|
|
Some("cannot combine --device with --json".to_string())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_args_accepts_probe_mode() {
|
|
let options = parse_args(["redbear-info".to_string(), "--probe".to_string()]).unwrap();
|
|
|
|
assert!(matches!(options.mode, OutputMode::Probe));
|
|
}
|
|
|
|
#[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([
|
|
"redbear-info".to_string(),
|
|
"--quirks".to_string(),
|
|
"--verbose".to_string(),
|
|
])
|
|
.unwrap();
|
|
|
|
assert!(matches!(options.mode, OutputMode::Quirks));
|
|
assert!(options.verbose);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_args_rejects_quirks_with_other_output_modes() {
|
|
assert_eq!(
|
|
parse_args([
|
|
"redbear-info".to_string(),
|
|
"--quirks".to_string(),
|
|
"--json".to_string(),
|
|
])
|
|
.err(),
|
|
Some("cannot combine --json with --quirks".to_string())
|
|
);
|
|
assert_eq!(
|
|
parse_args([
|
|
"redbear-info".to_string(),
|
|
"--test".to_string(),
|
|
"--quirks".to_string(),
|
|
])
|
|
.err(),
|
|
Some("cannot combine --quirks with --test".to_string())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn collect_quirks_reads_pci_class_and_usb_entries() {
|
|
let root = temp_root();
|
|
write_file(
|
|
&root,
|
|
"/etc/quirks.d/10-gpu.toml",
|
|
"[[pci_quirk]]\nvendor = 0x1002\nclass = 0x030000\nflags = [\"no_d3cold\", \"need_firmware\"]\ndescription = \"GPU class quirk\"\n\n[[pci_quirk]]\nvendor = 0x1002\ndevice = 0x744c\nflags = [\"need_iommu\"]\n\n[[usb_quirk]]\nvendor = 0x0bda\nproduct = 0x8153\nflags = [\"no_string_fetch\"]\n",
|
|
);
|
|
|
|
let report = collect_quirks(&Runtime::from_root(root.clone()));
|
|
assert!(report.load_errors.is_empty());
|
|
assert_eq!(report.files_loaded.len(), 1);
|
|
|
|
let file = &report.files_loaded[0];
|
|
assert_eq!(file.name, "10-gpu.toml");
|
|
assert_eq!(file.pci_quirks.len(), 2);
|
|
assert_eq!(file.usb_quirks.len(), 1);
|
|
assert_eq!(file.pci_quirks[0].vendor, "0x1002");
|
|
assert_eq!(file.pci_quirks[0].class.as_deref(), Some("0x030000"));
|
|
assert_eq!(
|
|
file.pci_quirks[0].description.as_deref(),
|
|
Some("GPU class quirk")
|
|
);
|
|
assert_eq!(file.pci_quirks[1].device.as_deref(), Some("0x744C"));
|
|
assert_eq!(file.usb_quirks[0].vendor, "0x0BDA");
|
|
assert_eq!(file.usb_quirks[0].product.as_deref(), Some("0x8153"));
|
|
|
|
fs::remove_dir_all(root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn collect_quirks_reports_missing_directory() {
|
|
let root = temp_root();
|
|
|
|
let report = collect_quirks(&Runtime::from_root(root.clone()));
|
|
assert!(report.files_loaded.is_empty());
|
|
assert_eq!(report.load_errors, vec!["quirks directory not found"]);
|
|
|
|
fs::remove_dir_all(root).unwrap();
|
|
}
|
|
}
|