Improve phase 5 and 6 validation tooling
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
+585
-10
@@ -1,5 +1,9 @@
|
||||
use std::path::Path;
|
||||
use std::process::{self, Command};
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
process::{self, Command},
|
||||
};
|
||||
|
||||
use redbear_hwutils::parse_args;
|
||||
use serde_json::Value;
|
||||
@@ -7,6 +11,46 @@ use serde_json::Value;
|
||||
const PROGRAM: &str = "redbear-phase5-network-check";
|
||||
const USAGE: &str = "Usage: redbear-phase5-network-check\n\nShow the installed Phase 5 networking/session plumbing surface inside the guest.";
|
||||
|
||||
const DBUS_SEND: &str = "dbus-send";
|
||||
const POWER_ROOT: &str = "/scheme/acpi/power";
|
||||
const UPOWER_DESTINATION: &str = "org.freedesktop.UPower";
|
||||
const UPOWER_PATH: &str = "/org/freedesktop/UPower";
|
||||
const UDISKS_DESTINATION: &str = "org.freedesktop.UDisks2";
|
||||
const UDISKS_ROOT_PATH: &str = "/org/freedesktop/UDisks2";
|
||||
const UDISKS_MANAGER_PATH: &str = "/org/freedesktop/UDisks2/Manager";
|
||||
const UDISKS_BLOCK_PREFIX: &str = "/org/freedesktop/UDisks2/block_devices/";
|
||||
const UDISKS_DRIVE_PREFIX: &str = "/org/freedesktop/UDisks2/drives/";
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct PowerRuntime {
|
||||
adapter_ids: Vec<String>,
|
||||
battery_ids: Vec<String>,
|
||||
native_paths_by_object: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct DiskRuntime {
|
||||
block_paths: BTreeSet<String>,
|
||||
drive_paths: BTreeSet<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
struct RootKey {
|
||||
disk_number: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
struct PartitionKey {
|
||||
disk_number: u32,
|
||||
partition_number: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum EntryKind {
|
||||
Root(RootKey),
|
||||
Partition(PartitionKey),
|
||||
}
|
||||
|
||||
fn require_path(path: &str) -> Result<(), String> {
|
||||
if Path::new(path).exists() {
|
||||
println!("{path}");
|
||||
@@ -31,6 +75,459 @@ fn optional_require_path(path: &str, label: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
fn require_one_path<'a>(paths: &'a [&'a str], label: &str) -> Result<&'a str, String> {
|
||||
for path in paths {
|
||||
if Path::new(path).exists() {
|
||||
println!("{label}={path}");
|
||||
return Ok(*path);
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!("missing any of: {}", paths.join(", ")))
|
||||
}
|
||||
|
||||
fn list_dir_names(path: impl AsRef<Path>) -> Vec<String> {
|
||||
let mut names = match fs::read_dir(path) {
|
||||
Ok(entries) => entries
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter_map(|entry| entry.file_name().into_string().ok())
|
||||
.collect::<Vec<_>>(),
|
||||
Err(_) => Vec::new(),
|
||||
};
|
||||
names.sort();
|
||||
names
|
||||
}
|
||||
|
||||
fn read_trimmed(path: impl AsRef<Path>) -> Option<String> {
|
||||
let value = fs::read_to_string(path).ok()?;
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn run_command(program: &str, args: &[&str], label: &str) -> Result<String, String> {
|
||||
let output = Command::new(program)
|
||||
.args(args)
|
||||
.output()
|
||||
.map_err(|err| format!("failed to run {label}: {err}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let detail = if !stderr.trim().is_empty() {
|
||||
stderr.trim().to_string()
|
||||
} else if !stdout.trim().is_empty() {
|
||||
stdout.trim().to_string()
|
||||
} else {
|
||||
String::from("no output")
|
||||
};
|
||||
return Err(format!(
|
||||
"{label} exited with status {}: {detail}",
|
||||
output.status
|
||||
));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
||||
}
|
||||
|
||||
fn quoted_values_with_prefix(output: &str, prefix: &str) -> BTreeSet<String> {
|
||||
let mut values = BTreeSet::new();
|
||||
|
||||
for line in output.lines() {
|
||||
let mut remainder = line;
|
||||
while let Some(start) = remainder.find('"') {
|
||||
remainder = &remainder[start + 1..];
|
||||
let Some(end) = remainder.find('"') else {
|
||||
break;
|
||||
};
|
||||
let candidate = &remainder[..end];
|
||||
if candidate.starts_with(prefix) {
|
||||
values.insert(candidate.to_string());
|
||||
}
|
||||
remainder = &remainder[end + 1..];
|
||||
}
|
||||
}
|
||||
|
||||
values
|
||||
}
|
||||
|
||||
fn quoted_value(line: &str) -> Option<&str> {
|
||||
let start = line.find('"')?;
|
||||
let remainder = &line[start + 1..];
|
||||
let end = remainder.find('"')?;
|
||||
Some(&remainder[..end])
|
||||
}
|
||||
|
||||
fn managed_object_keys_with_prefix(output: &str, prefix: &str) -> BTreeSet<String> {
|
||||
let lines = output.lines().collect::<Vec<_>>();
|
||||
let mut values = BTreeSet::new();
|
||||
|
||||
for window in lines.windows(2) {
|
||||
let current = window[0].trim();
|
||||
let next = window[1].trim();
|
||||
if current != "dict entry(" || !next.starts_with("object path ") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(candidate) = quoted_value(next) else {
|
||||
continue;
|
||||
};
|
||||
if candidate.starts_with(prefix) {
|
||||
values.insert(candidate.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
values
|
||||
}
|
||||
|
||||
fn summarize_set(values: &BTreeSet<String>) -> String {
|
||||
if values.is_empty() {
|
||||
String::from("none")
|
||||
} else {
|
||||
values.iter().cloned().collect::<Vec<_>>().join(", ")
|
||||
}
|
||||
}
|
||||
|
||||
fn note_bus_name_registered(list_names_output: &str, bus_name: &str, label: &str) {
|
||||
if list_names_output.contains(bus_name) {
|
||||
println!("{label}=present");
|
||||
} else {
|
||||
println!("{label}=activated_lazily");
|
||||
}
|
||||
}
|
||||
|
||||
fn contains_phase5_wifi_success(text: &str) -> bool {
|
||||
text.contains("PASS: bounded Intel Wi-Fi runtime path exercised inside target runtime")
|
||||
|| text.contains("PASS: bounded Intel Wi-Fi runtime path exercised on target")
|
||||
}
|
||||
|
||||
fn adapter_object_path(id: &str) -> String {
|
||||
format!("/org/freedesktop/UPower/devices/line_power_{id}")
|
||||
}
|
||||
|
||||
fn battery_object_path(id: &str) -> String {
|
||||
format!("/org/freedesktop/UPower/devices/battery_{id}")
|
||||
}
|
||||
|
||||
impl PowerRuntime {
|
||||
fn discover() -> Self {
|
||||
let adapter_ids = list_dir_names(PathBuf::from(POWER_ROOT).join("adapters"));
|
||||
let battery_ids = list_dir_names(PathBuf::from(POWER_ROOT).join("batteries"));
|
||||
let mut native_paths_by_object = BTreeMap::new();
|
||||
|
||||
for adapter_id in &adapter_ids {
|
||||
if let Some(native_path) = read_trimmed(
|
||||
PathBuf::from(POWER_ROOT)
|
||||
.join("adapters")
|
||||
.join(adapter_id)
|
||||
.join("path"),
|
||||
) {
|
||||
native_paths_by_object.insert(adapter_object_path(adapter_id), native_path);
|
||||
}
|
||||
}
|
||||
|
||||
for battery_id in &battery_ids {
|
||||
if let Some(native_path) = read_trimmed(
|
||||
PathBuf::from(POWER_ROOT)
|
||||
.join("batteries")
|
||||
.join(battery_id)
|
||||
.join("path"),
|
||||
) {
|
||||
native_paths_by_object.insert(battery_object_path(battery_id), native_path);
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
adapter_ids,
|
||||
battery_ids,
|
||||
native_paths_by_object,
|
||||
}
|
||||
}
|
||||
|
||||
fn expected_device_paths(&self) -> BTreeSet<String> {
|
||||
let mut paths = BTreeSet::new();
|
||||
for adapter_id in &self.adapter_ids {
|
||||
paths.insert(adapter_object_path(adapter_id));
|
||||
}
|
||||
for battery_id in &self.battery_ids {
|
||||
paths.insert(battery_object_path(battery_id));
|
||||
}
|
||||
paths
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_upower(list_names_output: &str) -> Result<(), String> {
|
||||
let runtime = PowerRuntime::discover();
|
||||
let expected_device_paths = runtime.expected_device_paths();
|
||||
println!("UPOWER_RUNTIME_ADAPTERS={}", runtime.adapter_ids.len());
|
||||
println!("UPOWER_RUNTIME_BATTERIES={}", runtime.battery_ids.len());
|
||||
|
||||
let enumerate_output = run_command(
|
||||
DBUS_SEND,
|
||||
&[
|
||||
"--system",
|
||||
"--dest=org.freedesktop.UPower",
|
||||
"--type=method_call",
|
||||
"--print-reply",
|
||||
UPOWER_PATH,
|
||||
"org.freedesktop.UPower.EnumerateDevices",
|
||||
],
|
||||
"dbus-send UPower EnumerateDevices",
|
||||
)?;
|
||||
let enumerated_device_paths =
|
||||
quoted_values_with_prefix(&enumerate_output, "/org/freedesktop/UPower/devices/");
|
||||
println!(
|
||||
"UPOWER_ENUMERATED_DEVICES={}",
|
||||
enumerated_device_paths.len()
|
||||
);
|
||||
note_bus_name_registered(list_names_output, UPOWER_DESTINATION, "UPOWER_BUS_NAME");
|
||||
|
||||
let missing_device_paths = expected_device_paths
|
||||
.difference(&enumerated_device_paths)
|
||||
.cloned()
|
||||
.collect::<BTreeSet<_>>();
|
||||
if !missing_device_paths.is_empty() {
|
||||
return Err(format!(
|
||||
"UPower did not enumerate runtime-backed devices: {}",
|
||||
summarize_set(&missing_device_paths)
|
||||
));
|
||||
}
|
||||
|
||||
let unexpected_device_paths = enumerated_device_paths
|
||||
.difference(&expected_device_paths)
|
||||
.cloned()
|
||||
.collect::<BTreeSet<_>>();
|
||||
if !unexpected_device_paths.is_empty() {
|
||||
return Err(format!(
|
||||
"UPower enumerated devices not backed by /scheme/acpi/power: {}",
|
||||
summarize_set(&unexpected_device_paths)
|
||||
));
|
||||
}
|
||||
|
||||
for (object_path, expected_native_path) in &runtime.native_paths_by_object {
|
||||
let native_path_output = run_command(
|
||||
DBUS_SEND,
|
||||
&[
|
||||
"--system",
|
||||
"--dest=org.freedesktop.UPower",
|
||||
"--type=method_call",
|
||||
"--print-reply",
|
||||
object_path.as_str(),
|
||||
"org.freedesktop.DBus.Properties.Get",
|
||||
"string:org.freedesktop.UPower.Device",
|
||||
"string:NativePath",
|
||||
],
|
||||
"dbus-send UPower Device.NativePath",
|
||||
)?;
|
||||
let reported_paths = quoted_values_with_prefix(&native_path_output, "/");
|
||||
let reported_native_path = reported_paths.iter().next().ok_or_else(|| {
|
||||
format!("UPower device {object_path} did not return a NativePath property value")
|
||||
})?;
|
||||
|
||||
if reported_native_path != expected_native_path {
|
||||
return Err(format!(
|
||||
"UPower device {object_path} reported NativePath {reported_native_path}, expected {expected_native_path}"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
println!("UPOWER_NATIVE_PATHS=validated");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl DiskRuntime {
|
||||
fn discover() -> Self {
|
||||
let mut block_paths = BTreeSet::new();
|
||||
let mut drive_paths = BTreeSet::new();
|
||||
|
||||
for scheme_name in list_dir_names("/scheme")
|
||||
.into_iter()
|
||||
.filter(|name| name.starts_with("disk."))
|
||||
{
|
||||
let scheme_path = PathBuf::from("/scheme").join(&scheme_name);
|
||||
|
||||
for entry_name in list_dir_names(&scheme_path) {
|
||||
match parse_disk_entry_name(&entry_name) {
|
||||
Some(EntryKind::Root(_)) => {
|
||||
drive_paths.insert(drive_object_path(&scheme_name, &entry_name));
|
||||
block_paths.insert(block_object_path(&scheme_name, &entry_name));
|
||||
}
|
||||
Some(EntryKind::Partition(_)) => {
|
||||
block_paths.insert(block_object_path(&scheme_name, &entry_name));
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
block_paths,
|
||||
drive_paths,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_disk_entry_name(entry_name: &str) -> Option<EntryKind> {
|
||||
if let Some(position) = entry_name.find('p') {
|
||||
let disk_number = entry_name[..position].parse().ok()?;
|
||||
let partition_number = entry_name[position + 1..].parse().ok()?;
|
||||
return Some(EntryKind::Partition(PartitionKey {
|
||||
disk_number,
|
||||
partition_number,
|
||||
}));
|
||||
}
|
||||
|
||||
Some(EntryKind::Root(RootKey {
|
||||
disk_number: entry_name.parse().ok()?,
|
||||
}))
|
||||
}
|
||||
|
||||
fn block_object_path(scheme_name: &str, entry_name: &str) -> String {
|
||||
format!(
|
||||
"{UDISKS_BLOCK_PREFIX}{}",
|
||||
stable_object_name(scheme_name, entry_name)
|
||||
)
|
||||
}
|
||||
|
||||
fn drive_object_path(scheme_name: &str, entry_name: &str) -> String {
|
||||
format!(
|
||||
"{UDISKS_DRIVE_PREFIX}{}",
|
||||
stable_object_name(scheme_name, entry_name)
|
||||
)
|
||||
}
|
||||
|
||||
fn stable_object_name(scheme_name: &str, entry_name: &str) -> String {
|
||||
format!(
|
||||
"{}_{}",
|
||||
encode_path_component(scheme_name),
|
||||
encode_path_component(entry_name)
|
||||
)
|
||||
}
|
||||
|
||||
fn encode_path_component(component: &str) -> String {
|
||||
let mut encoded = String::new();
|
||||
|
||||
for byte in component.bytes() {
|
||||
if byte.is_ascii_alphanumeric() {
|
||||
encoded.push(byte as char);
|
||||
} else {
|
||||
encoded.push('_');
|
||||
encoded.push(hex_char(byte >> 4));
|
||||
encoded.push(hex_char(byte & 0x0f));
|
||||
}
|
||||
}
|
||||
|
||||
if encoded.is_empty() {
|
||||
encoded.push('_');
|
||||
encoded.push('0');
|
||||
encoded.push('0');
|
||||
}
|
||||
|
||||
encoded
|
||||
}
|
||||
|
||||
fn hex_char(value: u8) -> char {
|
||||
match value {
|
||||
0..=9 => (b'0' + value) as char,
|
||||
10..=15 => (b'a' + (value - 10)) as char,
|
||||
_ => unreachable!("hex nibble out of range"),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_udisks(list_names_output: &str) -> Result<(), String> {
|
||||
let runtime = DiskRuntime::discover();
|
||||
println!(
|
||||
"UDISKS_RUNTIME_DRIVE_SURFACES={}",
|
||||
runtime.drive_paths.len()
|
||||
);
|
||||
println!(
|
||||
"UDISKS_RUNTIME_BLOCK_SURFACES={}",
|
||||
runtime.block_paths.len()
|
||||
);
|
||||
|
||||
let managed_objects_output = run_command(
|
||||
DBUS_SEND,
|
||||
&[
|
||||
"--system",
|
||||
"--dest=org.freedesktop.UDisks2",
|
||||
"--type=method_call",
|
||||
"--print-reply",
|
||||
UDISKS_ROOT_PATH,
|
||||
"org.freedesktop.DBus.ObjectManager.GetManagedObjects",
|
||||
],
|
||||
"dbus-send UDisks2 GetManagedObjects",
|
||||
)?;
|
||||
|
||||
let managed_object_paths =
|
||||
managed_object_keys_with_prefix(&managed_objects_output, "/org/freedesktop/UDisks2/");
|
||||
let managed_block_paths =
|
||||
managed_object_keys_with_prefix(&managed_objects_output, UDISKS_BLOCK_PREFIX);
|
||||
let managed_drive_paths =
|
||||
managed_object_keys_with_prefix(&managed_objects_output, UDISKS_DRIVE_PREFIX);
|
||||
let manager_present = managed_object_paths.contains(UDISKS_MANAGER_PATH);
|
||||
|
||||
if !manager_present && (!runtime.block_paths.is_empty() || !runtime.drive_paths.is_empty()) {
|
||||
return Err(format!(
|
||||
"UDisks2 GetManagedObjects did not include manager object {UDISKS_MANAGER_PATH} while runtime disk surfaces were present"
|
||||
));
|
||||
}
|
||||
|
||||
println!("UDISKS_MANAGED_OBJECTS=present");
|
||||
note_bus_name_registered(list_names_output, UDISKS_DESTINATION, "UDISKS_BUS_NAME");
|
||||
|
||||
let missing_block_paths = runtime
|
||||
.block_paths
|
||||
.difference(&managed_block_paths)
|
||||
.cloned()
|
||||
.collect::<BTreeSet<_>>();
|
||||
if !missing_block_paths.is_empty() {
|
||||
return Err(format!(
|
||||
"UDisks2 managed objects missed runtime-backed block devices: {}",
|
||||
summarize_set(&missing_block_paths)
|
||||
));
|
||||
}
|
||||
|
||||
let unexpected_block_paths = managed_block_paths
|
||||
.difference(&runtime.block_paths)
|
||||
.cloned()
|
||||
.collect::<BTreeSet<_>>();
|
||||
if !unexpected_block_paths.is_empty() {
|
||||
return Err(format!(
|
||||
"UDisks2 exposed block devices not backed by /scheme/disk.*: {}",
|
||||
summarize_set(&unexpected_block_paths)
|
||||
));
|
||||
}
|
||||
|
||||
let missing_drive_paths = runtime
|
||||
.drive_paths
|
||||
.difference(&managed_drive_paths)
|
||||
.cloned()
|
||||
.collect::<BTreeSet<_>>();
|
||||
if !missing_drive_paths.is_empty() {
|
||||
return Err(format!(
|
||||
"UDisks2 managed objects missed runtime-backed drives: {}",
|
||||
summarize_set(&missing_drive_paths)
|
||||
));
|
||||
}
|
||||
|
||||
let unexpected_drive_paths = managed_drive_paths
|
||||
.difference(&runtime.drive_paths)
|
||||
.cloned()
|
||||
.collect::<BTreeSet<_>>();
|
||||
if !unexpected_drive_paths.is_empty() {
|
||||
return Err(format!(
|
||||
"UDisks2 exposed drives not backed by /scheme/disk.*: {}",
|
||||
summarize_set(&unexpected_drive_paths)
|
||||
));
|
||||
}
|
||||
|
||||
println!("UDISKS_BLOCK_OBJECT_PATHS=validated");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
parse_args(PROGRAM, USAGE, std::env::args()).map_err(|err| {
|
||||
if err.is_empty() {
|
||||
@@ -41,7 +538,11 @@ fn run() -> Result<(), String> {
|
||||
|
||||
println!("=== Red Bear OS Phase 5 Networking Check ===");
|
||||
require_path("/usr/bin/dbus-daemon")?;
|
||||
require_path("/usr/bin/netctl")?;
|
||||
require_path("/usr/bin/dbus-send")?;
|
||||
let netctl_bin = require_one_path(
|
||||
&["/usr/bin/redbear-netctl", "/usr/bin/netctl"],
|
||||
"NETCTL_BIN",
|
||||
)?;
|
||||
require_path("/usr/bin/redbear-wifictl")?;
|
||||
|
||||
let info_output = Command::new("redbear-info")
|
||||
@@ -79,7 +580,7 @@ fn run() -> Result<(), String> {
|
||||
require_json_field(network, "wifi_connect_result")?;
|
||||
require_json_field(network, "wifi_disconnect_result")?;
|
||||
|
||||
let _ = Command::new("netctl").arg("status").status();
|
||||
let _ = Command::new(netctl_bin).arg("status").status();
|
||||
|
||||
let wifictl_output = Command::new("redbear-wifictl")
|
||||
.arg("--probe")
|
||||
@@ -103,6 +604,28 @@ fn run() -> Result<(), String> {
|
||||
return Err("redbear-wifictl --probe did not report capabilities=".to_string());
|
||||
}
|
||||
|
||||
if Path::new("/run/dbus/system_bus_socket").exists() {
|
||||
println!("DBUS_SYSTEM_BUS=present");
|
||||
} else {
|
||||
println!("DBUS_SYSTEM_BUS=missing");
|
||||
}
|
||||
|
||||
let list_names_output = run_command(
|
||||
DBUS_SEND,
|
||||
&[
|
||||
"--system",
|
||||
"--dest=org.freedesktop.DBus",
|
||||
"--type=method_call",
|
||||
"--print-reply",
|
||||
"/org/freedesktop/DBus",
|
||||
"org.freedesktop.DBus.ListNames",
|
||||
],
|
||||
"dbus-send org.freedesktop.DBus.ListNames",
|
||||
)?;
|
||||
|
||||
validate_upower(&list_names_output)?;
|
||||
validate_udisks(&list_names_output)?;
|
||||
|
||||
let wifi_check_output = Command::new("redbear-phase5-wifi-check")
|
||||
.output()
|
||||
.map_err(|err| format!("failed to run redbear-phase5-wifi-check: {err}"))?;
|
||||
@@ -121,7 +644,7 @@ fn run() -> Result<(), String> {
|
||||
));
|
||||
}
|
||||
let wifi_check_stdout = String::from_utf8_lossy(&wifi_check_output.stdout);
|
||||
if wifi_check_stdout.contains("PASS: bounded Intel Wi-Fi runtime path exercised on target") {
|
||||
if contains_phase5_wifi_success(&wifi_check_stdout) {
|
||||
println!("PHASE5_WIFI_CHECK=pass");
|
||||
} else {
|
||||
return Err(
|
||||
@@ -143,11 +666,6 @@ fn run() -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
if Path::new("/run/dbus/system_bus_socket").exists() {
|
||||
println!("DBUS_SYSTEM_BUS=present");
|
||||
} else {
|
||||
println!("DBUS_SYSTEM_BUS=missing");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -173,4 +691,61 @@ mod tests {
|
||||
let value: Value = serde_json::json!({"wifi_control_state": "present"});
|
||||
assert!(require_json_field(&value, "wifi_connect_result").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quoted_values_with_prefix_collects_expected_strings() {
|
||||
let output = r#"
|
||||
object path "/org/freedesktop/UPower/devices/line_power_AC"
|
||||
string "/scheme/acpi/power/adapters/AC"
|
||||
object path "/org/freedesktop/UDisks2/block_devices/disk_2e_nvme_0"
|
||||
"#;
|
||||
|
||||
let upower_paths = quoted_values_with_prefix(output, "/org/freedesktop/UPower/devices/");
|
||||
assert!(upower_paths.contains("/org/freedesktop/UPower/devices/line_power_AC"));
|
||||
|
||||
let disk_paths = quoted_values_with_prefix(output, UDISKS_BLOCK_PREFIX);
|
||||
assert!(disk_paths.contains("/org/freedesktop/UDisks2/block_devices/disk_2e_nvme_0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_object_keys_with_prefix_ignores_property_object_paths() {
|
||||
let output = r#"
|
||||
dict entry(
|
||||
object path "/org/freedesktop/UDisks2/block_devices/disk_2e_nvme_0"
|
||||
array [
|
||||
dict entry(
|
||||
string "org.freedesktop.UDisks2.Block"
|
||||
array [
|
||||
dict entry(
|
||||
string "Drive"
|
||||
variant object path "/org/freedesktop/UDisks2/drives/disk_2e_nvme_0"
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
"#;
|
||||
|
||||
let managed_blocks = managed_object_keys_with_prefix(output, UDISKS_BLOCK_PREFIX);
|
||||
assert_eq!(managed_blocks.len(), 1);
|
||||
assert!(managed_blocks.contains("/org/freedesktop/UDisks2/block_devices/disk_2e_nvme_0"));
|
||||
|
||||
let managed_drives = managed_object_keys_with_prefix(output, UDISKS_DRIVE_PREFIX);
|
||||
assert!(managed_drives.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stable_object_name_matches_udisks_inventory_encoding() {
|
||||
assert_eq!(stable_object_name("disk.nvme", "0p1"), "disk_2envme_0p1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contains_phase5_wifi_success_accepts_current_and_legacy_markers() {
|
||||
assert!(contains_phase5_wifi_success(
|
||||
"PASS: bounded Intel Wi-Fi runtime path exercised inside target runtime"
|
||||
));
|
||||
assert!(contains_phase5_wifi_success(
|
||||
"PASS: bounded Intel Wi-Fi runtime path exercised on target"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,30 @@
|
||||
use std::path::Path;
|
||||
use std::process::{self, Command};
|
||||
use std::process::{self, Command, Output};
|
||||
|
||||
use redbear_hwutils::parse_args;
|
||||
|
||||
const PROGRAM: &str = "redbear-phase6-kde-check";
|
||||
const USAGE: &str = "Usage: redbear-phase6-kde-check\n\nShow the installed Phase 6 KDE session surface inside the guest.";
|
||||
|
||||
const DBUS_ERROR_MARKERS: &[&str] = &[
|
||||
"org.freedesktop.DBus.Error",
|
||||
"QDBusError",
|
||||
"Could not connect to D-Bus",
|
||||
"ServiceUnknown",
|
||||
"No such interface",
|
||||
"NoReply",
|
||||
"UnknownMethod",
|
||||
];
|
||||
|
||||
const SOLID_CONSUMER_MARKERS: &[&str] = &[
|
||||
"StorageAccess.",
|
||||
"StorageDrive.",
|
||||
"StorageVolume.",
|
||||
"OpticalDrive.",
|
||||
"AcAdapter.",
|
||||
"Battery.",
|
||||
];
|
||||
|
||||
fn require_path(path: &str) -> Result<(), String> {
|
||||
if Path::new(path).exists() {
|
||||
println!("{path}");
|
||||
@@ -15,6 +34,167 @@ fn require_path(path: &str) -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn command_output(command: &mut Command, description: &str) -> Result<Output, String> {
|
||||
command
|
||||
.output()
|
||||
.map_err(|err| format!("failed to run {description}: {err}"))
|
||||
}
|
||||
|
||||
fn output_text(output: &Output) -> String {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
format!("{stdout}{stderr}")
|
||||
}
|
||||
|
||||
fn require_success(output: &Output, description: &str) -> Result<(), String> {
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
let text = output_text(output);
|
||||
let detail = text.trim();
|
||||
if detail.is_empty() {
|
||||
Err(format!(
|
||||
"{description} exited with status {}",
|
||||
output.status
|
||||
))
|
||||
} else {
|
||||
Err(format!(
|
||||
"{description} exited with status {}: {}",
|
||||
output.status, detail
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn contains_dbus_error(text: &str) -> bool {
|
||||
DBUS_ERROR_MARKERS
|
||||
.iter()
|
||||
.any(|marker| text.contains(marker))
|
||||
}
|
||||
|
||||
fn contains_solid_consumer_surface(text: &str) -> bool {
|
||||
SOLID_CONSUMER_MARKERS
|
||||
.iter()
|
||||
.any(|marker| text.contains(marker))
|
||||
}
|
||||
|
||||
fn require_dbus_free_output(output: &Output, description: &str) -> Result<(), String> {
|
||||
let text = output_text(output);
|
||||
if contains_dbus_error(&text) {
|
||||
Err(format!(
|
||||
"{description} reported a D-Bus error: {}",
|
||||
text.trim()
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn check_system_bus_consumers() -> Result<(), String> {
|
||||
if !Path::new("/usr/bin/dbus-send").exists() {
|
||||
println!("PHASE6_DBUS_SEND=missing");
|
||||
println!("PHASE6_UPOWER_ENUMERATE=skipped_missing_dbus_send");
|
||||
println!("PHASE6_UDISKS2_OBJECTS=skipped_missing_dbus_send");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("/usr/bin/dbus-send");
|
||||
|
||||
let names = command_output(
|
||||
Command::new("dbus-send")
|
||||
.arg("--system")
|
||||
.arg("--dest=org.freedesktop.DBus")
|
||||
.arg("--type=method_call")
|
||||
.arg("--print-reply")
|
||||
.arg("/org/freedesktop/DBus")
|
||||
.arg("org.freedesktop.DBus.ListNames"),
|
||||
"dbus-send ListNames",
|
||||
)?;
|
||||
require_success(&names, "dbus-send ListNames")?;
|
||||
let names_text = output_text(&names);
|
||||
let upower = command_output(
|
||||
Command::new("dbus-send")
|
||||
.arg("--system")
|
||||
.arg("--dest=org.freedesktop.UPower")
|
||||
.arg("--type=method_call")
|
||||
.arg("--print-reply")
|
||||
.arg("/org/freedesktop/UPower")
|
||||
.arg("org.freedesktop.UPower.EnumerateDevices"),
|
||||
"dbus-send UPower EnumerateDevices",
|
||||
)?;
|
||||
require_success(&upower, "dbus-send UPower EnumerateDevices")?;
|
||||
require_dbus_free_output(&upower, "dbus-send UPower EnumerateDevices")?;
|
||||
if names_text.contains("org.freedesktop.UPower") {
|
||||
println!("PHASE6_UPOWER_BUS_NAME=present");
|
||||
} else {
|
||||
println!("PHASE6_UPOWER_BUS_NAME=activated_lazily");
|
||||
}
|
||||
println!("PHASE6_UPOWER_ENUMERATE=ok");
|
||||
|
||||
let udisks = command_output(
|
||||
Command::new("dbus-send")
|
||||
.arg("--system")
|
||||
.arg("--dest=org.freedesktop.UDisks2")
|
||||
.arg("--type=method_call")
|
||||
.arg("--print-reply")
|
||||
.arg("/org/freedesktop/UDisks2")
|
||||
.arg("org.freedesktop.DBus.ObjectManager.GetManagedObjects"),
|
||||
"dbus-send UDisks2 GetManagedObjects",
|
||||
)?;
|
||||
require_success(&udisks, "dbus-send UDisks2 GetManagedObjects")?;
|
||||
require_dbus_free_output(&udisks, "dbus-send UDisks2 GetManagedObjects")?;
|
||||
if names_text.contains("org.freedesktop.UDisks2") {
|
||||
println!("PHASE6_UDISKS2_BUS_NAME=present");
|
||||
} else {
|
||||
println!("PHASE6_UDISKS2_BUS_NAME=activated_lazily");
|
||||
}
|
||||
println!("PHASE6_UDISKS2_OBJECTS=ok");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_solid_runtime() -> Result<(), String> {
|
||||
let tool = "/usr/bin/solid-hardware6";
|
||||
if !Path::new(tool).exists() {
|
||||
println!("PHASE6_SOLID_RUNTIME=blocked_missing_tool");
|
||||
println!("PHASE6_SOLID_TODO=solid-hardware6_not_present_in_image");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("{tool}");
|
||||
|
||||
let output = command_output(
|
||||
Command::new("solid-hardware6").arg("list").arg("details"),
|
||||
"solid-hardware6 list details",
|
||||
)?;
|
||||
require_success(&output, "solid-hardware6 list details")?;
|
||||
require_dbus_free_output(&output, "solid-hardware6 list details")?;
|
||||
|
||||
let text = output_text(&output);
|
||||
if contains_solid_consumer_surface(&text) {
|
||||
println!("PHASE6_SOLID_RUNTIME=checked");
|
||||
} else {
|
||||
println!("PHASE6_SOLID_RUNTIME=blocked_missing_storage_or_power_surface");
|
||||
println!("PHASE6_SOLID_TODO=solid-hardware6_did_not_expose_storage_or_power_surfaces");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_redbear_info() -> Result<(), String> {
|
||||
let output = command_output(
|
||||
Command::new("redbear-info").arg("--json"),
|
||||
"redbear-info --json",
|
||||
)?;
|
||||
if !output.stdout.is_empty() {
|
||||
print!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
}
|
||||
if !output.stderr.is_empty() {
|
||||
eprint!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
require_success(&output, "redbear-info --json")
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
parse_args(PROGRAM, USAGE, std::env::args()).map_err(|err| {
|
||||
if err.is_empty() {
|
||||
@@ -28,16 +208,10 @@ fn run() -> Result<(), String> {
|
||||
require_path("/usr/bin/kwin_wayland")?;
|
||||
require_path("/usr/bin/dbus-daemon")?;
|
||||
require_path("/usr/bin/seatd")?;
|
||||
check_system_bus_consumers()?;
|
||||
check_solid_runtime()?;
|
||||
|
||||
let status = Command::new("redbear-info")
|
||||
.arg("--json")
|
||||
.status()
|
||||
.map_err(|err| format!("failed to run redbear-info --json: {err}"))?;
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("redbear-info exited with status {status}"))
|
||||
}
|
||||
check_redbear_info()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
@@ -46,3 +220,32 @@ fn main() {
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn contains_dbus_error_detects_known_markers() {
|
||||
assert!(contains_dbus_error(
|
||||
"QDBusError(org.freedesktop.DBus.Error.ServiceUnknown, service missing)"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contains_dbus_error_ignores_clean_output() {
|
||||
assert!(!contains_dbus_error("method return time=0.0 sender=:1.2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contains_solid_consumer_surface_detects_storage_and_power_interfaces() {
|
||||
assert!(contains_solid_consumer_surface(
|
||||
"StorageDrive.removable = false\nBattery.percentage = 100"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contains_solid_consumer_surface_rejects_unrelated_output() {
|
||||
assert!(!contains_solid_consumer_surface("Processor.number = 8"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user