Advance Red Bear runtime services and tools

This commit is contained in:
2026-04-20 18:37:35 +01:00
parent 2b3b592dab
commit f3e6b09811
24 changed files with 1362 additions and 357 deletions
@@ -8,6 +8,8 @@ name = "redbear-authd"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
redbear-login-protocol = { path = "../../redbear-login-protocol/source" }
rust-argon2 = "3"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
# Pure-Rust SHA-256/SHA-512 crypt verifier for /etc/shadow entries. # Pure-Rust SHA-256/SHA-512 crypt verifier for /etc/shadow entries.
@@ -10,7 +10,9 @@ use std::{
time::{Duration, Instant}, 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}; use sha_crypt::{PasswordVerifier, ShaCrypt};
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
@@ -49,51 +51,6 @@ struct RuntimeState {
failures: Arc<Mutex<HashMap<String, FailureState>>>, failures: Arc<Mutex<HashMap<String, FailureState>>>,
} }
#[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<i32>,
message: String,
},
PowerResult {
request_id: u64,
ok: bool,
message: String,
},
Error {
message: String,
},
}
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")] #[serde(tag = "type", rename_all = "snake_case")]
enum SessiondUpdate { enum SessiondUpdate {
@@ -229,6 +186,9 @@ fn verify_shadow_password(password: &str, shadow_hash: &str) -> Result<bool, Ver
.verify_password(password.as_bytes(), shadow_hash) .verify_password(password.as_bytes(), shadow_hash)
.is_ok()); .is_ok());
} }
if shadow_hash.starts_with("$argon2") {
return Ok(verify_encoded(shadow_hash, password.as_bytes()).unwrap_or(false));
}
Err(VerifyError::UnsupportedHashFormat) Err(VerifyError::UnsupportedHashFormat)
} }
@@ -589,6 +549,15 @@ mod tests {
assert_eq!(verify_shadow_password("wrong", hash), Ok(false)); assert_eq!(verify_shadow_password("wrong", hash), Ok(false));
} }
#[test]
fn verify_shadow_password_accepts_argon2_hashes() {
let config = argon2::Config::default();
let hash = argon2::hash_encoded(b"password", b"testsalt", &config)
.expect("argon2 hash should encode");
assert_eq!(verify_shadow_password("password", &hash), Ok(true));
assert_eq!(verify_shadow_password("wrong", &hash), Ok(false));
}
#[test] #[test]
fn verify_shadow_password_rejects_unknown_hash_prefix() { fn verify_shadow_password_rejects_unknown_hash_prefix() {
assert_eq!(verify_shadow_password("password", "$1$legacy$hash"), Err(VerifyError::UnsupportedHashFormat)); assert_eq!(verify_shadow_password("password", "$1$legacy$hash"), Err(VerifyError::UnsupportedHashFormat));
@@ -9,5 +9,6 @@ path = "src/main.rs"
[dependencies] [dependencies]
libc = "0.2" libc = "0.2"
redbear-login-protocol = { path = "../../redbear-login-protocol/source" }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
@@ -12,7 +12,7 @@ export XCURSOR_THEME="${XCURSOR_THEME:-Pop}"
export XKB_CONFIG_ROOT="${XKB_CONFIG_ROOT:-/usr/share/X11/xkb}" export XKB_CONFIG_ROOT="${XKB_CONFIG_ROOT:-/usr/share/X11/xkb}"
if [ -z "${XDG_RUNTIME_DIR:-}" ]; then if [ -z "${XDG_RUNTIME_DIR:-}" ]; then
export XDG_RUNTIME_DIR="/tmp/run/greeter" export XDG_RUNTIME_DIR="/tmp/run/redbear-greeter"
fi fi
mkdir -p "$XDG_RUNTIME_DIR" mkdir -p "$XDG_RUNTIME_DIR"
@@ -21,4 +21,9 @@ if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ] && command -v dbus-launch >/scheme/nul
eval "$(dbus-launch --sh-syntax)" eval "$(dbus-launch --sh-syntax)"
fi 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
@@ -9,7 +9,7 @@ use std::{
time::{Duration, Instant}, 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 GREETER_SOCKET_PATH: &str = "/run/redbear-greeterd.sock";
const AUTH_SOCKET_PATH: &str = "/run/redbear-authd.sock"; const AUTH_SOCKET_PATH: &str = "/run/redbear-authd.sock";
@@ -57,88 +57,6 @@ struct GreeterDaemon {
restart_attempts: Vec<Instant>, restart_attempts: Vec<Instant>,
} }
#[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<i32>,
},
PowerResult {
ok: bool,
message: String,
#[allow(dead_code)]
request_id: u64,
},
Error {
message: String,
},
}
fn usage() -> &'static str { fn usage() -> &'static str {
"Usage: redbear-greeterd [--help]" "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<AuthResponse, String> { fn send_auth_request(request: &AuthRequest) -> Result<AuthResponse, String> {
let mut stream = UnixStream::connect(AUTH_SOCKET_PATH) let mut stream = UnixStream::connect(AUTH_SOCKET_PATH)
.map_err(|err| format!("failed to connect to {AUTH_SOCKET_PATH}: {err}"))?; .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}"))?; 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 { let response = send_auth_request(&AuthRequest::StartSession {
request_id: 2, request_id: 2,
username, username: username.to_string(),
session: "kde-wayland", session: String::from("kde-wayland"),
vt: self.vt, vt: self.vt,
})?; })?;
@@ -390,6 +308,9 @@ impl GreeterDaemon {
} }
fn handle_connection(&mut self, stream: UnixStream) -> Result<(), String> { 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 reader = BufReader::new(stream);
let mut line = String::new(); let mut line = String::new();
reader reader
@@ -413,8 +334,8 @@ impl GreeterDaemon {
self.set_state(GreeterState::Authenticating, "Authenticating"); self.set_state(GreeterState::Authenticating, "Authenticating");
match send_auth_request(&AuthRequest::Authenticate { match send_auth_request(&AuthRequest::Authenticate {
request_id: 1, request_id: 1,
username: &username, username: username.clone(),
password: &password, password: password.clone(),
vt: self.vt, vt: self.vt,
})? { })? {
AuthResponse::AuthenticateResult { ok, message, .. } => { AuthResponse::AuthenticateResult { ok, message, .. } => {
@@ -443,7 +364,7 @@ impl GreeterDaemon {
self.set_state(GreeterState::PowerAction, "Requesting shutdown"); self.set_state(GreeterState::PowerAction, "Requesting shutdown");
match send_auth_request(&AuthRequest::PowerAction { match send_auth_request(&AuthRequest::PowerAction {
request_id: 3, request_id: 3,
action: "shutdown", action: String::from("shutdown"),
})? { })? {
AuthResponse::PowerResult { ok, message, .. } => GreeterResponse::ActionResult { ok, message }, AuthResponse::PowerResult { ok, message, .. } => GreeterResponse::ActionResult { ok, message },
AuthResponse::Error { message } => GreeterResponse::Error { message }, AuthResponse::Error { message } => GreeterResponse::Error { message },
@@ -456,7 +377,7 @@ impl GreeterDaemon {
self.set_state(GreeterState::PowerAction, "Requesting reboot"); self.set_state(GreeterState::PowerAction, "Requesting reboot");
match send_auth_request(&AuthRequest::PowerAction { match send_auth_request(&AuthRequest::PowerAction {
request_id: 4, request_id: 4,
action: "reboot", action: String::from("reboot"),
})? { })? {
AuthResponse::PowerResult { ok, message, .. } => GreeterResponse::ActionResult { ok, message }, AuthResponse::PowerResult { ok, message, .. } => GreeterResponse::ActionResult { ok, message },
AuthResponse::Error { message } => GreeterResponse::Error { message }, AuthResponse::Error { message } => GreeterResponse::Error { message },
@@ -2,11 +2,15 @@
#include <QQmlApplicationEngine> #include <QQmlApplicationEngine>
#include <QQmlContext> #include <QQmlContext>
#include <QQuickStyle> #include <QQuickStyle>
#include <QQuickWindow>
#include "greeter_backend.h" #include "greeter_backend.h"
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
qputenv("QT_QUICK_CONTROLS_STYLE", QByteArrayLiteral("Basic")); 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); QGuiApplication app(argc, argv);
QQuickStyle::setStyle(QStringLiteral("Basic")); QQuickStyle::setStyle(QStringLiteral("Basic"));
@@ -83,11 +83,20 @@ path = "src/bin/redbear-phase-timer-check.rs"
name = "redbear-phase-dma-check" name = "redbear-phase-dma-check"
path = "src/bin/redbear-phase-dma-check.rs" 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]] [[bin]]
name = "redbear-usb-check" name = "redbear-usb-check"
path = "src/bin/redbear-usb-check.rs" path = "src/bin/redbear-usb-check.rs"
[dependencies] [dependencies]
redbear-login-protocol = { path = "../../redbear-login-protocol/source" }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
orbclient = "0.3" orbclient = "0.3"
@@ -4,12 +4,12 @@ use std::process;
use redbear_hwutils::{ use redbear_hwutils::{
lookup_pci_device_name, lookup_pci_vendor_name, parse_args, parse_pci_location, PciLocation, 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}; use redox_driver_sys::quirks::{lookup_pci_quirks, PciQuirkFlags};
const USAGE: &str = "Usage: lspci\nList PCI devices exposed by /scheme/pci."; 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 { struct PciDeviceSummary {
location: PciLocation, location: PciLocation,
vendor_id: u16, vendor_id: u16,
@@ -20,6 +20,9 @@ struct PciDeviceSummary {
revision: u8, revision: u8,
subvendor_id: u16, subvendor_id: u16,
subdevice_id: u16, subdevice_id: u16,
irq: Option<u32>,
interrupt_support: InterruptSupport,
irq_reason: Option<String>,
quirk_flags: PciQuirkFlags, quirk_flags: PciQuirkFlags,
} }
@@ -36,71 +39,33 @@ fn main() {
fn format_quirk_flags(flags: PciQuirkFlags) -> String { fn format_quirk_flags(flags: PciQuirkFlags) -> String {
let mut names = Vec::new(); let mut names = Vec::new();
if flags.contains(PciQuirkFlags::NO_MSI) { for (flag, name) in [
names.push("no_msi"); (PciQuirkFlags::NO_MSI, "no_msi"),
} (PciQuirkFlags::NO_MSIX, "no_msix"),
if flags.contains(PciQuirkFlags::NO_MSIX) { (PciQuirkFlags::FORCE_LEGACY_IRQ, "force_legacy_irq"),
names.push("no_msix"); (PciQuirkFlags::NO_PM, "no_pm"),
} (PciQuirkFlags::NO_D3COLD, "no_d3cold"),
if flags.contains(PciQuirkFlags::FORCE_LEGACY_IRQ) { (PciQuirkFlags::NO_ASPM, "no_aspm"),
names.push("force_legacy_irq"); (PciQuirkFlags::NEED_IOMMU, "need_iommu"),
} (PciQuirkFlags::NO_IOMMU, "no_iommu"),
if flags.contains(PciQuirkFlags::NO_PM) { (PciQuirkFlags::DMA_32BIT_ONLY, "dma_32bit_only"),
names.push("no_pm"); (PciQuirkFlags::RESIZE_BAR, "resize_bar"),
} (PciQuirkFlags::DISABLE_BAR_SIZING, "disable_bar_sizing"),
if flags.contains(PciQuirkFlags::NO_D3COLD) { (PciQuirkFlags::NEED_FIRMWARE, "need_firmware"),
names.push("no_d3cold"); (PciQuirkFlags::DISABLE_ACCEL, "disable_accel"),
} (PciQuirkFlags::FORCE_VRAM_ONLY, "force_vram_only"),
if flags.contains(PciQuirkFlags::NO_ASPM) { (PciQuirkFlags::NO_USB3, "no_usb3"),
names.push("no_aspm"); (PciQuirkFlags::RESET_DELAY_MS, "reset_delay_ms"),
} (PciQuirkFlags::NO_STRING_FETCH, "no_string_fetch"),
if flags.contains(PciQuirkFlags::NEED_IOMMU) { (PciQuirkFlags::BAD_EEPROM, "bad_eeprom"),
names.push("need_iommu"); (PciQuirkFlags::BUS_MASTER_DELAY, "bus_master_delay"),
} (PciQuirkFlags::WRONG_CLASS, "wrong_class"),
if flags.contains(PciQuirkFlags::NO_IOMMU) { (PciQuirkFlags::BROKEN_BRIDGE, "broken_bridge"),
names.push("no_iommu"); (PciQuirkFlags::NO_RESOURCE_RELOC, "no_resource_reloc"),
} ] {
if flags.contains(PciQuirkFlags::DMA_32BIT_ONLY) { if flags.contains(flag) {
names.push("dma_32bit_only"); names.push(name);
} }
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");
} }
names.join(",") names.join(",")
} }
@@ -163,6 +128,13 @@ fn run() -> Result<(), String> {
if !device.quirk_flags.is_empty() { if !device.quirk_flags.is_empty() {
print!(" quirks: {}", format_quirk_flags(device.quirk_flags)); 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!(); println!();
} }
@@ -195,47 +167,56 @@ fn collect_devices() -> Result<Vec<PciDeviceSummary>, String> {
Err(_) => continue, Err(_) => continue,
}; };
if config.len() < 16 { if config.len() < 64 {
continue; continue;
} }
let vendor_id = u16::from_le_bytes([config[0x00], config[0x01]]); let info = match parse_device_info_from_config_space(
let device_id = u16::from_le_bytes([config[0x02], config[0x03]]); redox_driver_sys::pci::PciLocation {
let revision = config[0x08]; segment: location.segment,
let prog_if = config[0x09]; bus: location.bus,
let subclass = config[0x0A]; device: location.device,
let class_code = config[0x0B]; function: location.function,
},
let (subvendor_id, subdevice_id) = if config.len() >= 0x30 { &config,
( ) {
u16::from_le_bytes([config[0x2C], config[0x2D]]), Some(info) => info,
u16::from_le_bytes([config[0x2E], config[0x2F]]), None => continue,
)
} else {
(0xFFFF, 0xFFFF)
}; };
let quirk_flags = lookup_quirks( let quirk_flags = lookup_quirks(
vendor_id, info.vendor_id,
device_id, info.device_id,
revision, info.revision,
class_code, info.class_code,
subclass, info.subclass,
prog_if, info.prog_if,
subvendor_id, info.subsystem_vendor_id,
subdevice_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 { devices.push(PciDeviceSummary {
location, location,
vendor_id, vendor_id: info.vendor_id,
device_id, device_id: info.device_id,
revision, revision: info.revision,
prog_if, prog_if: info.prog_if,
subclass, subclass: info.subclass,
class_code, class_code: info.class_code,
subvendor_id, subvendor_id: info.subsystem_vendor_id,
subdevice_id, subdevice_id: info.subsystem_device_id,
irq: info.irq,
interrupt_support: info.interrupt_support(),
irq_reason,
quirk_flags, quirk_flags,
}); });
} }
@@ -8,7 +8,7 @@ use std::{
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use serde::{Deserialize, Serialize}; use redbear_login_protocol::{GreeterRequest as Request, GreeterResponse};
const PROGRAM: &str = "redbear-greeter-check"; 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."; 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_REQUEST: &str = "/run/redbear-kde-session.validation-request";
const VALIDATION_SUCCESS: &str = "/run/redbear-kde-session.validation-success"; 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)] #[derive(Debug, PartialEq, Eq)]
enum Mode { enum Mode {
Status, Status,
@@ -64,6 +35,21 @@ enum Mode {
Valid { username: String, password: String }, Valid { username: String, password: String },
} }
fn parse_credentials(args: &mut impl Iterator<Item = String>, flag: &str) -> Result<(String, String), String> {
let username = args
.next()
.ok_or_else(|| format!("missing username after {flag}"))?;
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<I>(args: I) -> Result<Mode, String> fn parse_mode_from_args<I>(args: I) -> Result<Mode, String>
where where
I: IntoIterator<Item = String>, I: IntoIterator<Item = String>,
@@ -73,19 +59,11 @@ where
None => Ok(Mode::Status), None => Ok(Mode::Status),
Some(flag) if flag == "--help" || flag == "-h" => Err(String::new()), Some(flag) if flag == "--help" || flag == "-h" => Err(String::new()),
Some(flag) if flag == "--invalid" => { Some(flag) if flag == "--invalid" => {
let username = args.next().ok_or_else(|| String::from("missing username after --invalid"))?; let (username, password) = parse_credentials(&mut args, &flag)?;
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"));
}
Ok(Mode::Invalid { username, password }) Ok(Mode::Invalid { username, password })
} }
Some(flag) if flag == "--valid" => { Some(flag) if flag == "--valid" => {
let username = args.next().ok_or_else(|| String::from("missing username after --valid"))?; let (username, password) = parse_credentials(&mut args, &flag)?;
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"));
}
Ok(Mode::Valid { username, password }) Ok(Mode::Valid { username, password })
} }
Some(other) => Err(format!("unsupported argument '{other}'")), Some(other) => Err(format!("unsupported argument '{other}'")),
@@ -96,7 +74,7 @@ fn parse_mode() -> Result<Mode, String> {
parse_mode_from_args(std::env::args().skip(1)) parse_mode_from_args(std::env::args().skip(1))
} }
fn send_request(request: &Request<'_>) -> Result<Response, String> { fn send_request(request: &Request) -> Result<GreeterResponse, String> {
let mut stream = UnixStream::connect(GREETER_SOCKET) let mut stream = UnixStream::connect(GREETER_SOCKET)
.map_err(|err| format!("failed to connect to {GREETER_SOCKET}: {err}"))?; .map_err(|err| format!("failed to connect to {GREETER_SOCKET}: {err}"))?;
let payload = serde_json::to_string(request) let payload = serde_json::to_string(request)
@@ -139,7 +117,7 @@ fn wait_for_greeter_ready(timeout: Duration) -> Result<(), String> {
let start = Instant::now(); let start = Instant::now();
while start.elapsed() <= timeout { while start.elapsed() <= timeout {
match send_request(&Request::Hello { version: 1 }) { 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}"); println!("GREETER_VALID_READY_MESSAGE={message}");
return Ok(()); return Ok(());
} }
@@ -169,7 +147,7 @@ fn run_status() -> Result<(), String> {
require_path(GREETER_SOCKET)?; require_path(GREETER_SOCKET)?;
match send_request(&Request::Hello { version: 1 })? { match send_request(&Request::Hello { version: 1 })? {
Response::HelloOk { GreeterResponse::HelloOk {
background, background,
icon, icon,
session_name, session_name,
@@ -184,15 +162,18 @@ fn run_status() -> Result<(), String> {
println!("GREETER_HELLO=ok"); println!("GREETER_HELLO=ok");
Ok(()) Ok(())
} }
Response::Error { message } => Err(format!("greeter hello failed: {message}")), GreeterResponse::Error { message } => Err(format!("greeter hello failed: {message}")),
Response::Other => Err(String::from("unexpected greeter hello response")), GreeterResponse::ActionResult { .. } => Err(String::from("unexpected power response when greeting greeter")),
Response::LoginResult { .. } => Err(String::from("unexpected login result when greeting greeter")), GreeterResponse::LoginResult { .. } => Err(String::from("unexpected login result when greeting greeter")),
} }
} }
fn run_invalid(username: &str, password: &str) -> Result<(), String> { fn run_invalid(username: &str, password: &str) -> Result<(), String> {
match send_request(&Request::SubmitLogin { username, password })? { match send_request(&Request::SubmitLogin {
Response::LoginResult { ok, state, message } => { username: username.to_string(),
password: password.to_string(),
})? {
GreeterResponse::LoginResult { ok, state, message } => {
println!("GREETER_INVALID_STATE={state}"); println!("GREETER_INVALID_STATE={state}");
println!("GREETER_INVALID_MESSAGE={message}"); println!("GREETER_INVALID_MESSAGE={message}");
if ok { if ok {
@@ -202,9 +183,9 @@ fn run_invalid(username: &str, password: &str) -> Result<(), String> {
Ok(()) Ok(())
} }
} }
Response::Error { message } => Err(format!("invalid-login request failed: {message}")), GreeterResponse::Error { message } => Err(format!("invalid-login request failed: {message}")),
Response::Other => Err(String::from("unexpected greeter response for invalid login")), GreeterResponse::ActionResult { .. } => Err(String::from("unexpected power response for invalid login")),
Response::HelloOk { .. } => Err(String::from("unexpected hello 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") fs::write(VALIDATION_REQUEST, b"bounded-session\n")
.map_err(|err| format!("failed to create validation request: {err}"))?; .map_err(|err| format!("failed to create validation request: {err}"))?;
match send_request(&Request::SubmitLogin { username, password })? { match send_request(&Request::SubmitLogin {
Response::LoginResult { ok, state, message } => { username: username.to_string(),
password: password.to_string(),
})? {
GreeterResponse::LoginResult { ok, state, message } => {
println!("GREETER_VALID_STATE={state}"); println!("GREETER_VALID_STATE={state}");
println!("GREETER_VALID_MESSAGE={message}"); println!("GREETER_VALID_MESSAGE={message}");
if !ok { if !ok {
@@ -223,15 +207,15 @@ fn run_valid(username: &str, password: &str) -> Result<(), String> {
return Err(String::from("valid login unexpectedly failed")); return Err(String::from("valid login unexpectedly failed"));
} }
} }
Response::Error { message } => { GreeterResponse::Error { message } => {
let _ = fs::remove_file(VALIDATION_REQUEST); let _ = fs::remove_file(VALIDATION_REQUEST);
return Err(format!("valid-login request failed: {message}")); return Err(format!("valid-login request failed: {message}"));
} }
Response::Other => { GreeterResponse::ActionResult { .. } => {
let _ = fs::remove_file(VALIDATION_REQUEST); 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); let _ = fs::remove_file(VALIDATION_REQUEST);
return Err(String::from("unexpected hello response for valid login")); return Err(String::from("unexpected hello response for valid login"));
} }
@@ -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<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 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();
}
}
@@ -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<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 collect_irq_reports(root: &Path) -> Vec<IrqReport> {
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::<u32>().ok(),
"device" => device = Some(value.trim().to_string()),
"mode" => mode = Some(value.trim().to_string()),
"reason" => reason = Some(value.trim().to_string()),
_ => {}
}
}
if let (Some(driver), Some(pid), Some(device), Some(mode), Some(reason)) =
(driver, pid, device, mode, reason)
{
if !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();
}
}
@@ -19,6 +19,20 @@ fn require_path(path: &str) -> Result<(), String> {
} }
} }
fn monotonic_path() -> Result<String, String> {
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<TimeSpec, String> { fn read_timespec(fd: &Fd) -> Result<TimeSpec, String> {
let mut time = TimeSpec::default(); let mut time = TimeSpec::default();
let bytes = libredox::call::read(fd.raw(), &mut time) let bytes = libredox::call::read(fd.raw(), &mut time)
@@ -43,8 +57,7 @@ fn run() -> Result<(), String> {
println!("=== Red Bear OS Timer Runtime Check ==="); println!("=== Red Bear OS Timer Runtime Check ===");
let time_path = format!("/scheme/time/{}", flag::CLOCK_MONOTONIC); let time_path = monotonic_path()?;
require_path(&time_path)?;
let time_fd = Fd::open(&time_path, flag::O_RDWR, 0) let time_fd = Fd::open(&time_path, flag::O_RDWR, 0)
.map_err(|err| format!("failed to open {time_path}: {err}"))?; .map_err(|err| format!("failed to open {time_path}: {err}"))?;
@@ -274,6 +274,10 @@ impl PowerRuntime {
} }
} }
fn available(&self) -> bool {
Path::new(POWER_ROOT).exists()
}
fn expected_device_paths(&self) -> BTreeSet<String> { fn expected_device_paths(&self) -> BTreeSet<String> {
let mut paths = BTreeSet::new(); let mut paths = BTreeSet::new();
for adapter_id in &self.adapter_ids { for adapter_id in &self.adapter_ids {
@@ -289,8 +293,13 @@ impl PowerRuntime {
fn validate_upower(list_names_output: &str) -> Result<(), String> { fn validate_upower(list_names_output: &str) -> Result<(), String> {
let runtime = PowerRuntime::discover(); let runtime = PowerRuntime::discover();
let expected_device_paths = runtime.expected_device_paths(); 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_ADAPTERS={}", runtime.adapter_ids.len());
println!("UPOWER_RUNTIME_BATTERIES={}", runtime.battery_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( let enumerate_output = run_command_with_retry(
DBUS_SEND, 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"); 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 let missing_device_paths = expected_device_paths
.difference(&enumerated_device_paths) .difference(&enumerated_device_paths)
.cloned() .cloned()
@@ -736,6 +757,13 @@ mod tests {
assert!(disk_paths.contains("/org/freedesktop/UDisks2/block_devices/disk_2e_nvme_0")); 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] #[test]
fn managed_object_keys_with_prefix_ignores_property_object_paths() { fn managed_object_keys_with_prefix_ignores_property_object_paths() {
let output = r#" let output = r#"
@@ -8,4 +8,5 @@ name = "redbear-info"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
redox-driver-sys = { path = "../../../../recipes/drivers/redox-driver-sys/source" }
toml = "0.8" toml = "0.8"
@@ -5,6 +5,8 @@ use std::path::PathBuf;
use std::process; use std::process;
use std::time::{SystemTime, UNIX_EPOCH}; 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; use toml::Value;
#[cfg(test)] #[cfg(test)]
@@ -92,12 +94,29 @@ struct NetworkReport {
struct HardwareReport { struct HardwareReport {
pci_devices: usize, pci_devices: usize,
pci_irq_none: usize,
pci_irq_legacy: usize,
pci_irq_msi: usize,
pci_irq_msix: usize,
pci_irq_forced_legacy: usize,
pci_irq_msix_disabled_by_quirk: usize,
pci_irq_msi_disabled_by_quirk: usize,
runtime_irq_reports: Vec<IrqRuntimeReport>,
usb_controllers: usize, usb_controllers: usize,
drm_cards: usize, drm_cards: usize,
acpi_power_surface_present: bool,
rtl8125_present: bool, rtl8125_present: bool,
virtio_net_present: bool, virtio_net_present: bool,
} }
struct IrqRuntimeReport {
driver: String,
pid: u32,
device: String,
mode: String,
reason: String,
}
struct QuirkFile { struct QuirkFile {
name: String, name: String,
pci_quirks: Vec<QuirkEntry>, pci_quirks: Vec<QuirkEntry>,
@@ -333,6 +352,16 @@ const INTEGRATIONS: &[IntegrationCheck] = &[
note: "Functional when the firmware scheme is enumerable.", note: "Functional when the firmware scheme is enumerable.",
functional_probe: Some(probe_firmware_scheme), 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 { IntegrationCheck {
name: "iommu", name: "iommu",
category: "System", category: "System",
@@ -855,6 +884,14 @@ fn collect_hardware(runtime: &Runtime, network: &NetworkReport) -> HardwareRepor
.iter() .iter()
.filter(|entry| entry.contains("--") && entry.contains('.')) .filter(|entry| entry.contains("--") && entry.contains('.'))
.count(); .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 let usb_controllers = runtime
.read_dir_names("/scheme") .read_dir_names("/scheme")
@@ -869,19 +906,42 @@ fn collect_hardware(runtime: &Runtime, network: &NetworkReport) -> HardwareRepor
.into_iter() .into_iter()
.filter(|name| name.starts_with("card")) .filter(|name| name.starts_with("card"))
.count(); .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 config_path = format!("/scheme/pci/{entry}/config");
let Some(bytes) = read_prefix_bytes(runtime, &config_path, 4) else { let Some(bytes) = read_prefix_bytes(runtime, &config_path, 64) else {
return false; continue;
}; };
if bytes.len() < 4 { if bytes.len() < 64 {
return false; continue;
} }
let vendor = u16::from_le_bytes([bytes[0], bytes[1]]); if let Some(location) = parse_scheme_pci_location(entry) {
let device = u16::from_le_bytes([bytes[2], bytes[3]]); if let Some(info) = parse_device_info_from_config_space(location, &bytes) {
vendor == RTL8125_VENDOR_ID && device == RTL8125_DEVICE_ID match info.interrupt_support() {
}) || network 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 .network_schemes
.iter() .iter()
.any(|name| name.contains("rtl8125")); .any(|name| name.contains("rtl8125"));
@@ -909,13 +969,99 @@ fn collect_hardware(runtime: &Runtime, network: &NetworkReport) -> HardwareRepor
HardwareReport { HardwareReport {
pci_devices, 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, usb_controllers,
drm_cards, drm_cards,
acpi_power_surface_present,
rtl8125_present, rtl8125_present,
virtio_net_present, virtio_net_present,
} }
} }
fn parse_scheme_pci_location(entry: &str) -> Option<redox_driver_sys::pci::PciLocation> {
let (segment, rest) = entry.split_once("--")?;
let (bus, rest) = rest.split_once("--")?;
let (device, function) = rest.split_once('.')?;
Some(redox_driver_sys::pci::PciLocation {
segment: u16::from_str_radix(segment, 16).ok()?,
bus: u8::from_str_radix(bus, 16).ok()?,
device: u8::from_str_radix(device, 16).ok()?,
function: function.parse().ok()?,
})
}
fn collect_irq_runtime_reports(runtime: &Runtime) -> Vec<IrqRuntimeReport> {
let mut reports = Vec::new();
let mut seen = std::collections::BTreeSet::new();
for dir in [
"/tmp/redbear-irq-report",
"/tmp/run/redbear-irq-report",
"/run/redbear-irq-report",
"/var/run/redbear-irq-report",
"/scheme/initfs/tmp/redbear-irq-report",
"/scheme/initfs/tmp/run/redbear-irq-report",
"/scheme/initfs/run/redbear-irq-report",
"/scheme/initfs/var/run/redbear-irq-report",
] {
let entries = runtime.read_dir_names(dir).unwrap_or_default();
for name in entries.into_iter().filter(|name| name.ends_with(".env")) {
let path = format!("{dir}/{name}");
if !seen.insert(path.clone()) {
continue;
}
let Some(content) = runtime.read_to_string(&path) else {
continue;
};
let mut driver = None;
let mut pid = None;
let mut device = None;
let mut mode = None;
let mut reason = None;
for line in content.lines() {
let Some((key, value)) = line.split_once('=') else {
continue;
};
match key.trim() {
"driver" => driver = Some(value.trim().to_string()),
"pid" => pid = value.trim().parse::<u32>().ok(),
"device" => device = Some(value.trim().to_string()),
"mode" => mode = Some(value.trim().to_string()),
"reason" => reason = Some(value.trim().to_string()),
_ => {}
}
}
if let (Some(driver), Some(pid), Some(device), Some(mode), Some(reason)) =
(driver, pid, device, mode, reason)
{
if !runtime.exists(&format!("/proc/{pid}")) {
continue;
}
reports.push(IrqRuntimeReport {
driver,
pid,
device,
mode,
reason,
});
}
}
}
reports.sort_by(|left, right| left.driver.cmp(&right.driver).then(left.device.cmp(&right.device)));
reports
}
fn collect_quirks(runtime: &Runtime) -> QuirksReport { fn collect_quirks(runtime: &Runtime) -> QuirksReport {
let mut files_loaded = Vec::new(); let mut files_loaded = Vec::new();
let mut load_errors = Vec::new(); let mut load_errors = Vec::new();
@@ -1359,8 +1505,38 @@ fn print_table(report: &Report<'_>, verbose: bool) {
print_section_header("Hardware"); print_section_header("Hardware");
println!(" PCI devices: {}", report.hardware.pci_devices); 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!(" USB controllers: {}", report.hardware.usb_controllers);
println!(" DRM cards: {}", report.hardware.drm_cards); println!(" DRM cards: {}", report.hardware.drm_cards);
println!(
" ACPI power surface: {}",
if report.hardware.acpi_power_surface_present {
"present"
} else {
"unavailable"
}
);
println!( println!(
" RTL8125 device seen: {}", " RTL8125 device seen: {}",
if report.hardware.rtl8125_present { if report.hardware.rtl8125_present {
@@ -1758,6 +1934,70 @@ fn print_json(report: &Report<'_>) {
true, true,
4, 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( push_json_number_field(
&mut out, &mut out,
"usb_controllers", "usb_controllers",
@@ -1766,6 +2006,13 @@ fn print_json(report: &Report<'_>) {
4, 4,
); );
push_json_number_field(&mut out, "drm_cards", report.hardware.drm_cards, true, 4); push_json_number_field(&mut out, "drm_cards", report.hardware.drm_cards, true, 4);
push_json_bool_field(
&mut out,
"acpi_power_surface_present",
report.hardware.acpi_power_surface_present,
true,
4,
);
push_json_bool_field( push_json_bool_field(
&mut out, &mut out,
"rtl8125_present", "rtl8125_present",
@@ -2160,6 +2407,23 @@ fn probe_iommu_scheme(
probe_named_scheme(runtime, "iommu") probe_named_scheme(runtime, "iommu")
} }
fn probe_acpi_power_surface(
runtime: &Runtime,
_network: &NetworkReport,
_hardware: &HardwareReport,
_check: &IntegrationCheck,
) -> Option<String> {
let adapters = runtime.read_dir_names("/scheme/acpi/power/adapters");
let batteries = runtime.read_dir_names("/scheme/acpi/power/batteries");
runtime.exists("/scheme/acpi/power").then(|| {
format!(
"acpi power surface visible (adapters={}, batteries={})",
adapters.as_ref().map(|items| items.len()).unwrap_or(0),
batteries.as_ref().map(|items| items.len()).unwrap_or(0)
)
})
}
fn probe_serio_surface( fn probe_serio_surface(
runtime: &Runtime, runtime: &Runtime,
_network: &NetworkReport, _network: &NetworkReport,
@@ -2927,16 +3191,12 @@ mod tests {
fn rtl8125_hardware_detection_parses_pci_config() { fn rtl8125_hardware_detection_parses_pci_config() {
let root = temp_root(); let root = temp_root();
create_dir(&root, "/scheme/pci/0000--02--00.0"); create_dir(&root, "/scheme/pci/0000--02--00.0");
let config = [ let mut config = [0u8; 64];
(RTL8125_VENDOR_ID & 0xff) as u8, config[0x00] = (RTL8125_VENDOR_ID & 0xff) as u8;
(RTL8125_VENDOR_ID >> 8) as u8, config[0x01] = (RTL8125_VENDOR_ID >> 8) as u8;
(RTL8125_DEVICE_ID & 0xff) as u8, config[0x02] = (RTL8125_DEVICE_ID & 0xff) as u8;
(RTL8125_DEVICE_ID >> 8) as u8, config[0x03] = (RTL8125_DEVICE_ID >> 8) as u8;
0, config[0x0e] = 0x00;
0,
0,
0,
];
let path = root.join("scheme/pci/0000--02--00.0/config"); let path = root.join("scheme/pci/0000--02--00.0/config");
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap(); fs::create_dir_all(parent).unwrap();
@@ -2954,16 +3214,12 @@ mod tests {
fn virtio_net_hardware_detection_parses_pci_config() { fn virtio_net_hardware_detection_parses_pci_config() {
let root = temp_root(); let root = temp_root();
create_dir(&root, "/scheme/pci/0000--00--03.0"); create_dir(&root, "/scheme/pci/0000--00--03.0");
let config = [ let mut config = [0u8; 64];
(VIRTIO_NET_VENDOR_ID & 0xff) as u8, config[0x00] = (VIRTIO_NET_VENDOR_ID & 0xff) as u8;
(VIRTIO_NET_VENDOR_ID >> 8) as u8, config[0x01] = (VIRTIO_NET_VENDOR_ID >> 8) as u8;
(VIRTIO_NET_DEVICE_ID & 0xff) as u8, config[0x02] = (VIRTIO_NET_DEVICE_ID & 0xff) as u8;
(VIRTIO_NET_DEVICE_ID >> 8) as u8, config[0x03] = (VIRTIO_NET_DEVICE_ID >> 8) as u8;
0, config[0x0e] = 0x00;
0,
0,
0,
];
let path = root.join("scheme/pci/0000--00--03.0/config"); let path = root.join("scheme/pci/0000--00--03.0/config");
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap(); fs::create_dir_all(parent).unwrap();
@@ -2977,6 +3233,127 @@ mod tests {
fs::remove_dir_all(root).unwrap(); 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] #[test]
fn json_output_contains_network_and_integration_state() { fn json_output_contains_network_and_integration_state() {
let root = temp_root(); let root = temp_root();
@@ -2997,6 +3374,7 @@ mod tests {
let report = collect_report(&Runtime::from_root(root.clone())); let report = collect_report(&Runtime::from_root(root.clone()));
assert!(!report.hardware.virtio_net_present); assert!(!report.hardware.virtio_net_present);
assert!(!report.hardware.acpi_power_surface_present);
let mut output = String::new(); let mut output = String::new();
output.push_str("{"); output.push_str("{");
push_json_string_field( push_json_string_field(
@@ -3031,6 +3409,12 @@ mod tests {
.iter() .iter()
.any(|item| item.check.name == "redbear-btctl") .any(|item| item.check.name == "redbear-btctl")
); );
assert!(
report
.integrations
.iter()
.any(|item| item.check.name == "redbear-upower")
);
assert!( assert!(
report report
.integrations .integrations
@@ -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"
@@ -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<i32>,
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);
}
}
@@ -251,11 +251,19 @@ fn env_value(keys: &[&str]) -> Option<String> {
fn build_environment(account: &Account, args: &Args, runtime_dir: &Path) -> BTreeMap<String, String> { fn build_environment(account: &Account, args: &Args, runtime_dir: &Path) -> BTreeMap<String, String> {
let mut values = BTreeMap::new(); 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("USER"), account.username.clone());
values.insert(String::from("LOGNAME"), account.username.clone()); values.insert(String::from("LOGNAME"), account.username.clone());
values.insert(String::from("SHELL"), account.shell.clone()); values.insert(String::from("SHELL"), account.shell.clone());
values.insert(String::from("PATH"), String::from("/usr/bin:/bin")); 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("XDG_RUNTIME_DIR"), runtime_dir.display().to_string());
values.insert(String::from("WAYLAND_DISPLAY"), args.wayland_display.clone()); values.insert(String::from("WAYLAND_DISPLAY"), args.wayland_display.clone());
values.insert(String::from("XDG_SEAT"), String::from("seat0")); 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("DISPLAY"), String::new());
values.insert(String::from("XDG_SESSION_TYPE"), String::from("wayland")); 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"]) { if let Some(theme) = env_value(&["XCURSOR_THEME"]) {
values.insert(String::from("XCURSOR_THEME"), 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 => { LaunchMode::Session => {
values.insert(String::from("XDG_CURRENT_DESKTOP"), String::from("KDE")); values.insert(String::from("XDG_CURRENT_DESKTOP"), String::from("KDE"));
values.insert(String::from("KDE_FULL_SESSION"), String::from("true")); values.insert(String::from("KDE_FULL_SESSION"), String::from("true"));
values.insert(String::from("XDG_SESSION_ID"), String::from("c1"));
} }
LaunchMode::Command { .. } => {} LaunchMode::Command { .. } => {}
} }
@@ -485,9 +506,15 @@ mod tests {
}; };
let envs = build_environment(&account, &args, Path::new("/run/user/1000")); 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_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["KDE_FULL_SESSION"], "true");
assert_eq!(envs["XDG_SESSION_ID"], "c1");
assert_eq!(envs["XDG_VTNR"], "3"); assert_eq!(envs["XDG_VTNR"], "3");
assert!(envs.contains_key("LANG"));
} }
#[test] #[test]
@@ -512,8 +539,13 @@ mod tests {
}; };
let envs = build_environment(&account, &args, Path::new("/tmp/run/greeter")); 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("XDG_CURRENT_DESKTOP"));
assert!(!envs.contains_key("KDE_FULL_SESSION")); assert!(!envs.contains_key("KDE_FULL_SESSION"));
assert!(!envs.contains_key("XDG_SESSION_ID"));
assert_eq!(envs["XDG_SESSION_TYPE"], "wayland"); assert_eq!(envs["XDG_SESSION_TYPE"], "wayland");
} }
@@ -1,5 +1,7 @@
use zbus::Connection; use zbus::Connection;
use crate::runtime_state::SharedRuntime;
#[cfg(target_os = "redox")] #[cfg(target_os = "redox")]
const KSTOP_PATH: &str = "/scheme/kernel.acpi/kstop"; const KSTOP_PATH: &str = "/scheme/kernel.acpi/kstop";
@@ -13,10 +15,13 @@ fn wait_for_shutdown_edge() -> std::io::Result<()> {
Ok(()) Ok(())
} }
pub async fn watch_and_emit(connection: Connection) { pub async fn watch_and_emit(connection: Connection, runtime: SharedRuntime) {
#[cfg(target_os = "redox")] #[cfg(target_os = "redox")]
match tokio::task::spawn_blocking(wait_for_shutdown_edge).await { match tokio::task::spawn_blocking(wait_for_shutdown_edge).await {
Ok(Ok(())) => { Ok(Ok(())) => {
if let Ok(mut state) = runtime.write() {
state.preparing_for_shutdown = true;
}
let _ = connection let _ = connection
.emit_signal( .emit_signal(
None::<&str>, None::<&str>,
@@ -38,5 +43,6 @@ pub async fn watch_and_emit(connection: Connection) {
#[cfg(not(target_os = "redox"))] #[cfg(not(target_os = "redox"))]
{ {
let _ = connection; let _ = connection;
let _ = runtime;
} }
} }
@@ -133,7 +133,7 @@ async fn run_daemon() -> Result<(), Box<dyn Error>> {
Ok(connection) => { Ok(connection) => {
eprintln!("redbear-sessiond: registered {BUS_NAME} on the system bus"); eprintln!("redbear-sessiond: registered {BUS_NAME} on the system bus");
control::start_control_socket(runtime.clone()); 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?; wait_for_shutdown().await?;
drop(connection); drop(connection);
return Ok(()); return Ok(());
@@ -31,7 +31,7 @@ impl LoginManager {
.runtime .runtime
.read() .read()
.map_err(|_| fdo::Error::Failed(String::from("login1 runtime state is poisoned")))?; .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()); return Ok(self.session_path.clone());
} }
@@ -111,7 +111,10 @@ impl LoginManager {
#[zbus(property(emits_changed_signal = "const"), name = "PreparingForShutdown")] #[zbus(property(emits_changed_signal = "const"), name = "PreparingForShutdown")]
fn preparing_for_shutdown(&self) -> bool { fn preparing_for_shutdown(&self) -> bool {
false self.runtime
.read()
.map(|runtime| runtime.preparing_for_shutdown)
.unwrap_or(false)
} }
#[zbus(signal, name = "PrepareForSleep")] #[zbus(signal, name = "PrepareForSleep")]
@@ -123,3 +126,60 @@ impl LoginManager {
before: bool, before: bool,
) -> zbus::Result<()>; ) -> 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());
}
}
@@ -10,6 +10,7 @@ pub struct SessionRuntime {
pub leader: u32, pub leader: u32,
pub state: String, pub state: String,
pub active: bool, pub active: bool,
pub preparing_for_shutdown: bool,
} }
impl Default for SessionRuntime { impl Default for SessionRuntime {
@@ -23,6 +24,7 @@ impl Default for SessionRuntime {
leader: std::process::id(), leader: std::process::id(),
state: String::from("online"), state: String::from("online"),
active: true, active: true,
preparing_for_shutdown: false,
} }
} }
} }
@@ -1,4 +1,4 @@
use std::sync::Mutex; use std::{process::Command, sync::Mutex};
use zbus::{fdo, interface, zvariant::OwnedObjectPath}; use zbus::{fdo, interface, zvariant::OwnedObjectPath};
@@ -27,17 +27,55 @@ impl LoginSeat {
.lock() .lock()
.map_err(|_| fdo::Error::Failed(String::from("seat VT state is poisoned"))) .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")] #[interface(name = "org.freedesktop.login1.Seat")]
impl LoginSeat { impl LoginSeat {
fn switch_to(&mut self, vt: u32) -> fdo::Result<()> { fn switch_to(&mut self, vt: u32) -> fdo::Result<()> {
Self::request_vt_switch("inputd", vt)?;
let mut last_requested_vt = self.last_requested_vt()?; let mut last_requested_vt = self.last_requested_vt()?;
*last_requested_vt = vt; *last_requested_vt = vt;
eprintln!(
"redbear-sessiond: SwitchTo requested for seat {} -> vt {vt} (delegated to inputd -A externally)", let mut runtime = self
self.id .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(()) Ok(())
} }
@@ -48,24 +86,12 @@ impl LoginSeat {
#[zbus(property(emits_changed_signal = "const"), name = "ActiveSession")] #[zbus(property(emits_changed_signal = "const"), name = "ActiveSession")]
fn active_session(&self) -> (String, OwnedObjectPath) { fn active_session(&self) -> (String, OwnedObjectPath) {
( (self.current_session_id(), self.session_path.clone())
self.runtime
.read()
.map(|runtime| runtime.session_id.clone())
.unwrap_or_else(|_| String::from("c1")),
self.session_path.clone(),
)
} }
#[zbus(property(emits_changed_signal = "const"), name = "Sessions")] #[zbus(property(emits_changed_signal = "const"), name = "Sessions")]
fn sessions(&self) -> Vec<(String, OwnedObjectPath)> { fn sessions(&self) -> Vec<(String, OwnedObjectPath)> {
vec![( vec![(self.current_session_id(), self.session_path.clone())]
self.runtime
.read()
.map(|runtime| runtime.session_id.clone())
.unwrap_or_else(|_| String::from("c1")),
self.session_path.clone(),
)]
} }
#[zbus(property(emits_changed_signal = "const"), name = "CanGraphical")] #[zbus(property(emits_changed_signal = "const"), name = "CanGraphical")]
@@ -83,3 +109,39 @@ impl LoginSeat {
false 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");
}
}
@@ -4,7 +4,6 @@ use std::{
fs, fs,
path::{Path, PathBuf}, path::{Path, PathBuf},
process, process,
thread,
time::Duration, time::Duration,
}; };
@@ -178,6 +177,14 @@ fn battery_state_to_upower(state_bits: u64, percentage: Option<f64>) -> u32 {
DEVICE_STATE_UNKNOWN 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 { impl PowerRuntime {
fn discover() -> Result<Self, Box<dyn Error>> { fn discover() -> Result<Self, Box<dyn Error>> {
let root = PathBuf::from(ACPI_POWER_ROOT); 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()); let mut object_paths = Vec::with_capacity(adapter_ids.len() + battery_ids.len());
for adapter_id in &adapter_ids { for adapter_id in &adapter_ids {
object_paths.push(parse_object_path(&format!( object_paths.push(parse_object_path(&adapter_object_path(adapter_id))?);
"/org/freedesktop/UPower/devices/line_power_{adapter_id}"
))?);
} }
for battery_id in &battery_ids { for battery_id in &battery_ids {
object_paths.push(parse_object_path(&format!( object_paths.push(parse_object_path(&battery_object_path(battery_id))?);
"/org/freedesktop/UPower/devices/battery_{battery_id}"
))?);
} }
Ok(Self { Ok(Self {
@@ -204,6 +207,10 @@ impl PowerRuntime {
}) })
} }
fn available(&self) -> bool {
self.root.exists()
}
fn adapter_dir(&self, id: &str) -> PathBuf { fn adapter_dir(&self, id: &str) -> PathBuf {
self.root.join("adapters").join(id) self.root.join("adapters").join(id)
} }
@@ -472,6 +479,11 @@ impl PowerDevice {
async fn run_daemon() -> Result<(), Box<dyn Error>> { async fn run_daemon() -> Result<(), Box<dyn Error>> {
wait_for_dbus_socket().await; wait_for_dbus_socket().await;
let runtime = PowerRuntime::discover()?; 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 _display_device_path = parse_object_path(DISPLAY_DEVICE_PATH)?;
let mut last_err = None; let mut last_err = None;
@@ -492,7 +504,7 @@ async fn run_daemon() -> Result<(), Box<dyn Error>> {
)?; )?;
for adapter_id in &runtime.adapter_ids { 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( builder = builder.serve_at(
path, path,
PowerDevice { PowerDevice {
@@ -502,7 +514,7 @@ async fn run_daemon() -> Result<(), Box<dyn Error>> {
)?; )?;
} }
for battery_id in &runtime.battery_ids { 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( builder = builder.serve_at(
path, path,
PowerDevice { 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);
}
}