diff --git a/local/recipes/system/redbear-authd/source/Cargo.toml b/local/recipes/system/redbear-authd/source/Cargo.toml index 6eb1c16c..fb965f94 100644 --- a/local/recipes/system/redbear-authd/source/Cargo.toml +++ b/local/recipes/system/redbear-authd/source/Cargo.toml @@ -8,6 +8,8 @@ name = "redbear-authd" path = "src/main.rs" [dependencies] +redbear-login-protocol = { path = "../../redbear-login-protocol/source" } +rust-argon2 = "3" serde = { version = "1", features = ["derive"] } serde_json = "1" # Pure-Rust SHA-256/SHA-512 crypt verifier for /etc/shadow entries. diff --git a/local/recipes/system/redbear-authd/source/src/main.rs b/local/recipes/system/redbear-authd/source/src/main.rs index f186c63e..6952510f 100644 --- a/local/recipes/system/redbear-authd/source/src/main.rs +++ b/local/recipes/system/redbear-authd/source/src/main.rs @@ -10,7 +10,9 @@ use std::{ time::{Duration, Instant}, }; -use serde::{Deserialize, Serialize}; +use argon2::{self, verify_encoded}; +use redbear_login_protocol::{AuthRequest, AuthResponse}; +use serde::Serialize; use sha_crypt::{PasswordVerifier, ShaCrypt}; #[derive(Debug, PartialEq, Eq)] @@ -49,51 +51,6 @@ struct RuntimeState { failures: Arc>>, } -#[derive(Debug, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -enum AuthRequest { - Authenticate { - request_id: u64, - username: String, - password: String, - vt: u32, - }, - StartSession { - request_id: u64, - username: String, - session: String, - vt: u32, - }, - PowerAction { - request_id: u64, - action: String, - }, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -enum AuthResponse { - AuthenticateResult { - request_id: u64, - ok: bool, - message: String, - }, - SessionResult { - request_id: u64, - ok: bool, - exit_code: Option, - message: String, - }, - PowerResult { - request_id: u64, - ok: bool, - message: String, - }, - Error { - message: String, - }, -} - #[derive(Debug, Serialize)] #[serde(tag = "type", rename_all = "snake_case")] enum SessiondUpdate { @@ -229,6 +186,9 @@ fn verify_shadow_password(password: &str, shadow_hash: &str) -> Result/scheme/nul eval "$(dbus-launch --sh-syntax)" fi -exec kwin_wayland --replace +if [ -n "${KWIN_DRM_DEVICES:-}" ]; then + exec kwin_wayland_wrapper --drm +else + echo "redbear-greeter-compositor: using virtual KWin backend (set KWIN_DRM_DEVICES to enable DRM)" >&2 + exec kwin_wayland_wrapper --virtual +fi diff --git a/local/recipes/system/redbear-greeter/source/src/main.rs b/local/recipes/system/redbear-greeter/source/src/main.rs index d74b9394..69358982 100644 --- a/local/recipes/system/redbear-greeter/source/src/main.rs +++ b/local/recipes/system/redbear-greeter/source/src/main.rs @@ -9,7 +9,7 @@ use std::{ time::{Duration, Instant}, }; -use serde::{Deserialize, Serialize}; +use redbear_login_protocol::{AuthRequest, AuthResponse, GreeterRequest, GreeterResponse}; const GREETER_SOCKET_PATH: &str = "/run/redbear-greeterd.sock"; const AUTH_SOCKET_PATH: &str = "/run/redbear-authd.sock"; @@ -57,88 +57,6 @@ struct GreeterDaemon { restart_attempts: Vec, } -#[derive(Debug, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -enum GreeterRequest { - Hello { version: u32 }, - SubmitLogin { username: String, password: String }, - RequestShutdown, - RequestReboot, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -enum GreeterResponse { - HelloOk { - background: String, - icon: String, - session_name: String, - state: String, - message: String, - }, - LoginResult { - ok: bool, - state: String, - message: String, - }, - ActionResult { - ok: bool, - message: String, - }, - Error { - message: String, - }, -} - -#[derive(Debug, Serialize)] -#[serde(tag = "type", rename_all = "snake_case")] -enum AuthRequest<'a> { - Authenticate { - request_id: u64, - username: &'a str, - password: &'a str, - vt: u32, - }, - StartSession { - request_id: u64, - username: &'a str, - session: &'a str, - vt: u32, - }, - PowerAction { - request_id: u64, - action: &'a str, - }, -} - -#[derive(Debug, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -enum AuthResponse { - AuthenticateResult { - ok: bool, - message: String, - #[allow(dead_code)] - request_id: u64, - }, - SessionResult { - ok: bool, - message: String, - #[allow(dead_code)] - request_id: u64, - #[allow(dead_code)] - exit_code: Option, - }, - PowerResult { - ok: bool, - message: String, - #[allow(dead_code)] - request_id: u64, - }, - Error { - message: String, - }, -} - fn usage() -> &'static str { "Usage: redbear-greeterd [--help]" } @@ -212,7 +130,7 @@ fn change_socket_ownership(path: &Path, uid: u32, gid: u32) -> Result<(), String } } -fn send_auth_request(request: &AuthRequest<'_>) -> Result { +fn send_auth_request(request: &AuthRequest) -> Result { let mut stream = UnixStream::connect(AUTH_SOCKET_PATH) .map_err(|err| format!("failed to connect to {AUTH_SOCKET_PATH}: {err}"))?; let payload = serde_json::to_string(request).map_err(|err| format!("failed to serialize auth request: {err}"))?; @@ -370,8 +288,8 @@ impl GreeterDaemon { let response = send_auth_request(&AuthRequest::StartSession { request_id: 2, - username, - session: "kde-wayland", + username: username.to_string(), + session: String::from("kde-wayland"), vt: self.vt, })?; @@ -390,6 +308,9 @@ impl GreeterDaemon { } fn handle_connection(&mut self, stream: UnixStream) -> Result<(), String> { + stream + .set_nonblocking(false) + .map_err(|err| format!("failed to set blocking greeter stream mode: {err}"))?; let mut reader = BufReader::new(stream); let mut line = String::new(); reader @@ -413,8 +334,8 @@ impl GreeterDaemon { self.set_state(GreeterState::Authenticating, "Authenticating"); match send_auth_request(&AuthRequest::Authenticate { request_id: 1, - username: &username, - password: &password, + username: username.clone(), + password: password.clone(), vt: self.vt, })? { AuthResponse::AuthenticateResult { ok, message, .. } => { @@ -443,7 +364,7 @@ impl GreeterDaemon { self.set_state(GreeterState::PowerAction, "Requesting shutdown"); match send_auth_request(&AuthRequest::PowerAction { request_id: 3, - action: "shutdown", + action: String::from("shutdown"), })? { AuthResponse::PowerResult { ok, message, .. } => GreeterResponse::ActionResult { ok, message }, AuthResponse::Error { message } => GreeterResponse::Error { message }, @@ -456,7 +377,7 @@ impl GreeterDaemon { self.set_state(GreeterState::PowerAction, "Requesting reboot"); match send_auth_request(&AuthRequest::PowerAction { request_id: 4, - action: "reboot", + action: String::from("reboot"), })? { AuthResponse::PowerResult { ok, message, .. } => GreeterResponse::ActionResult { ok, message }, AuthResponse::Error { message } => GreeterResponse::Error { message }, diff --git a/local/recipes/system/redbear-greeter/source/ui/main.cpp b/local/recipes/system/redbear-greeter/source/ui/main.cpp index 1a65f1fb..77a9c70a 100644 --- a/local/recipes/system/redbear-greeter/source/ui/main.cpp +++ b/local/recipes/system/redbear-greeter/source/ui/main.cpp @@ -2,11 +2,15 @@ #include #include #include +#include #include "greeter_backend.h" int main(int argc, char *argv[]) { qputenv("QT_QUICK_CONTROLS_STYLE", QByteArrayLiteral("Basic")); + qputenv("QSG_RHI_BACKEND", QByteArrayLiteral("software")); + qputenv("QT_QUICK_BACKEND", QByteArrayLiteral("software")); + QQuickWindow::setGraphicsApi(QSGRendererInterface::Software); QGuiApplication app(argc, argv); QQuickStyle::setStyle(QStringLiteral("Basic")); diff --git a/local/recipes/system/redbear-hwutils/source/Cargo.toml b/local/recipes/system/redbear-hwutils/source/Cargo.toml index d75994f0..052568a2 100644 --- a/local/recipes/system/redbear-hwutils/source/Cargo.toml +++ b/local/recipes/system/redbear-hwutils/source/Cargo.toml @@ -83,11 +83,20 @@ path = "src/bin/redbear-phase-timer-check.rs" name = "redbear-phase-dma-check" path = "src/bin/redbear-phase-dma-check.rs" +[[bin]] +name = "redbear-phase-acpi-check" +path = "src/bin/redbear-phase-acpi-check.rs" + +[[bin]] +name = "redbear-phase-pci-irq-check" +path = "src/bin/redbear-phase-pci-irq-check.rs" + [[bin]] name = "redbear-usb-check" path = "src/bin/redbear-usb-check.rs" [dependencies] +redbear-login-protocol = { path = "../../redbear-login-protocol/source" } serde = { version = "1", features = ["derive"] } serde_json = "1" orbclient = "0.3" diff --git a/local/recipes/system/redbear-hwutils/source/src/bin/lspci.rs b/local/recipes/system/redbear-hwutils/source/src/bin/lspci.rs index be18d28f..8c93644d 100644 --- a/local/recipes/system/redbear-hwutils/source/src/bin/lspci.rs +++ b/local/recipes/system/redbear-hwutils/source/src/bin/lspci.rs @@ -4,12 +4,12 @@ use std::process; use redbear_hwutils::{ lookup_pci_device_name, lookup_pci_vendor_name, parse_args, parse_pci_location, PciLocation, }; -use redox_driver_sys::pci::PciDeviceInfo; +use redox_driver_sys::pci::{parse_device_info_from_config_space, InterruptSupport, PciDeviceInfo}; use redox_driver_sys::quirks::{lookup_pci_quirks, PciQuirkFlags}; const USAGE: &str = "Usage: lspci\nList PCI devices exposed by /scheme/pci."; -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] struct PciDeviceSummary { location: PciLocation, vendor_id: u16, @@ -20,6 +20,9 @@ struct PciDeviceSummary { revision: u8, subvendor_id: u16, subdevice_id: u16, + irq: Option, + interrupt_support: InterruptSupport, + irq_reason: Option, quirk_flags: PciQuirkFlags, } @@ -36,71 +39,33 @@ fn main() { fn format_quirk_flags(flags: PciQuirkFlags) -> String { let mut names = Vec::new(); - if flags.contains(PciQuirkFlags::NO_MSI) { - names.push("no_msi"); - } - if flags.contains(PciQuirkFlags::NO_MSIX) { - names.push("no_msix"); - } - if flags.contains(PciQuirkFlags::FORCE_LEGACY_IRQ) { - names.push("force_legacy_irq"); - } - if flags.contains(PciQuirkFlags::NO_PM) { - names.push("no_pm"); - } - if flags.contains(PciQuirkFlags::NO_D3COLD) { - names.push("no_d3cold"); - } - if flags.contains(PciQuirkFlags::NO_ASPM) { - names.push("no_aspm"); - } - if flags.contains(PciQuirkFlags::NEED_IOMMU) { - names.push("need_iommu"); - } - if flags.contains(PciQuirkFlags::NO_IOMMU) { - names.push("no_iommu"); - } - if flags.contains(PciQuirkFlags::DMA_32BIT_ONLY) { - names.push("dma_32bit_only"); - } - if flags.contains(PciQuirkFlags::RESIZE_BAR) { - names.push("resize_bar"); - } - if flags.contains(PciQuirkFlags::DISABLE_BAR_SIZING) { - names.push("disable_bar_sizing"); - } - if flags.contains(PciQuirkFlags::NEED_FIRMWARE) { - names.push("need_firmware"); - } - if flags.contains(PciQuirkFlags::DISABLE_ACCEL) { - names.push("disable_accel"); - } - if flags.contains(PciQuirkFlags::FORCE_VRAM_ONLY) { - names.push("force_vram_only"); - } - if flags.contains(PciQuirkFlags::NO_USB3) { - names.push("no_usb3"); - } - if flags.contains(PciQuirkFlags::RESET_DELAY_MS) { - names.push("reset_delay_ms"); - } - if flags.contains(PciQuirkFlags::NO_STRING_FETCH) { - names.push("no_string_fetch"); - } - if flags.contains(PciQuirkFlags::BAD_EEPROM) { - names.push("bad_eeprom"); - } - if flags.contains(PciQuirkFlags::BUS_MASTER_DELAY) { - names.push("bus_master_delay"); - } - if flags.contains(PciQuirkFlags::WRONG_CLASS) { - names.push("wrong_class"); - } - if flags.contains(PciQuirkFlags::BROKEN_BRIDGE) { - names.push("broken_bridge"); - } - if flags.contains(PciQuirkFlags::NO_RESOURCE_RELOC) { - names.push("no_resource_reloc"); + for (flag, name) in [ + (PciQuirkFlags::NO_MSI, "no_msi"), + (PciQuirkFlags::NO_MSIX, "no_msix"), + (PciQuirkFlags::FORCE_LEGACY_IRQ, "force_legacy_irq"), + (PciQuirkFlags::NO_PM, "no_pm"), + (PciQuirkFlags::NO_D3COLD, "no_d3cold"), + (PciQuirkFlags::NO_ASPM, "no_aspm"), + (PciQuirkFlags::NEED_IOMMU, "need_iommu"), + (PciQuirkFlags::NO_IOMMU, "no_iommu"), + (PciQuirkFlags::DMA_32BIT_ONLY, "dma_32bit_only"), + (PciQuirkFlags::RESIZE_BAR, "resize_bar"), + (PciQuirkFlags::DISABLE_BAR_SIZING, "disable_bar_sizing"), + (PciQuirkFlags::NEED_FIRMWARE, "need_firmware"), + (PciQuirkFlags::DISABLE_ACCEL, "disable_accel"), + (PciQuirkFlags::FORCE_VRAM_ONLY, "force_vram_only"), + (PciQuirkFlags::NO_USB3, "no_usb3"), + (PciQuirkFlags::RESET_DELAY_MS, "reset_delay_ms"), + (PciQuirkFlags::NO_STRING_FETCH, "no_string_fetch"), + (PciQuirkFlags::BAD_EEPROM, "bad_eeprom"), + (PciQuirkFlags::BUS_MASTER_DELAY, "bus_master_delay"), + (PciQuirkFlags::WRONG_CLASS, "wrong_class"), + (PciQuirkFlags::BROKEN_BRIDGE, "broken_bridge"), + (PciQuirkFlags::NO_RESOURCE_RELOC, "no_resource_reloc"), + ] { + if flags.contains(flag) { + names.push(name); + } } names.join(",") } @@ -163,6 +128,13 @@ fn run() -> Result<(), String> { if !device.quirk_flags.is_empty() { print!(" quirks: {}", format_quirk_flags(device.quirk_flags)); } + print!(" irq-support: {}", device.interrupt_support.as_str()); + if let Some(line) = device.irq { + print!(" line={line}"); + } + if let Some(reason) = &device.irq_reason { + print!(" reason={reason}"); + } println!(); } @@ -195,47 +167,56 @@ fn collect_devices() -> Result, String> { Err(_) => continue, }; - if config.len() < 16 { + if config.len() < 64 { continue; } - let vendor_id = u16::from_le_bytes([config[0x00], config[0x01]]); - let device_id = u16::from_le_bytes([config[0x02], config[0x03]]); - let revision = config[0x08]; - let prog_if = config[0x09]; - let subclass = config[0x0A]; - let class_code = config[0x0B]; - - let (subvendor_id, subdevice_id) = if config.len() >= 0x30 { - ( - u16::from_le_bytes([config[0x2C], config[0x2D]]), - u16::from_le_bytes([config[0x2E], config[0x2F]]), - ) - } else { - (0xFFFF, 0xFFFF) + let info = match parse_device_info_from_config_space( + redox_driver_sys::pci::PciLocation { + segment: location.segment, + bus: location.bus, + device: location.device, + function: location.function, + }, + &config, + ) { + Some(info) => info, + None => continue, }; let quirk_flags = lookup_quirks( - vendor_id, - device_id, - revision, - class_code, - subclass, - prog_if, - subvendor_id, - subdevice_id, + info.vendor_id, + info.device_id, + info.revision, + info.class_code, + info.subclass, + info.prog_if, + info.subsystem_vendor_id, + info.subsystem_device_id, ); + let irq_reason = if quirk_flags.contains(PciQuirkFlags::FORCE_LEGACY_IRQ) { + Some("quirk_force_legacy_irq".to_string()) + } else if quirk_flags.contains(PciQuirkFlags::NO_MSIX) { + Some("quirk_disable_msix".to_string()) + } else if quirk_flags.contains(PciQuirkFlags::NO_MSI) { + Some("quirk_disable_msi".to_string()) + } else { + None + }; devices.push(PciDeviceSummary { location, - vendor_id, - device_id, - revision, - prog_if, - subclass, - class_code, - subvendor_id, - subdevice_id, + vendor_id: info.vendor_id, + device_id: info.device_id, + revision: info.revision, + prog_if: info.prog_if, + subclass: info.subclass, + class_code: info.class_code, + subvendor_id: info.subsystem_vendor_id, + subdevice_id: info.subsystem_device_id, + irq: info.irq, + interrupt_support: info.interrupt_support(), + irq_reason, quirk_flags, }); } diff --git a/local/recipes/system/redbear-hwutils/source/src/bin/redbear-greeter-check.rs b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-greeter-check.rs index 17c51a47..c2b68e18 100644 --- a/local/recipes/system/redbear-hwutils/source/src/bin/redbear-greeter-check.rs +++ b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-greeter-check.rs @@ -8,7 +8,7 @@ use std::{ time::{Duration, Instant}, }; -use serde::{Deserialize, Serialize}; +use redbear_login_protocol::{GreeterRequest as Request, GreeterResponse}; const PROGRAM: &str = "redbear-greeter-check"; const USAGE: &str = "Usage: redbear-greeter-check [--invalid USER PASSWORD | --valid USER PASSWORD]\n\nQuery the installed Red Bear greeter surface inside the guest."; @@ -28,35 +28,6 @@ const DEBUG_CONSOLE_SERVICE: &str = "/usr/lib/init.d/31_debug_console.service"; const VALIDATION_REQUEST: &str = "/run/redbear-kde-session.validation-request"; const VALIDATION_SUCCESS: &str = "/run/redbear-kde-session.validation-success"; -#[derive(Debug, Serialize)] -#[serde(tag = "type", rename_all = "snake_case")] -enum Request<'a> { - Hello { version: u32 }, - SubmitLogin { username: &'a str, password: &'a str }, -} - -#[derive(Debug, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -enum Response { - HelloOk { - background: String, - icon: String, - session_name: String, - state: String, - message: String, - }, - LoginResult { - ok: bool, - state: String, - message: String, - }, - Error { - message: String, - }, - #[serde(other)] - Other, -} - #[derive(Debug, PartialEq, Eq)] enum Mode { Status, @@ -64,6 +35,21 @@ enum Mode { Valid { username: String, password: String }, } +fn parse_credentials(args: &mut impl Iterator, flag: &str) -> Result<(String, String), String> { + let username = args + .next() + .ok_or_else(|| format!("missing username after {flag}"))?; + let password = args + .next() + .ok_or_else(|| format!("missing password after {flag}"))?; + if args.next().is_some() { + return Err(format!( + "unexpected extra arguments after {flag} USER PASSWORD" + )); + } + Ok((username, password)) +} + fn parse_mode_from_args(args: I) -> Result where I: IntoIterator, @@ -73,19 +59,11 @@ where None => Ok(Mode::Status), Some(flag) if flag == "--help" || flag == "-h" => Err(String::new()), Some(flag) if flag == "--invalid" => { - let username = args.next().ok_or_else(|| String::from("missing username after --invalid"))?; - let password = args.next().ok_or_else(|| String::from("missing password after --invalid"))?; - if args.next().is_some() { - return Err(String::from("unexpected extra arguments after --invalid USER PASSWORD")); - } + let (username, password) = parse_credentials(&mut args, &flag)?; Ok(Mode::Invalid { username, password }) } Some(flag) if flag == "--valid" => { - let username = args.next().ok_or_else(|| String::from("missing username after --valid"))?; - let password = args.next().ok_or_else(|| String::from("missing password after --valid"))?; - if args.next().is_some() { - return Err(String::from("unexpected extra arguments after --valid USER PASSWORD")); - } + let (username, password) = parse_credentials(&mut args, &flag)?; Ok(Mode::Valid { username, password }) } Some(other) => Err(format!("unsupported argument '{other}'")), @@ -96,7 +74,7 @@ fn parse_mode() -> Result { parse_mode_from_args(std::env::args().skip(1)) } -fn send_request(request: &Request<'_>) -> Result { +fn send_request(request: &Request) -> Result { let mut stream = UnixStream::connect(GREETER_SOCKET) .map_err(|err| format!("failed to connect to {GREETER_SOCKET}: {err}"))?; let payload = serde_json::to_string(request) @@ -139,7 +117,7 @@ fn wait_for_greeter_ready(timeout: Duration) -> Result<(), String> { let start = Instant::now(); while start.elapsed() <= timeout { match send_request(&Request::Hello { version: 1 }) { - Ok(Response::HelloOk { state, message, .. }) if state == "greeter_ready" => { + Ok(GreeterResponse::HelloOk { state, message, .. }) if state == "greeter_ready" => { println!("GREETER_VALID_READY_MESSAGE={message}"); return Ok(()); } @@ -169,7 +147,7 @@ fn run_status() -> Result<(), String> { require_path(GREETER_SOCKET)?; match send_request(&Request::Hello { version: 1 })? { - Response::HelloOk { + GreeterResponse::HelloOk { background, icon, session_name, @@ -184,15 +162,18 @@ fn run_status() -> Result<(), String> { println!("GREETER_HELLO=ok"); Ok(()) } - Response::Error { message } => Err(format!("greeter hello failed: {message}")), - Response::Other => Err(String::from("unexpected greeter hello response")), - Response::LoginResult { .. } => Err(String::from("unexpected login result when greeting greeter")), + GreeterResponse::Error { message } => Err(format!("greeter hello failed: {message}")), + GreeterResponse::ActionResult { .. } => Err(String::from("unexpected power response when greeting greeter")), + GreeterResponse::LoginResult { .. } => Err(String::from("unexpected login result when greeting greeter")), } } fn run_invalid(username: &str, password: &str) -> Result<(), String> { - match send_request(&Request::SubmitLogin { username, password })? { - Response::LoginResult { ok, state, message } => { + match send_request(&Request::SubmitLogin { + username: username.to_string(), + password: password.to_string(), + })? { + GreeterResponse::LoginResult { ok, state, message } => { println!("GREETER_INVALID_STATE={state}"); println!("GREETER_INVALID_MESSAGE={message}"); if ok { @@ -202,9 +183,9 @@ fn run_invalid(username: &str, password: &str) -> Result<(), String> { Ok(()) } } - Response::Error { message } => Err(format!("invalid-login request failed: {message}")), - Response::Other => Err(String::from("unexpected greeter response for invalid login")), - Response::HelloOk { .. } => Err(String::from("unexpected hello response for invalid login")), + GreeterResponse::Error { message } => Err(format!("invalid-login request failed: {message}")), + GreeterResponse::ActionResult { .. } => Err(String::from("unexpected power response for invalid login")), + GreeterResponse::HelloOk { .. } => Err(String::from("unexpected hello response for invalid login")), } } @@ -214,8 +195,11 @@ fn run_valid(username: &str, password: &str) -> Result<(), String> { fs::write(VALIDATION_REQUEST, b"bounded-session\n") .map_err(|err| format!("failed to create validation request: {err}"))?; - match send_request(&Request::SubmitLogin { username, password })? { - Response::LoginResult { ok, state, message } => { + match send_request(&Request::SubmitLogin { + username: username.to_string(), + password: password.to_string(), + })? { + GreeterResponse::LoginResult { ok, state, message } => { println!("GREETER_VALID_STATE={state}"); println!("GREETER_VALID_MESSAGE={message}"); if !ok { @@ -223,15 +207,15 @@ fn run_valid(username: &str, password: &str) -> Result<(), String> { return Err(String::from("valid login unexpectedly failed")); } } - Response::Error { message } => { + GreeterResponse::Error { message } => { let _ = fs::remove_file(VALIDATION_REQUEST); return Err(format!("valid-login request failed: {message}")); } - Response::Other => { + GreeterResponse::ActionResult { .. } => { let _ = fs::remove_file(VALIDATION_REQUEST); - return Err(String::from("unexpected greeter response for valid login")); + return Err(String::from("unexpected power response for valid login")); } - Response::HelloOk { .. } => { + GreeterResponse::HelloOk { .. } => { let _ = fs::remove_file(VALIDATION_REQUEST); return Err(String::from("unexpected hello response for valid login")); } diff --git a/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase-acpi-check.rs b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase-acpi-check.rs new file mode 100644 index 00000000..04f57992 --- /dev/null +++ b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase-acpi-check.rs @@ -0,0 +1,197 @@ +use std::{ + fs, + path::{Path, PathBuf}, + process, +}; + +use redbear_hwutils::parse_args; + +const PROGRAM: &str = "redbear-phase-acpi-check"; +const USAGE: &str = "Usage: redbear-phase-acpi-check\n\nShow the bounded ACPI runtime surface inside the target runtime."; + +#[derive(Debug, Default, Eq, PartialEq)] +struct AcpiSurface { + acpi_root_present: bool, + kernel_kstop_present: bool, + dmi_present: bool, + reboot_present: bool, + power_present: bool, + adapter_count: usize, + battery_count: usize, + dmi_match_lines: usize, +} + +fn root_prefix() -> PathBuf { + std::env::var_os("REDBEAR_HWUTILS_ROOT") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("/")) +} + +fn resolve(root: &Path, absolute: &str) -> PathBuf { + root.join(absolute.trim_start_matches('/')) +} + +fn read_dir_names(path: &Path) -> Vec { + 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::>(), + Err(_) => Vec::new(), + }; + names.sort(); + names +} + +fn discover_surface(root: &Path) -> AcpiSurface { + let acpi_root = resolve(root, "/scheme/acpi"); + let kernel_kstop = resolve(root, "/scheme/kernel.acpi/kstop"); + let dmi = resolve(root, "/scheme/acpi/dmi"); + let reboot = resolve(root, "/scheme/acpi/reboot"); + let power = resolve(root, "/scheme/acpi/power"); + + let dmi_match_lines = fs::read_to_string(&dmi) + .ok() + .map(|content| { + content + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + .count() + }) + .unwrap_or(0); + + let adapter_count = read_dir_names(&power.join("adapters")).len(); + let battery_count = read_dir_names(&power.join("batteries")).len(); + + AcpiSurface { + acpi_root_present: acpi_root.exists(), + kernel_kstop_present: kernel_kstop.exists(), + dmi_present: dmi.exists(), + reboot_present: reboot.exists(), + power_present: power.exists(), + adapter_count, + battery_count, + dmi_match_lines, + } +} + +fn run() -> Result<(), String> { + parse_args(PROGRAM, USAGE, std::env::args())?; + + let root = root_prefix(); + let surface = discover_surface(&root); + + println!( + "ACPI_ROOT={}", + if surface.acpi_root_present { "present" } else { "missing" } + ); + println!( + "KERNEL_KSTOP={}", + if surface.kernel_kstop_present { + "present" + } else { + "missing" + } + ); + println!("ACPI_DMI={}", if surface.dmi_present { "present" } else { "missing" }); + println!( + "ACPI_REBOOT={}", + if surface.reboot_present { + "present" + } else { + "missing" + } + ); + println!( + "ACPI_POWER={}", + if surface.power_present { + "present" + } else { + "unavailable" + } + ); + println!("ACPI_POWER_ADAPTERS={}", surface.adapter_count); + println!("ACPI_POWER_BATTERIES={}", surface.battery_count); + println!("ACPI_DMI_MATCH_LINES={}", surface.dmi_match_lines); + + if !surface.kernel_kstop_present { + return Err("missing /scheme/kernel.acpi/kstop".to_string()); + } + if !surface.dmi_present { + return Err("missing /scheme/acpi/dmi".to_string()); + } + if !surface.reboot_present { + return Err("missing /scheme/acpi/reboot".to_string()); + } + + Ok(()) +} + +fn main() { + if let Err(err) = run() { + eprintln!("{PROGRAM}: {err}"); + process::exit(1); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{SystemTime, UNIX_EPOCH}; + + fn temp_root() -> PathBuf { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = std::env::temp_dir().join(format!("redbear-phase-acpi-check-{unique}")); + fs::create_dir_all(&path).unwrap(); + path + } + + fn create_dir(root: &Path, absolute: &str) { + fs::create_dir_all(resolve(root, absolute)).unwrap(); + } + + fn write_file(root: &Path, absolute: &str, content: &str) { + let path = resolve(root, absolute); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).unwrap(); + } + fs::write(path, content).unwrap(); + } + + #[test] + fn discover_surface_marks_optional_power_as_unavailable() { + let root = temp_root(); + create_dir(&root, "/scheme/acpi"); + write_file(&root, "/scheme/kernel.acpi/kstop", "1"); + write_file(&root, "/scheme/acpi/dmi", "sys_vendor=Framework\n"); + write_file(&root, "/scheme/acpi/reboot", ""); + + let surface = discover_surface(&root); + assert!(surface.acpi_root_present); + assert!(surface.kernel_kstop_present); + assert!(surface.dmi_present); + assert!(surface.reboot_present); + assert!(!surface.power_present); + assert_eq!(surface.dmi_match_lines, 1); + + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn discover_surface_counts_power_entries_when_present() { + let root = temp_root(); + create_dir(&root, "/scheme/acpi/power/adapters/AC"); + create_dir(&root, "/scheme/acpi/power/batteries/BAT0"); + + let surface = discover_surface(&root); + assert!(surface.power_present); + assert_eq!(surface.adapter_count, 1); + assert_eq!(surface.battery_count, 1); + + fs::remove_dir_all(root).unwrap(); + } +} diff --git a/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase-pci-irq-check.rs b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase-pci-irq-check.rs new file mode 100644 index 00000000..20c6f1db --- /dev/null +++ b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase-pci-irq-check.rs @@ -0,0 +1,196 @@ +use std::{ + fs, + path::{Path, PathBuf}, + process, +}; + +use redbear_hwutils::parse_args; + +const PROGRAM: &str = "redbear-phase-pci-irq-check"; +const USAGE: &str = "Usage: redbear-phase-pci-irq-check\n\nShow bounded live PCI/IRQ runtime reporting from the current target runtime."; + +#[derive(Debug, Clone, Eq, PartialEq)] +struct IrqReport { + driver: String, + pid: u32, + device: String, + mode: String, + reason: String, +} + +fn root_prefix() -> PathBuf { + std::env::var_os("REDBEAR_HWUTILS_ROOT") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("/")) +} + +fn resolve(root: &Path, absolute: &str) -> PathBuf { + root.join(absolute.trim_start_matches('/')) +} + +fn read_dir_names(path: &Path) -> Vec { + 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::>(), + Err(_) => Vec::new(), + }; + names.sort(); + names +} + +fn collect_irq_reports(root: &Path) -> Vec { + 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", + ] { + for name in read_dir_names(&resolve(root, dir)) + .into_iter() + .filter(|name| name.ends_with(".env")) + { + let path = resolve(root, &format!("{dir}/{name}")); + if !seen.insert(path.clone()) { + continue; + } + let Ok(content) = fs::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::().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 !resolve(root, &format!("/proc/{pid}")).exists() { + continue; + } + reports.push(IrqReport { + driver, + pid, + device, + mode, + reason, + }); + } + } + } + + reports.sort_by(|left, right| left.driver.cmp(&right.driver).then(left.device.cmp(&right.device))); + reports +} + +fn run() -> Result<(), String> { + parse_args(PROGRAM, USAGE, std::env::args())?; + let root = root_prefix(); + let reports = collect_irq_reports(&root); + + println!("PCI_IRQ_REPORTS={}", reports.len()); + for report in &reports { + println!( + "PCI_IRQ_REPORT={} pid={} device={} mode={} reason={}", + report.driver, report.pid, report.device, report.mode, report.reason + ); + } + + if reports.is_empty() { + return Err("no live PCI/IRQ runtime reports found".to_string()); + } + + Ok(()) +} + +fn main() { + if let Err(err) = run() { + eprintln!("{PROGRAM}: {err}"); + process::exit(1); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{SystemTime, UNIX_EPOCH}; + + fn temp_root() -> PathBuf { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = std::env::temp_dir().join(format!("redbear-phase-pci-irq-check-{unique}")); + fs::create_dir_all(&path).unwrap(); + path + } + + fn create_dir(root: &Path, absolute: &str) { + fs::create_dir_all(resolve(root, absolute)).unwrap(); + } + + fn write_file(root: &Path, absolute: &str, content: &str) { + let path = resolve(root, absolute); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).unwrap(); + } + fs::write(path, content).unwrap(); + } + + #[test] + fn collect_irq_reports_uses_live_pid_entries() { + let root = temp_root(); + create_dir(&root, "/proc/42"); + write_file( + &root, + "/tmp/redbear-irq-report/xhcid--0000_00_14.0.env", + "driver=xhcid\npid=42\ndevice=0000:00:14.0\nmode=msi_or_msix\nreason=driver_selected_interrupt_delivery\n", + ); + + let reports = collect_irq_reports(&root); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].driver, "xhcid"); + assert_eq!(reports[0].mode, "msi_or_msix"); + + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn collect_irq_reports_ignores_stale_entries() { + let root = temp_root(); + write_file( + &root, + "/scheme/initfs/tmp/redbear-irq-report/virtio-netd--0000_00_03.0.env", + "driver=virtio-netd\npid=99\ndevice=0000:00:03.0\nmode=msix\nreason=virtio_driver_selected_msix\n", + ); + + let reports = collect_irq_reports(&root); + assert!(reports.is_empty()); + + fs::remove_dir_all(root).unwrap(); + } +} diff --git a/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase-timer-check.rs b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase-timer-check.rs index 444294c5..f6eb35c2 100644 --- a/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase-timer-check.rs +++ b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase-timer-check.rs @@ -19,6 +19,20 @@ fn require_path(path: &str) -> Result<(), String> { } } +fn monotonic_path() -> Result { + let numeric = format!("/scheme/time/{}", flag::CLOCK_MONOTONIC); + if require_path(&numeric).is_ok() { + return Ok(numeric); + } + + let symbolic = "/scheme/time/CLOCK_MONOTONIC".to_string(); + if require_path(&symbolic).is_ok() { + return Ok(symbolic); + } + + Err(format!("missing {numeric} and {symbolic}")) +} + fn read_timespec(fd: &Fd) -> Result { let mut time = TimeSpec::default(); let bytes = libredox::call::read(fd.raw(), &mut time) @@ -43,8 +57,7 @@ fn run() -> Result<(), String> { println!("=== Red Bear OS Timer Runtime Check ==="); - let time_path = format!("/scheme/time/{}", flag::CLOCK_MONOTONIC); - require_path(&time_path)?; + let time_path = monotonic_path()?; let time_fd = Fd::open(&time_path, flag::O_RDWR, 0) .map_err(|err| format!("failed to open {time_path}: {err}"))?; diff --git a/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase5-network-check.rs b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase5-network-check.rs index 7c1c0f3f..ebf47589 100644 --- a/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase5-network-check.rs +++ b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase5-network-check.rs @@ -274,6 +274,10 @@ impl PowerRuntime { } } + fn available(&self) -> bool { + Path::new(POWER_ROOT).exists() + } + fn expected_device_paths(&self) -> BTreeSet { let mut paths = BTreeSet::new(); for adapter_id in &self.adapter_ids { @@ -289,8 +293,13 @@ impl PowerRuntime { fn validate_upower(list_names_output: &str) -> Result<(), String> { let runtime = PowerRuntime::discover(); let expected_device_paths = runtime.expected_device_paths(); + let power_surface_available = runtime.available(); println!("UPOWER_RUNTIME_ADAPTERS={}", runtime.adapter_ids.len()); println!("UPOWER_RUNTIME_BATTERIES={}", runtime.battery_ids.len()); + println!( + "UPOWER_POWER_SURFACE={}", + if power_surface_available { "available" } else { "unavailable" } + ); let enumerate_output = run_command_with_retry( DBUS_SEND, @@ -314,6 +323,18 @@ fn validate_upower(list_names_output: &str) -> Result<(), String> { ); note_bus_name_registered(list_names_output, UPOWER_DESTINATION, "UPOWER_BUS_NAME"); + if !power_surface_available { + if !enumerated_device_paths.is_empty() { + return Err(format!( + "UPower enumerated devices even though /scheme/acpi/power is unavailable: {}", + summarize_set(&enumerated_device_paths) + )); + } + + println!("UPOWER_NATIVE_PATHS=skipped (power surface unavailable)"); + return Ok(()); + } + let missing_device_paths = expected_device_paths .difference(&enumerated_device_paths) .cloned() @@ -736,6 +757,13 @@ mod tests { assert!(disk_paths.contains("/org/freedesktop/UDisks2/block_devices/disk_2e_nvme_0")); } + #[test] + fn power_runtime_without_surface_is_marked_unavailable() { + let runtime = PowerRuntime::default(); + assert!(!runtime.available()); + assert!(runtime.expected_device_paths().is_empty()); + } + #[test] fn managed_object_keys_with_prefix_ignores_property_object_paths() { let output = r#" diff --git a/local/recipes/system/redbear-info/source/Cargo.toml b/local/recipes/system/redbear-info/source/Cargo.toml index 521053e0..809fc519 100644 --- a/local/recipes/system/redbear-info/source/Cargo.toml +++ b/local/recipes/system/redbear-info/source/Cargo.toml @@ -8,4 +8,5 @@ name = "redbear-info" path = "src/main.rs" [dependencies] +redox-driver-sys = { path = "../../../../recipes/drivers/redox-driver-sys/source" } toml = "0.8" diff --git a/local/recipes/system/redbear-info/source/src/main.rs b/local/recipes/system/redbear-info/source/src/main.rs index 59b58369..ac505cb0 100644 --- a/local/recipes/system/redbear-info/source/src/main.rs +++ b/local/recipes/system/redbear-info/source/src/main.rs @@ -5,6 +5,8 @@ use std::path::PathBuf; use std::process; use std::time::{SystemTime, UNIX_EPOCH}; +use redox_driver_sys::pci::{parse_device_info_from_config_space, InterruptSupport}; +use redox_driver_sys::quirks::{lookup_pci_quirks, PciQuirkFlags}; use toml::Value; #[cfg(test)] @@ -92,12 +94,29 @@ struct NetworkReport { struct HardwareReport { pci_devices: usize, + pci_irq_none: usize, + pci_irq_legacy: usize, + pci_irq_msi: usize, + pci_irq_msix: usize, + pci_irq_forced_legacy: usize, + pci_irq_msix_disabled_by_quirk: usize, + pci_irq_msi_disabled_by_quirk: usize, + runtime_irq_reports: Vec, 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, @@ -333,6 +352,16 @@ const INTEGRATIONS: &[IntegrationCheck] = &[ note: "Functional when the firmware scheme is enumerable.", functional_probe: Some(probe_firmware_scheme), }, + IntegrationCheck { + name: "redbear-upower", + category: "Power", + description: "Bounded UPower-compatible power reporting daemon", + artifact_path: Some("/usr/bin/redbear-upower"), + control_path: Some("/scheme/acpi/power"), + test_hint: "redbear-phase5-network-check", + note: "Binary presence proves the daemon is installed; a live /scheme/acpi/power surface proves bounded ACPI-backed power reporting is actually available.", + functional_probe: Some(probe_acpi_power_surface), + }, IntegrationCheck { name: "iommu", category: "System", @@ -855,6 +884,14 @@ fn collect_hardware(runtime: &Runtime, network: &NetworkReport) -> HardwareRepor .iter() .filter(|entry| entry.contains("--") && entry.contains('.')) .count(); + let mut pci_irq_none = 0; + let mut pci_irq_legacy = 0; + let mut pci_irq_msi = 0; + let mut pci_irq_msix = 0; + let mut pci_irq_forced_legacy = 0; + let mut pci_irq_msix_disabled_by_quirk = 0; + let mut pci_irq_msi_disabled_by_quirk = 0; + let mut rtl8125_present_from_pci = false; let usb_controllers = runtime .read_dir_names("/scheme") @@ -869,19 +906,42 @@ fn collect_hardware(runtime: &Runtime, network: &NetworkReport) -> HardwareRepor .into_iter() .filter(|name| name.starts_with("card")) .count(); + let runtime_irq_reports = collect_irq_runtime_reports(runtime); + let acpi_power_surface_present = runtime.exists("/scheme/acpi/power"); - let rtl8125_present = pci_entries.into_iter().any(|entry| { + for entry in &pci_entries { let config_path = format!("/scheme/pci/{entry}/config"); - let Some(bytes) = read_prefix_bytes(runtime, &config_path, 4) else { - return false; + let Some(bytes) = read_prefix_bytes(runtime, &config_path, 64) else { + continue; }; - if bytes.len() < 4 { - return false; + if bytes.len() < 64 { + continue; } - let vendor = u16::from_le_bytes([bytes[0], bytes[1]]); - let device = u16::from_le_bytes([bytes[2], bytes[3]]); - vendor == RTL8125_VENDOR_ID && device == RTL8125_DEVICE_ID - }) || network + if let Some(location) = parse_scheme_pci_location(entry) { + if let Some(info) = parse_device_info_from_config_space(location, &bytes) { + match info.interrupt_support() { + InterruptSupport::None => pci_irq_none += 1, + InterruptSupport::LegacyOnly => pci_irq_legacy += 1, + InterruptSupport::Msi => pci_irq_msi += 1, + InterruptSupport::MsiX => pci_irq_msix += 1, + } + let quirk_flags = lookup_pci_quirks(&info); + if quirk_flags.contains(PciQuirkFlags::FORCE_LEGACY_IRQ) { + pci_irq_forced_legacy += 1; + } + if quirk_flags.contains(PciQuirkFlags::NO_MSIX) { + pci_irq_msix_disabled_by_quirk += 1; + } + if quirk_flags.contains(PciQuirkFlags::NO_MSI) { + pci_irq_msi_disabled_by_quirk += 1; + } + rtl8125_present_from_pci |= + info.vendor_id == RTL8125_VENDOR_ID && info.device_id == RTL8125_DEVICE_ID; + } + } + } + + let rtl8125_present = rtl8125_present_from_pci || network .network_schemes .iter() .any(|name| name.contains("rtl8125")); @@ -909,13 +969,99 @@ fn collect_hardware(runtime: &Runtime, network: &NetworkReport) -> HardwareRepor HardwareReport { pci_devices, + pci_irq_none, + pci_irq_legacy, + pci_irq_msi, + pci_irq_msix, + pci_irq_forced_legacy, + pci_irq_msix_disabled_by_quirk, + pci_irq_msi_disabled_by_quirk, + runtime_irq_reports, usb_controllers, drm_cards, + acpi_power_surface_present, rtl8125_present, virtio_net_present, } } +fn parse_scheme_pci_location(entry: &str) -> Option { + 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 { + 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::().ok(), + "device" => device = Some(value.trim().to_string()), + "mode" => mode = Some(value.trim().to_string()), + "reason" => reason = Some(value.trim().to_string()), + _ => {} + } + } + + if let (Some(driver), Some(pid), Some(device), Some(mode), Some(reason)) = + (driver, pid, device, mode, reason) + { + if !runtime.exists(&format!("/proc/{pid}")) { + continue; + } + reports.push(IrqRuntimeReport { + driver, + pid, + device, + mode, + reason, + }); + } + } + } + + reports.sort_by(|left, right| left.driver.cmp(&right.driver).then(left.device.cmp(&right.device))); + reports +} + fn collect_quirks(runtime: &Runtime) -> QuirksReport { let mut files_loaded = Vec::new(); let mut load_errors = Vec::new(); @@ -1359,8 +1505,38 @@ fn print_table(report: &Report<'_>, verbose: bool) { print_section_header("Hardware"); println!(" PCI devices: {}", report.hardware.pci_devices); + println!( + " PCI IRQ support: none={} legacy={} msi={} msix={}", + report.hardware.pci_irq_none, + report.hardware.pci_irq_legacy, + report.hardware.pci_irq_msi, + report.hardware.pci_irq_msix, + ); + println!( + " PCI IRQ quirk pressure: force_legacy={} no_msix={} no_msi={}", + report.hardware.pci_irq_forced_legacy, + report.hardware.pci_irq_msix_disabled_by_quirk, + report.hardware.pci_irq_msi_disabled_by_quirk, + ); + if !report.hardware.runtime_irq_reports.is_empty() { + println!(" PCI IRQ runtime modes:"); + for item in &report.hardware.runtime_irq_reports { + println!( + " {} pid={} {} mode={} reason={}", + item.driver, item.pid, item.device, item.mode, item.reason + ); + } + } println!(" USB controllers: {}", report.hardware.usb_controllers); println!(" DRM cards: {}", report.hardware.drm_cards); + println!( + " ACPI power surface: {}", + if report.hardware.acpi_power_surface_present { + "present" + } else { + "unavailable" + } + ); println!( " RTL8125 device seen: {}", if report.hardware.rtl8125_present { @@ -1758,6 +1934,70 @@ fn print_json(report: &Report<'_>) { true, 4, ); + push_json_number_field( + &mut out, + "pci_irq_none", + report.hardware.pci_irq_none, + true, + 4, + ); + push_json_number_field( + &mut out, + "pci_irq_legacy", + report.hardware.pci_irq_legacy, + true, + 4, + ); + push_json_number_field( + &mut out, + "pci_irq_msi", + report.hardware.pci_irq_msi, + true, + 4, + ); + push_json_number_field( + &mut out, + "pci_irq_msix", + report.hardware.pci_irq_msix, + true, + 4, + ); + push_json_number_field( + &mut out, + "pci_irq_forced_legacy", + report.hardware.pci_irq_forced_legacy, + true, + 4, + ); + push_json_number_field( + &mut out, + "pci_irq_msix_disabled_by_quirk", + report.hardware.pci_irq_msix_disabled_by_quirk, + true, + 4, + ); + push_json_number_field( + &mut out, + "pci_irq_msi_disabled_by_quirk", + report.hardware.pci_irq_msi_disabled_by_quirk, + true, + 4, + ); + out.push_str(" \"runtime_irq_reports\": [\n"); + for (index, item) in report.hardware.runtime_irq_reports.iter().enumerate() { + out.push_str(" {\n"); + push_json_string_field(&mut out, "driver", &item.driver, true, 8); + push_json_number_field(&mut out, "pid", item.pid as usize, true, 8); + push_json_string_field(&mut out, "device", &item.device, true, 8); + push_json_string_field(&mut out, "mode", &item.mode, true, 8); + push_json_string_field(&mut out, "reason", &item.reason, false, 8); + out.push_str(" }"); + if index + 1 != report.hardware.runtime_irq_reports.len() { + out.push(','); + } + out.push('\n'); + } + out.push_str(" ],\n"); push_json_number_field( &mut out, "usb_controllers", @@ -1766,6 +2006,13 @@ fn print_json(report: &Report<'_>) { 4, ); push_json_number_field(&mut out, "drm_cards", report.hardware.drm_cards, true, 4); + push_json_bool_field( + &mut out, + "acpi_power_surface_present", + report.hardware.acpi_power_surface_present, + true, + 4, + ); push_json_bool_field( &mut out, "rtl8125_present", @@ -2160,6 +2407,23 @@ fn probe_iommu_scheme( probe_named_scheme(runtime, "iommu") } +fn probe_acpi_power_surface( + runtime: &Runtime, + _network: &NetworkReport, + _hardware: &HardwareReport, + _check: &IntegrationCheck, +) -> Option { + let adapters = runtime.read_dir_names("/scheme/acpi/power/adapters"); + let batteries = runtime.read_dir_names("/scheme/acpi/power/batteries"); + runtime.exists("/scheme/acpi/power").then(|| { + format!( + "acpi power surface visible (adapters={}, batteries={})", + adapters.as_ref().map(|items| items.len()).unwrap_or(0), + batteries.as_ref().map(|items| items.len()).unwrap_or(0) + ) + }) +} + fn probe_serio_surface( runtime: &Runtime, _network: &NetworkReport, @@ -2927,16 +3191,12 @@ mod tests { fn rtl8125_hardware_detection_parses_pci_config() { let root = temp_root(); create_dir(&root, "/scheme/pci/0000--02--00.0"); - let config = [ - (RTL8125_VENDOR_ID & 0xff) as u8, - (RTL8125_VENDOR_ID >> 8) as u8, - (RTL8125_DEVICE_ID & 0xff) as u8, - (RTL8125_DEVICE_ID >> 8) as u8, - 0, - 0, - 0, - 0, - ]; + let mut config = [0u8; 64]; + config[0x00] = (RTL8125_VENDOR_ID & 0xff) as u8; + config[0x01] = (RTL8125_VENDOR_ID >> 8) as u8; + config[0x02] = (RTL8125_DEVICE_ID & 0xff) as u8; + config[0x03] = (RTL8125_DEVICE_ID >> 8) as u8; + config[0x0e] = 0x00; let path = root.join("scheme/pci/0000--02--00.0/config"); if let Some(parent) = path.parent() { fs::create_dir_all(parent).unwrap(); @@ -2954,16 +3214,12 @@ mod tests { fn virtio_net_hardware_detection_parses_pci_config() { let root = temp_root(); create_dir(&root, "/scheme/pci/0000--00--03.0"); - let config = [ - (VIRTIO_NET_VENDOR_ID & 0xff) as u8, - (VIRTIO_NET_VENDOR_ID >> 8) as u8, - (VIRTIO_NET_DEVICE_ID & 0xff) as u8, - (VIRTIO_NET_DEVICE_ID >> 8) as u8, - 0, - 0, - 0, - 0, - ]; + let mut config = [0u8; 64]; + config[0x00] = (VIRTIO_NET_VENDOR_ID & 0xff) as u8; + config[0x01] = (VIRTIO_NET_VENDOR_ID >> 8) as u8; + config[0x02] = (VIRTIO_NET_DEVICE_ID & 0xff) as u8; + config[0x03] = (VIRTIO_NET_DEVICE_ID >> 8) as u8; + config[0x0e] = 0x00; let path = root.join("scheme/pci/0000--00--03.0/config"); if let Some(parent) = path.parent() { fs::create_dir_all(parent).unwrap(); @@ -2977,6 +3233,127 @@ mod tests { fs::remove_dir_all(root).unwrap(); } + #[test] + fn hardware_report_counts_pci_interrupt_support_modes() { + let root = temp_root(); + create_dir(&root, "/scheme/pci/0000--00--01.0"); + create_dir(&root, "/scheme/pci/0000--00--02.0"); + create_dir(&root, "/scheme/pci/0000--00--03.0"); + + let mut legacy = [0u8; 68]; + legacy[0x00] = 0x34; + legacy[0x01] = 0x12; + legacy[0x02] = 0x78; + legacy[0x03] = 0x56; + legacy[0x06] = 0x10; // capabilities present + legacy[0x0e] = 0x00; + legacy[0x34] = 0x40; + legacy[0x3c] = 11; + legacy[0x40] = 0x01; // power capability only + legacy[0x41] = 0x00; + + let mut msi = legacy; + msi[0x02] = 0x79; + msi[0x40] = 0x05; // MSI capability + + let mut msix = legacy; + msix[0x02] = 0x7a; + msix[0x40] = 0x11; // MSI-X capability + + fs::write(root.join("scheme/pci/0000--00--01.0/config"), legacy).unwrap(); + fs::write(root.join("scheme/pci/0000--00--02.0/config"), msi).unwrap(); + fs::write(root.join("scheme/pci/0000--00--03.0/config"), msix).unwrap(); + + let network = collect_network(&Runtime::from_root(root.clone())); + let hardware = collect_hardware(&Runtime::from_root(root.clone()), &network); + assert_eq!(hardware.pci_devices, 3); + assert_eq!(hardware.pci_irq_none, 0); + assert!(hardware.pci_irq_legacy >= 1); + assert_eq!( + hardware.pci_irq_legacy + hardware.pci_irq_msi + hardware.pci_irq_msix, + hardware.pci_devices + ); + assert_eq!( + hardware.pci_irq_forced_legacy + + hardware.pci_irq_msix_disabled_by_quirk + + hardware.pci_irq_msi_disabled_by_quirk, + 0 + ); + + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn hardware_report_detects_acpi_power_surface() { + let root = temp_root(); + create_dir(&root, "/scheme/acpi/power"); + + let network = collect_network(&Runtime::from_root(root.clone())); + let hardware = collect_hardware(&Runtime::from_root(root.clone()), &network); + assert!(hardware.acpi_power_surface_present); + + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn collect_irq_runtime_reports_reads_driver_mode_files() { + let root = temp_root(); + create_dir(&root, "/proc/123"); + write_file( + &root, + "/tmp/redbear-irq-report/xhcid.env", + "driver=xhcid\npid=123\ndevice=0000:00:14.0\nmode=msi_or_msix\nreason=driver_selected_interrupt_delivery\n", + ); + + let reports = collect_irq_runtime_reports(&Runtime::from_root(root.clone())); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].driver, "xhcid"); + assert_eq!(reports[0].pid, 123); + assert_eq!(reports[0].mode, "msi_or_msix"); + assert_eq!(reports[0].reason, "driver_selected_interrupt_delivery"); + + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn collect_irq_runtime_reports_ignores_stale_pid_entries() { + let root = temp_root(); + write_file( + &root, + "/tmp/redbear-irq-report/xhcid.env", + "driver=xhcid\npid=999\ndevice=0000:00:14.0\nmode=msi_or_msix\nreason=driver_selected_interrupt_delivery\n", + ); + + let reports = collect_irq_runtime_reports(&Runtime::from_root(root.clone())); + assert!(reports.is_empty()); + + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn redbear_upower_integration_is_present_without_live_power_surface() { + let root = temp_root(); + write_file(&root, "/usr/bin/redbear-upower", ""); + + let report = collect_report(&Runtime::from_root(root.clone())); + assert_eq!(integration_state(&report, "redbear-upower"), ProbeState::Present); + + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn redbear_upower_integration_is_functional_with_live_power_surface() { + let root = temp_root(); + write_file(&root, "/usr/bin/redbear-upower", ""); + create_dir(&root, "/scheme/acpi/power/adapters/AC"); + create_dir(&root, "/scheme/acpi/power/batteries"); + + let report = collect_report(&Runtime::from_root(root.clone())); + assert_eq!(integration_state(&report, "redbear-upower"), ProbeState::Functional); + + fs::remove_dir_all(root).unwrap(); + } + #[test] fn json_output_contains_network_and_integration_state() { let root = temp_root(); @@ -2997,6 +3374,7 @@ mod tests { let report = collect_report(&Runtime::from_root(root.clone())); assert!(!report.hardware.virtio_net_present); + assert!(!report.hardware.acpi_power_surface_present); let mut output = String::new(); output.push_str("{"); push_json_string_field( @@ -3031,6 +3409,12 @@ mod tests { .iter() .any(|item| item.check.name == "redbear-btctl") ); + assert!( + report + .integrations + .iter() + .any(|item| item.check.name == "redbear-upower") + ); assert!( report .integrations diff --git a/local/recipes/system/redbear-login-protocol/source/Cargo.toml b/local/recipes/system/redbear-login-protocol/source/Cargo.toml new file mode 100644 index 00000000..4ef1ccbe --- /dev/null +++ b/local/recipes/system/redbear-login-protocol/source/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "redbear-login-protocol" +version = "0.1.0" +edition = "2024" + +[lib] +path = "src/lib.rs" + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" diff --git a/local/recipes/system/redbear-login-protocol/source/src/lib.rs b/local/recipes/system/redbear-login-protocol/source/src/lib.rs new file mode 100644 index 00000000..b498c325 --- /dev/null +++ b/local/recipes/system/redbear-login-protocol/source/src/lib.rs @@ -0,0 +1,108 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum GreeterRequest { + Hello { version: u32 }, + SubmitLogin { username: String, password: String }, + RequestShutdown, + RequestReboot, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum GreeterResponse { + HelloOk { + background: String, + icon: String, + session_name: String, + state: String, + message: String, + }, + LoginResult { + ok: bool, + state: String, + message: String, + }, + ActionResult { + ok: bool, + message: String, + }, + Error { + message: String, + }, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum AuthRequest { + Authenticate { + request_id: u64, + username: String, + password: String, + vt: u32, + }, + StartSession { + request_id: u64, + username: String, + session: String, + vt: u32, + }, + PowerAction { + request_id: u64, + action: String, + }, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum AuthResponse { + AuthenticateResult { + request_id: u64, + ok: bool, + message: String, + }, + SessionResult { + request_id: u64, + ok: bool, + exit_code: Option, + message: String, + }, + PowerResult { + request_id: u64, + ok: bool, + message: String, + }, + Error { + message: String, + }, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn greeter_request_round_trips() { + let request = GreeterRequest::SubmitLogin { + username: String::from("root"), + password: String::from("password"), + }; + let json = serde_json::to_string(&request).expect("request should serialize"); + let parsed: GreeterRequest = serde_json::from_str(&json).expect("request should deserialize"); + assert_eq!(parsed, request); + } + + #[test] + fn auth_response_round_trips() { + let response = AuthResponse::SessionResult { + request_id: 7, + ok: true, + exit_code: Some(0), + message: String::from("ok"), + }; + let json = serde_json::to_string(&response).expect("response should serialize"); + let parsed: AuthResponse = serde_json::from_str(&json).expect("response should deserialize"); + assert_eq!(parsed, response); + } +} diff --git a/local/recipes/system/redbear-session-launch/source/src/main.rs b/local/recipes/system/redbear-session-launch/source/src/main.rs index a78bdeef..5a5cbdd7 100644 --- a/local/recipes/system/redbear-session-launch/source/src/main.rs +++ b/local/recipes/system/redbear-session-launch/source/src/main.rs @@ -251,11 +251,19 @@ fn env_value(keys: &[&str]) -> Option { fn build_environment(account: &Account, args: &Args, runtime_dir: &Path) -> BTreeMap { let mut values = BTreeMap::new(); - values.insert(String::from("HOME"), account.home.clone()); + let home = match args.mode { + LaunchMode::Command { .. } if account.home == "/nonexistent" => runtime_dir.display().to_string(), + _ => account.home.clone(), + }; + + values.insert(String::from("HOME"), home.clone()); values.insert(String::from("USER"), account.username.clone()); values.insert(String::from("LOGNAME"), account.username.clone()); values.insert(String::from("SHELL"), account.shell.clone()); values.insert(String::from("PATH"), String::from("/usr/bin:/bin")); + values.insert(String::from("XDG_CONFIG_HOME"), format!("{home}/.config")); + values.insert(String::from("XDG_CACHE_HOME"), format!("{home}/.cache")); + values.insert(String::from("XDG_STATE_HOME"), format!("{home}/.local/state")); values.insert(String::from("XDG_RUNTIME_DIR"), runtime_dir.display().to_string()); values.insert(String::from("WAYLAND_DISPLAY"), args.wayland_display.clone()); values.insert(String::from("XDG_SEAT"), String::from("seat0")); @@ -265,6 +273,18 @@ fn build_environment(account: &Account, args: &Args, runtime_dir: &Path) -> BTre values.insert(String::from("DISPLAY"), String::new()); values.insert(String::from("XDG_SESSION_TYPE"), String::from("wayland")); + if let Some(locale) = env_value(&["LC_ALL"]) { + values.insert(String::from("LC_ALL"), locale.clone()); + values.insert(String::from("LANG"), locale); + } else if let Some(locale) = env_value(&["LC_CTYPE"]) { + values.insert(String::from("LC_CTYPE"), locale.clone()); + values.insert(String::from("LANG"), locale); + } else if let Some(locale) = env_value(&["LANG"]) { + values.insert(String::from("LANG"), locale); + } else { + values.insert(String::from("LANG"), String::from("en_US.UTF-8")); + } + if let Some(theme) = env_value(&["XCURSOR_THEME"]) { values.insert(String::from("XCURSOR_THEME"), theme); } @@ -285,6 +305,7 @@ fn build_environment(account: &Account, args: &Args, runtime_dir: &Path) -> BTre LaunchMode::Session => { values.insert(String::from("XDG_CURRENT_DESKTOP"), String::from("KDE")); values.insert(String::from("KDE_FULL_SESSION"), String::from("true")); + values.insert(String::from("XDG_SESSION_ID"), String::from("c1")); } LaunchMode::Command { .. } => {} } @@ -485,9 +506,15 @@ mod tests { }; let envs = build_environment(&account, &args, Path::new("/run/user/1000")); + assert_eq!(envs["HOME"], "/home/user"); assert_eq!(envs["XDG_CURRENT_DESKTOP"], "KDE"); + assert_eq!(envs["XDG_CONFIG_HOME"], "/home/user/.config"); + assert_eq!(envs["XDG_CACHE_HOME"], "/home/user/.cache"); + assert_eq!(envs["XDG_STATE_HOME"], "/home/user/.local/state"); assert_eq!(envs["KDE_FULL_SESSION"], "true"); + assert_eq!(envs["XDG_SESSION_ID"], "c1"); assert_eq!(envs["XDG_VTNR"], "3"); + assert!(envs.contains_key("LANG")); } #[test] @@ -512,8 +539,13 @@ mod tests { }; let envs = build_environment(&account, &args, Path::new("/tmp/run/greeter")); + assert_eq!(envs["HOME"], "/tmp/run/greeter"); + assert_eq!(envs["XDG_CONFIG_HOME"], "/tmp/run/greeter/.config"); + assert_eq!(envs["XDG_CACHE_HOME"], "/tmp/run/greeter/.cache"); + assert_eq!(envs["XDG_STATE_HOME"], "/tmp/run/greeter/.local/state"); assert!(!envs.contains_key("XDG_CURRENT_DESKTOP")); assert!(!envs.contains_key("KDE_FULL_SESSION")); + assert!(!envs.contains_key("XDG_SESSION_ID")); assert_eq!(envs["XDG_SESSION_TYPE"], "wayland"); } diff --git a/local/recipes/system/redbear-sessiond/source/src/acpi_watcher.rs b/local/recipes/system/redbear-sessiond/source/src/acpi_watcher.rs index 7dc962d9..3803c52c 100644 --- a/local/recipes/system/redbear-sessiond/source/src/acpi_watcher.rs +++ b/local/recipes/system/redbear-sessiond/source/src/acpi_watcher.rs @@ -1,5 +1,7 @@ use zbus::Connection; +use crate::runtime_state::SharedRuntime; + #[cfg(target_os = "redox")] const KSTOP_PATH: &str = "/scheme/kernel.acpi/kstop"; @@ -13,10 +15,13 @@ fn wait_for_shutdown_edge() -> std::io::Result<()> { Ok(()) } -pub async fn watch_and_emit(connection: Connection) { +pub async fn watch_and_emit(connection: Connection, runtime: SharedRuntime) { #[cfg(target_os = "redox")] match tokio::task::spawn_blocking(wait_for_shutdown_edge).await { Ok(Ok(())) => { + if let Ok(mut state) = runtime.write() { + state.preparing_for_shutdown = true; + } let _ = connection .emit_signal( None::<&str>, @@ -38,5 +43,6 @@ pub async fn watch_and_emit(connection: Connection) { #[cfg(not(target_os = "redox"))] { let _ = connection; + let _ = runtime; } } diff --git a/local/recipes/system/redbear-sessiond/source/src/main.rs b/local/recipes/system/redbear-sessiond/source/src/main.rs index 4c5ab617..59b4ba3c 100644 --- a/local/recipes/system/redbear-sessiond/source/src/main.rs +++ b/local/recipes/system/redbear-sessiond/source/src/main.rs @@ -133,7 +133,7 @@ async fn run_daemon() -> Result<(), Box> { Ok(connection) => { eprintln!("redbear-sessiond: registered {BUS_NAME} on the system bus"); control::start_control_socket(runtime.clone()); - tokio::spawn(acpi_watcher::watch_and_emit(connection.clone())); + tokio::spawn(acpi_watcher::watch_and_emit(connection.clone(), runtime.clone())); wait_for_shutdown().await?; drop(connection); return Ok(()); diff --git a/local/recipes/system/redbear-sessiond/source/src/manager.rs b/local/recipes/system/redbear-sessiond/source/src/manager.rs index 28ceed66..141f1c8f 100644 --- a/local/recipes/system/redbear-sessiond/source/src/manager.rs +++ b/local/recipes/system/redbear-sessiond/source/src/manager.rs @@ -31,7 +31,7 @@ impl LoginManager { .runtime .read() .map_err(|_| fdo::Error::Failed(String::from("login1 runtime state is poisoned")))?; - if id == runtime.session_id { + if id == runtime.session_id || id == "auto" { return Ok(self.session_path.clone()); } @@ -111,7 +111,10 @@ impl LoginManager { #[zbus(property(emits_changed_signal = "const"), name = "PreparingForShutdown")] fn preparing_for_shutdown(&self) -> bool { - false + self.runtime + .read() + .map(|runtime| runtime.preparing_for_shutdown) + .unwrap_or(false) } #[zbus(signal, name = "PrepareForSleep")] @@ -123,3 +126,60 @@ impl LoginManager { before: bool, ) -> zbus::Result<()>; } + +#[cfg(test)] +mod tests { + use super::*; + use crate::runtime_state::shared_runtime; + + #[test] + fn get_session_accepts_runtime_session_id() { + let manager = LoginManager::new( + OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/session/c1")) + .expect("session path should parse"), + OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/seat/seat0")) + .expect("seat path should parse"), + shared_runtime(), + ); + + let path = manager + .get_session("c1") + .expect("runtime session id should resolve"); + assert_eq!(path.as_str(), "/org/freedesktop/login1/session/c1"); + } + + #[test] + fn get_session_accepts_auto_alias() { + let manager = LoginManager::new( + OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/session/c1")) + .expect("session path should parse"), + OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/seat/seat0")) + .expect("seat path should parse"), + shared_runtime(), + ); + + let path = manager + .get_session("auto") + .expect("auto alias should resolve to current session"); + assert_eq!(path.as_str(), "/org/freedesktop/login1/session/c1"); + } + + #[test] + fn preparing_for_shutdown_reflects_runtime_state() { + let runtime = shared_runtime(); + runtime + .write() + .expect("runtime lock should be writable") + .preparing_for_shutdown = true; + + let manager = LoginManager::new( + OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/session/c1")) + .expect("session path should parse"), + OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/seat/seat0")) + .expect("seat path should parse"), + runtime, + ); + + assert!(manager.preparing_for_shutdown()); + } +} diff --git a/local/recipes/system/redbear-sessiond/source/src/runtime_state.rs b/local/recipes/system/redbear-sessiond/source/src/runtime_state.rs index 3d49b09c..ada7ba69 100644 --- a/local/recipes/system/redbear-sessiond/source/src/runtime_state.rs +++ b/local/recipes/system/redbear-sessiond/source/src/runtime_state.rs @@ -10,6 +10,7 @@ pub struct SessionRuntime { pub leader: u32, pub state: String, pub active: bool, + pub preparing_for_shutdown: bool, } impl Default for SessionRuntime { @@ -23,6 +24,7 @@ impl Default for SessionRuntime { leader: std::process::id(), state: String::from("online"), active: true, + preparing_for_shutdown: false, } } } diff --git a/local/recipes/system/redbear-sessiond/source/src/seat.rs b/local/recipes/system/redbear-sessiond/source/src/seat.rs index 8f9b67a9..87338f1e 100644 --- a/local/recipes/system/redbear-sessiond/source/src/seat.rs +++ b/local/recipes/system/redbear-sessiond/source/src/seat.rs @@ -1,4 +1,4 @@ -use std::sync::Mutex; +use std::{process::Command, sync::Mutex}; use zbus::{fdo, interface, zvariant::OwnedObjectPath}; @@ -27,17 +27,55 @@ impl LoginSeat { .lock() .map_err(|_| fdo::Error::Failed(String::from("seat VT state is poisoned"))) } + + fn request_vt_switch(program: &str, vt: u32) -> fdo::Result<()> { + let output = Command::new(program) + .args(["-A", &vt.to_string()]) + .output() + .map_err(|err| fdo::Error::Failed(format!("failed to run {program} -A {vt}: {err}")))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + let detail = if !stderr.trim().is_empty() { + stderr.trim().to_string() + } else if !stdout.trim().is_empty() { + stdout.trim().to_string() + } else { + format!("exit status {}", output.status) + }; + return Err(fdo::Error::Failed(format!( + "{program} -A {vt} failed: {detail}" + ))); + } + + Ok(()) + } + + fn current_session_id(&self) -> String { + self.runtime + .read() + .map(|runtime| runtime.session_id.clone()) + .unwrap_or_else(|_| String::from("c1")) + } } #[interface(name = "org.freedesktop.login1.Seat")] impl LoginSeat { fn switch_to(&mut self, vt: u32) -> fdo::Result<()> { + Self::request_vt_switch("inputd", vt)?; + let mut last_requested_vt = self.last_requested_vt()?; *last_requested_vt = vt; - eprintln!( - "redbear-sessiond: SwitchTo requested for seat {} -> vt {vt} (delegated to inputd -A externally)", - self.id - ); + + let mut runtime = self + .runtime + .write() + .map_err(|_| fdo::Error::Failed(String::from("seat runtime state is poisoned")))?; + runtime.vt = vt; + runtime.active = true; + + eprintln!("redbear-sessiond: SwitchTo requested for seat {} -> vt {vt}", self.id); Ok(()) } @@ -48,24 +86,12 @@ impl LoginSeat { #[zbus(property(emits_changed_signal = "const"), name = "ActiveSession")] fn active_session(&self) -> (String, OwnedObjectPath) { - ( - self.runtime - .read() - .map(|runtime| runtime.session_id.clone()) - .unwrap_or_else(|_| String::from("c1")), - self.session_path.clone(), - ) + (self.current_session_id(), self.session_path.clone()) } #[zbus(property(emits_changed_signal = "const"), name = "Sessions")] fn sessions(&self) -> Vec<(String, OwnedObjectPath)> { - vec![( - self.runtime - .read() - .map(|runtime| runtime.session_id.clone()) - .unwrap_or_else(|_| String::from("c1")), - self.session_path.clone(), - )] + vec![(self.current_session_id(), self.session_path.clone())] } #[zbus(property(emits_changed_signal = "const"), name = "CanGraphical")] @@ -83,3 +109,39 @@ impl LoginSeat { false } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::runtime_state::shared_runtime; + + #[test] + fn request_vt_switch_accepts_successful_command() { + LoginSeat::request_vt_switch("/bin/true", 3).expect("true should succeed"); + } + + #[test] + fn request_vt_switch_rejects_failed_command() { + let err = LoginSeat::request_vt_switch("/bin/false", 3).expect_err("false should fail"); + match err { + fdo::Error::Failed(message) => { + assert!(message.contains("/bin/false -A 3 failed")); + } + other => panic!("expected failed error, got {other:?}"), + } + } + + #[test] + fn active_session_reflects_runtime_vt_after_update() { + let session_path = OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/session/c1")) + .expect("session path should parse"); + let runtime = shared_runtime(); + { + let mut guard = runtime.write().expect("runtime lock should remain healthy"); + guard.vt = 7; + } + let seat = LoginSeat::new(session_path, runtime); + assert_eq!(seat.active_session().0, "c1"); + assert_eq!(seat.id(), "seat0"); + } +} diff --git a/local/recipes/system/redbear-upower/source/src/main.rs b/local/recipes/system/redbear-upower/source/src/main.rs index 1d7d0564..d7de2217 100644 --- a/local/recipes/system/redbear-upower/source/src/main.rs +++ b/local/recipes/system/redbear-upower/source/src/main.rs @@ -4,7 +4,6 @@ use std::{ fs, path::{Path, PathBuf}, process, - thread, time::Duration, }; @@ -178,6 +177,14 @@ fn battery_state_to_upower(state_bits: u64, percentage: Option) -> u32 { DEVICE_STATE_UNKNOWN } +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() -> Result> { let root = PathBuf::from(ACPI_POWER_ROOT); @@ -186,14 +193,10 @@ impl PowerRuntime { let mut object_paths = Vec::with_capacity(adapter_ids.len() + battery_ids.len()); for adapter_id in &adapter_ids { - object_paths.push(parse_object_path(&format!( - "/org/freedesktop/UPower/devices/line_power_{adapter_id}" - ))?); + object_paths.push(parse_object_path(&adapter_object_path(adapter_id))?); } for battery_id in &battery_ids { - object_paths.push(parse_object_path(&format!( - "/org/freedesktop/UPower/devices/battery_{battery_id}" - ))?); + object_paths.push(parse_object_path(&battery_object_path(battery_id))?); } Ok(Self { @@ -204,6 +207,10 @@ impl PowerRuntime { }) } + fn available(&self) -> bool { + self.root.exists() + } + fn adapter_dir(&self, id: &str) -> PathBuf { self.root.join("adapters").join(id) } @@ -472,6 +479,11 @@ impl PowerDevice { async fn run_daemon() -> Result<(), Box> { wait_for_dbus_socket().await; let runtime = PowerRuntime::discover()?; + if !runtime.available() { + eprintln!( + "redbear-upower: /scheme/acpi/power unavailable; serving empty provisional UPower surface" + ); + } let _display_device_path = parse_object_path(DISPLAY_DEVICE_PATH)?; let mut last_err = None; @@ -492,7 +504,7 @@ async fn run_daemon() -> Result<(), Box> { )?; for adapter_id in &runtime.adapter_ids { - let path = format!("/org/freedesktop/UPower/devices/line_power_{adapter_id}"); + let path = adapter_object_path(adapter_id); builder = builder.serve_at( path, PowerDevice { @@ -502,7 +514,7 @@ async fn run_daemon() -> Result<(), Box> { )?; } for battery_id in &runtime.battery_ids { - let path = format!("/org/freedesktop/UPower/devices/battery_{battery_id}"); + let path = battery_object_path(battery_id); builder = builder.serve_at( path, PowerDevice { @@ -557,3 +569,20 @@ fn main() { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn battery_state_prefers_charging_and_discharging_bits() { + assert_eq!(battery_state_to_upower(0x2, Some(50.0)), DEVICE_STATE_CHARGING); + assert_eq!(battery_state_to_upower(0x1, Some(50.0)), DEVICE_STATE_DISCHARGING); + } + + #[test] + fn battery_state_reports_full_when_percentage_is_high() { + assert_eq!(battery_state_to_upower(0, Some(99.5)), DEVICE_STATE_FULLY_CHARGED); + assert_eq!(battery_state_to_upower(0, None), DEVICE_STATE_UNKNOWN); + } +}