Advance redbear-full Wayland, greeter, and Qt integration

Consolidate the active desktop path around redbear-full while landing the greeter/session stack and the runtime fixes needed to keep Wayland and KWin bring-up moving forward.
This commit is contained in:
2026-04-19 17:59:58 +01:00
parent 370d27f44d
commit 9880e0a5b2
137 changed files with 14176 additions and 2016 deletions
+46 -8
View File
@@ -28,6 +28,7 @@ struct DiscoveryResult {
source: DiscoverySource,
kernel_acpi_status: &'static str,
ivrs_path: Option<PathBuf>,
dmar_present: bool,
}
#[cfg_attr(not(target_os = "redox"), allow(dead_code))]
@@ -141,7 +142,7 @@ fn read_sdt_from_physical(phys_addr: u64) -> Result<Vec<u8>, String> {
}
#[cfg(target_os = "redox")]
fn detect_units_from_kernel_acpi() -> Result<Vec<AmdViUnit>, String> {
fn find_kernel_acpi_table(signature: &[u8; 4]) -> Result<Option<Vec<u8>>, String> {
let rxsdt = match fs::read("/scheme/kernel.acpi/rxsdt") {
Ok(bytes) => bytes,
Err(err) => {
@@ -150,14 +151,14 @@ fn detect_units_from_kernel_acpi() -> Result<Vec<AmdViUnit>, String> {
};
if rxsdt.len() < ACPI_HEADER_LEN {
return Ok(Vec::new());
return Ok(None);
}
let signature = &rxsdt[0..4];
let entry_size = match signature {
let root_signature = &rxsdt[0..4];
let entry_size = match root_signature {
b"RSDT" => 4,
b"XSDT" => 8,
_ => return Ok(Vec::new()),
_ => return Ok(None),
};
let mut offset = ACPI_HEADER_LEN;
@@ -169,24 +170,46 @@ fn detect_units_from_kernel_acpi() -> Result<Vec<AmdViUnit>, String> {
};
let table = read_sdt_from_physical(phys_addr)?;
if table.len() >= 4 && &table[0..4] == b"IVRS" {
return AmdViUnit::detect(&table).map_err(|err| format!("failed to parse IVRS: {err}"));
if table.len() >= 4 && &table[0..4] == signature {
return Ok(Some(table));
}
offset += entry_size;
}
Ok(Vec::new())
Ok(None)
}
#[cfg(target_os = "redox")]
fn detect_units_from_kernel_acpi() -> Result<Vec<AmdViUnit>, String> {
match find_kernel_acpi_table(b"IVRS")? {
Some(table) => AmdViUnit::detect(&table).map_err(|err| format!("failed to parse IVRS: {err}")),
None => Ok(Vec::new()),
}
}
#[cfg(target_os = "redox")]
fn detect_dmar_from_kernel_acpi() -> Result<bool, String> {
Ok(find_kernel_acpi_table(b"DMAR")?.is_some())
}
#[cfg(target_os = "redox")]
fn discover_units() -> Result<DiscoveryResult, String> {
let dmar_present = match detect_dmar_from_kernel_acpi() {
Ok(present) => present,
Err(err) => {
info!("iommu: kernel ACPI DMAR discovery unavailable: {err}");
false
}
};
match detect_units_from_kernel_acpi() {
Ok(units) if !units.is_empty() => Ok(DiscoveryResult {
units,
source: DiscoverySource::KernelAcpi,
kernel_acpi_status: "ok",
ivrs_path: None,
dmar_present,
}),
Ok(_units) => {
let (units, ivrs_path) = detect_units_from_discovered_ivrs()?;
@@ -199,6 +222,7 @@ fn discover_units() -> Result<DiscoveryResult, String> {
units,
kernel_acpi_status: "empty",
ivrs_path,
dmar_present,
})
}
Err(err) => {
@@ -213,6 +237,7 @@ fn discover_units() -> Result<DiscoveryResult, String> {
units,
kernel_acpi_status: "error",
ivrs_path,
dmar_present,
})
}
}
@@ -230,6 +255,7 @@ fn discover_units() -> Result<DiscoveryResult, String> {
units,
kernel_acpi_status: "unsupported",
ivrs_path,
dmar_present: false,
})
}
@@ -254,6 +280,11 @@ fn run() -> Result<(), String> {
discovery.source.as_str()
);
}
if discovery.dmar_present {
info!(
"iommu: detected kernel ACPI DMAR table; Intel VT-d runtime ownership should converge here rather than remain in acpid"
);
}
for (index, unit) in discovery.units.iter().enumerate() {
info!(
"iommu: discovered unit {} at MMIO {:#x}; initialization is deferred until first use",
@@ -308,6 +339,7 @@ fn run_self_test() -> Result<(), String> {
println!("discovery_source={}", discovery.source.as_str());
println!("kernel_acpi_status={}", discovery.kernel_acpi_status);
println!("dmar_present={}", if discovery.dmar_present { 1 } else { 0 });
println!(
"ivrs_path={}",
discovery
@@ -430,4 +462,10 @@ mod tests {
assert_eq!(DiscoverySource::Filesystem.as_str(), "filesystem");
assert_eq!(DiscoverySource::None.as_str(), "none");
}
#[test]
fn host_discovery_defaults_to_no_dmar() {
let discovery = super::discover_units().expect("host discovery should succeed");
assert!(!discovery.dmar_present);
}
}
@@ -0,0 +1,8 @@
[source]
path = "source"
[build]
template = "cargo"
[package.files]
"/usr/bin/redbear-authd" = "redbear-authd"
@@ -0,0 +1,15 @@
[package]
name = "redbear-authd"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "redbear-authd"
path = "src/main.rs"
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# Pure-Rust SHA-256/SHA-512 crypt verifier for /etc/shadow entries.
# Free/open-source (`MIT OR Apache-2.0` upstream; acceptable under the project's free-software policy).
sha-crypt = "0.6.0-rc.4"
@@ -0,0 +1,741 @@
use std::{
collections::HashMap,
env,
fs,
io::{BufRead, BufReader, Write},
os::unix::{fs::PermissionsExt, net::{UnixListener, UnixStream}},
path::Path,
process::{self, Command},
sync::{Arc, Mutex},
time::{Duration, Instant},
};
use serde::{Deserialize, Serialize};
use sha_crypt::{PasswordVerifier, ShaCrypt};
#[derive(Debug, PartialEq, Eq)]
enum VerifyError {
UnsupportedHashFormat,
}
const AUTH_SOCKET_PATH: &str = "/run/redbear-authd.sock";
const SESSIOND_SOCKET_PATH: &str = "/run/redbear-sessiond-control.sock";
const FAILURE_WINDOW: Duration = Duration::from_secs(60);
const LOCKOUT_DURATION: Duration = Duration::from_secs(30);
#[derive(Clone, Debug)]
struct Account {
username: String,
password: String,
uid: u32,
shell: String,
}
#[derive(Clone, Debug)]
struct Approval {
expires_at: Instant,
vt: u32,
}
#[derive(Clone, Debug, Default)]
struct FailureState {
attempts: Vec<Instant>,
locked_until: Option<Instant>,
}
#[derive(Clone, Debug, Default)]
struct RuntimeState {
approvals: Arc<Mutex<HashMap<String, Approval>>>,
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)]
#[serde(tag = "type", rename_all = "snake_case")]
enum SessiondUpdate {
SetSession {
username: String,
uid: u32,
vt: u32,
leader: u32,
state: String,
},
ResetSession {
vt: u32,
},
}
fn usage() -> &'static str {
"Usage: redbear-authd [--help]"
}
fn parse_args() -> Result<(), String> {
let mut args = env::args().skip(1);
match args.next() {
None => Ok(()),
Some(arg) if arg == "--help" || arg == "-h" => Err(String::new()),
Some(arg) => Err(format!("unrecognized argument '{arg}'")),
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum AccountFormat {
Redox,
Unix,
}
fn split_account_fields(line: &str) -> (AccountFormat, Vec<String>) {
let format = if line.contains(';') {
AccountFormat::Redox
} else {
AccountFormat::Unix
};
let delimiter = match format {
AccountFormat::Redox => ';',
AccountFormat::Unix => ':',
};
(format, line.split(delimiter).map(str::to_string).collect::<Vec<_>>())
}
fn load_shadow_passwords() -> Result<HashMap<String, String>, String> {
if !Path::new("/etc/shadow").exists() {
return Ok(HashMap::new());
}
let mut passwords = HashMap::new();
let contents = fs::read_to_string("/etc/shadow")
.map_err(|err| format!("failed to read /etc/shadow: {err}"))?;
for (index, raw_line) in contents.lines().enumerate() {
let line = raw_line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let (_format, parts) = split_account_fields(line);
if parts.len() < 2 {
return Err(format!("invalid shadow entry on line {}", index + 1));
}
passwords.insert(parts[0].clone(), parts[1].clone());
}
Ok(passwords)
}
fn load_account(username: &str) -> Result<Account, String> {
let shadow_passwords = load_shadow_passwords()?;
let contents = fs::read_to_string("/etc/passwd")
.map_err(|err| format!("failed to read /etc/passwd: {err}"))?;
for (index, raw_line) in contents.lines().enumerate() {
let line = raw_line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let (format, parts) = split_account_fields(line);
if parts[0] != username {
continue;
}
let (uid_index, gid_index, shell_index, passwd_index) = match format {
AccountFormat::Redox if parts.len() >= 6 => (1, 2, 5, None),
AccountFormat::Unix if parts.len() >= 7 => (2, 3, 6, Some(1)),
AccountFormat::Redox => {
return Err(format!("invalid Redox passwd entry for user '{username}' on line {}", index + 1))
}
AccountFormat::Unix => {
return Err(format!("invalid passwd entry for user '{username}' on line {}", index + 1))
}
};
let uid = parts[uid_index]
.parse::<u32>()
.map_err(|_| format!("invalid uid for user '{username}'"))?;
let _gid = parts[gid_index]
.parse::<u32>()
.map_err(|_| format!("invalid gid for user '{username}'"))?;
let password = shadow_passwords
.get(username)
.cloned()
.unwrap_or_else(|| passwd_index.map(|index| parts[index].clone()).unwrap_or_default());
return Ok(Account {
username: parts[0].clone(),
password,
uid,
shell: parts[shell_index].clone(),
});
}
Err(format!("unknown user '{username}'"))
}
fn trim_failures(entries: &mut Vec<Instant>, now: Instant) {
entries.retain(|entry| now.saturating_duration_since(*entry) <= FAILURE_WINDOW);
}
fn login_allowed(account: &Account) -> bool {
if account.uid != 0 && account.uid < 1000 {
return false;
}
!account.shell.is_empty()
}
fn verify_shadow_password(password: &str, shadow_hash: &str) -> Result<bool, VerifyError> {
if shadow_hash.starts_with("$6$") || shadow_hash.starts_with("$5$") {
return Ok(ShaCrypt::default()
.verify_password(password.as_bytes(), shadow_hash)
.is_ok());
}
Err(VerifyError::UnsupportedHashFormat)
}
fn verify_password(account: &Account, password: &str) -> bool {
if account.password.is_empty() || account.password.starts_with('!') || account.password.starts_with('*') {
return false;
}
if account.password.starts_with('$') {
match verify_shadow_password(password, &account.password) {
Ok(ok) => return ok,
Err(VerifyError::UnsupportedHashFormat) => {
eprintln!(
"redbear-authd: password hash for user {} uses an unsupported shadow format",
account.username
);
return false;
}
}
}
account.password == password
}
fn remember_success(state: &RuntimeState, username: &str, vt: u32) -> Result<(), String> {
let mut approvals = state
.approvals
.lock()
.map_err(|_| String::from("approval state is poisoned"))?;
approvals.insert(
username.to_string(),
Approval {
expires_at: Instant::now() + Duration::from_secs(15),
vt,
},
);
let mut failures = state
.failures
.lock()
.map_err(|_| String::from("failure state is poisoned"))?;
failures.remove(username);
Ok(())
}
fn remember_failure(state: &RuntimeState, username: &str) -> Result<String, String> {
let mut failures = state
.failures
.lock()
.map_err(|_| String::from("failure state is poisoned"))?;
let now = Instant::now();
let entry = failures.entry(username.to_string()).or_default();
trim_failures(&mut entry.attempts, now);
entry.attempts.push(now);
if entry.attempts.len() >= 5 {
entry.locked_until = Some(now + LOCKOUT_DURATION);
Ok(String::from("Too many failed attempts. Try again shortly."))
} else {
Ok(String::from("Invalid username or password."))
}
}
fn check_lockout(state: &RuntimeState, username: &str) -> Result<Option<String>, String> {
let mut failures = state
.failures
.lock()
.map_err(|_| String::from("failure state is poisoned"))?;
let now = Instant::now();
if let Some(entry) = failures.get_mut(username) {
trim_failures(&mut entry.attempts, now);
if let Some(locked_until) = entry.locked_until {
if locked_until > now {
return Ok(Some(String::from("Too many failed attempts. Try again shortly.")));
}
entry.locked_until = None;
}
}
Ok(None)
}
fn take_approval(state: &RuntimeState, username: &str, vt: u32) -> Result<(), String> {
let mut approvals = state
.approvals
.lock()
.map_err(|_| String::from("approval state is poisoned"))?;
let Some(approval) = approvals.remove(username) else {
return Err(String::from("No recent authentication approval exists for this user."));
};
if approval.expires_at < Instant::now() {
return Err(String::from("Authentication approval expired. Please log in again."));
}
if approval.vt != vt {
return Err(String::from("Authentication approval does not match the requested VT."));
}
Ok(())
}
fn send_sessiond_update(message: &SessiondUpdate) {
let Ok(mut stream) = UnixStream::connect(SESSIOND_SOCKET_PATH) else {
return;
};
let Ok(json) = serde_json::to_string(message) else {
return;
};
let _ = stream.write_all(json.as_bytes());
let _ = stream.write_all(b"\n");
}
fn launch_session(account: &Account, session: &str, vt: u32) -> Result<Option<i32>, String> {
if session != "kde-wayland" {
return Err(format!("unsupported session '{session}'"));
}
let mut child = Command::new("/usr/bin/redbear-session-launch")
.arg("--username")
.arg(&account.username)
.arg("--mode")
.arg("session")
.arg("--session")
.arg(session)
.arg("--vt")
.arg(vt.to_string())
.spawn()
.map_err(|err| format!("failed to launch session for {}: {err}", account.username))?;
send_sessiond_update(&SessiondUpdate::SetSession {
username: account.username.clone(),
uid: account.uid,
vt,
leader: child.id(),
state: String::from("online"),
});
let status = child
.wait()
.map_err(|err| format!("failed while waiting for session process: {err}"))?;
send_sessiond_update(&SessiondUpdate::ResetSession { vt });
Ok(status.code())
}
fn run_power_action(action: &str) -> Result<String, String> {
let candidates: &[&[&str]] = match action {
"shutdown" => &[&["/usr/bin/shutdown"], &["shutdown"], &["poweroff"]],
"reboot" => &[&["/usr/bin/reboot"], &["reboot"]],
other => return Err(format!("unsupported power action '{other}'")),
};
for candidate in candidates {
let program = candidate[0];
let args = &candidate[1..];
let Ok(status) = Command::new(program).args(args).status() else {
continue;
};
if status.success() {
return Ok(format!("{action} requested"));
}
}
Err(format!("failed to execute {action} command"))
}
fn handle_request(request: AuthRequest, state: &RuntimeState) -> AuthResponse {
match request {
AuthRequest::Authenticate {
request_id,
username,
password,
vt,
} => {
match check_lockout(state, &username) {
Ok(Some(message)) => {
return AuthResponse::AuthenticateResult {
request_id,
ok: false,
message,
};
}
Ok(None) => {}
Err(message) => return AuthResponse::Error { message },
}
match load_account(&username) {
Ok(account) if login_allowed(&account) && verify_password(&account, &password) => {
if let Err(message) = remember_success(state, &username, vt) {
return AuthResponse::Error { message };
}
AuthResponse::AuthenticateResult {
request_id,
ok: true,
message: String::from("Authentication successful."),
}
}
Ok(_) | Err(_) => {
let message = remember_failure(state, &username)
.unwrap_or_else(|_| String::from("Invalid username or password."));
AuthResponse::AuthenticateResult {
request_id,
ok: false,
message,
}
}
}
}
AuthRequest::StartSession {
request_id,
username,
session,
vt,
} => {
if let Err(message) = take_approval(state, &username, vt) {
return AuthResponse::SessionResult {
request_id,
ok: false,
exit_code: None,
message,
};
}
match load_account(&username).and_then(|account| {
let exit_code = launch_session(&account, &session, vt)?;
Ok((account, exit_code))
}) {
Ok((_account, exit_code)) => AuthResponse::SessionResult {
request_id,
ok: true,
exit_code,
message: String::from("Session completed."),
},
Err(message) => AuthResponse::SessionResult {
request_id,
ok: false,
exit_code: None,
message,
},
}
}
AuthRequest::PowerAction { request_id, action } => match run_power_action(&action) {
Ok(message) => AuthResponse::PowerResult {
request_id,
ok: true,
message,
},
Err(message) => AuthResponse::PowerResult {
request_id,
ok: false,
message,
},
},
}
}
fn handle_connection(stream: UnixStream, state: RuntimeState) {
let mut reader = BufReader::new(stream);
let mut line = String::new();
if reader.read_line(&mut line).is_err() {
return;
}
let response = match serde_json::from_str::<AuthRequest>(line.trim()) {
Ok(request) => handle_request(request, &state),
Err(err) => AuthResponse::Error {
message: format!("invalid request: {err}"),
},
};
let Ok(payload) = serde_json::to_string(&response) else {
return;
};
let mut stream = reader.into_inner();
let _ = stream.write_all(payload.as_bytes());
let _ = stream.write_all(b"\n");
}
fn run() -> Result<(), String> {
match parse_args() {
Ok(()) => {}
Err(err) if err.is_empty() => {
println!("{}", usage());
return Ok(());
}
Err(err) => return Err(err),
}
if Path::new(AUTH_SOCKET_PATH).exists() {
fs::remove_file(AUTH_SOCKET_PATH)
.map_err(|err| format!("failed to remove stale auth socket {AUTH_SOCKET_PATH}: {err}"))?;
}
let listener = UnixListener::bind(AUTH_SOCKET_PATH)
.map_err(|err| format!("failed to bind auth socket {AUTH_SOCKET_PATH}: {err}"))?;
fs::set_permissions(AUTH_SOCKET_PATH, fs::Permissions::from_mode(0o600))
.map_err(|err| format!("failed to set permissions on {AUTH_SOCKET_PATH}: {err}"))?;
let state = RuntimeState::default();
eprintln!("redbear-authd: listening on {AUTH_SOCKET_PATH}");
for stream in listener.incoming() {
match stream {
Ok(stream) => handle_connection(stream, state.clone()),
Err(err) => eprintln!("redbear-authd: failed to accept connection: {err}"),
}
}
Ok(())
}
fn main() {
if let Err(err) = run() {
eprintln!("redbear-authd: {err}");
eprintln!("{}", usage());
process::exit(1);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{BufRead, BufReader, Write};
fn send_handle_connection_request(request: &str) -> AuthResponse {
let state = RuntimeState::default();
let (mut client, server) = UnixStream::pair().expect("socket pair should open");
client
.write_all(request.as_bytes())
.and_then(|_| client.write_all(b"\n"))
.expect("request should write");
handle_connection(server, state);
let mut line = String::new();
BufReader::new(client)
.read_line(&mut line)
.expect("response should read");
serde_json::from_str(line.trim()).expect("response should parse")
}
#[test]
fn verify_password_accepts_plain_passwords() {
let account = Account {
username: String::from("root"),
password: String::from("password"),
uid: 0,
shell: String::from("/usr/bin/ion"),
};
assert!(verify_password(&account, "password"));
assert!(!verify_password(&account, "wrong"));
}
#[test]
fn verify_shadow_password_accepts_sha512_crypt() {
let hash = "$6$saltstring$adDbXsJjcDlq2662QPgd.tkSOVmnG9Tt3oXl4HR60SusC3AGjirnDenVZp3DGwLwqy6iYKCzannhaX9DR72nN1";
assert_eq!(verify_shadow_password("password", hash), Ok(true));
assert_eq!(verify_shadow_password("wrong", hash), Ok(false));
}
#[test]
fn verify_shadow_password_accepts_sha256_crypt() {
let hash = "$5$saltstring$OH4IDuTlsuTYPdED1gsuiRMyTAwNlRWyA6Xr3I4/dQ5";
assert_eq!(verify_shadow_password("password", hash), Ok(true));
assert_eq!(verify_shadow_password("wrong", hash), Ok(false));
}
#[test]
fn verify_shadow_password_rejects_unknown_hash_prefix() {
assert_eq!(verify_shadow_password("password", "$1$legacy$hash"), Err(VerifyError::UnsupportedHashFormat));
}
#[test]
fn verify_password_rejects_locked_accounts() {
let account = Account {
username: String::from("greeter"),
password: String::from("!"),
uid: 101,
shell: String::from("/usr/bin/ion"),
};
assert!(!verify_password(&account, "anything"));
}
#[test]
fn login_allowed_rejects_low_uid_non_root_accounts() {
let account = Account {
username: String::from("greeter"),
password: String::from("password"),
uid: 101,
shell: String::from("/usr/bin/ion"),
};
assert!(!login_allowed(&account));
}
#[test]
fn remember_failure_locks_after_five_attempts() {
let state = RuntimeState::default();
for _ in 0..4 {
let message = remember_failure(&state, "user").expect("failure tracking should succeed");
assert_eq!(message, "Invalid username or password.");
}
let message = remember_failure(&state, "user").expect("lockout tracking should succeed");
assert_eq!(message, "Too many failed attempts. Try again shortly.");
assert_eq!(
check_lockout(&state, "user").expect("lockout lookup should succeed"),
Some(String::from("Too many failed attempts. Try again shortly."))
);
}
#[test]
fn take_approval_rejects_vt_mismatch() {
let state = RuntimeState::default();
remember_success(&state, "user", 3).expect("approval should be recorded");
assert_eq!(
take_approval(&state, "user", 4),
Err(String::from("Authentication approval does not match the requested VT."))
);
}
#[test]
fn start_session_request_rejects_missing_approval() {
let state = RuntimeState::default();
let response = handle_request(
AuthRequest::StartSession {
request_id: 7,
username: String::from("user"),
session: String::from("kde-wayland"),
vt: 3,
},
&state,
);
match response {
AuthResponse::SessionResult {
request_id,
ok,
exit_code,
message,
} => {
assert_eq!(request_id, 7);
assert!(!ok);
assert_eq!(exit_code, None);
assert_eq!(message, "No recent authentication approval exists for this user.");
}
_ => panic!("expected session_result response"),
}
}
#[test]
fn authenticate_request_rejects_locked_account_marker() {
let account = Account {
username: String::from("greeter"),
password: String::from("!"),
uid: 101,
shell: String::from("/usr/bin/ion"),
};
assert!(!login_allowed(&account) || !verify_password(&account, "anything"));
}
#[test]
fn power_action_request_rejects_unsupported_action() {
let state = RuntimeState::default();
let response = handle_request(
AuthRequest::PowerAction {
request_id: 11,
action: String::from("hibernate"),
},
&state,
);
match response {
AuthResponse::PowerResult {
request_id,
ok,
message,
} => {
assert_eq!(request_id, 11);
assert!(!ok);
assert_eq!(message, "unsupported power action 'hibernate'");
}
_ => panic!("expected power_result response"),
}
}
#[test]
fn handle_connection_returns_error_for_invalid_json() {
match send_handle_connection_request("not-json") {
AuthResponse::Error { message } => {
assert!(message.contains("invalid request:"));
}
_ => panic!("expected error response"),
}
}
#[test]
fn split_account_fields_detects_redox_layout() {
let (format, parts) = split_account_fields("greeter;101;101;Greeter;/nonexistent;/usr/bin/ion");
assert_eq!(format, AccountFormat::Redox);
assert_eq!(parts[0], "greeter");
assert_eq!(parts[1], "101");
}
#[test]
fn split_account_fields_detects_unix_layout() {
let (format, parts) = split_account_fields("root:x:0:0:root:/root:/usr/bin/ion");
assert_eq!(format, AccountFormat::Unix);
assert_eq!(parts[2], "0");
}
#[test]
fn split_account_fields_keeps_empty_redox_shadow_hash() {
let (_format, parts) = split_account_fields("greeter;");
assert_eq!(parts, vec![String::from("greeter"), String::new()]);
}
}
@@ -0,0 +1,47 @@
[source]
path = "source"
[build]
template = "custom"
dependencies = ["qtbase", "qtdeclarative", "qtwayland"]
script = """
set -ex
DYNAMIC_INIT
for qtdir in plugins mkspecs metatypes modules; do
if [ -d "${COOKBOOK_SYSROOT}/usr/${qtdir}" ] && [ -d "${COOKBOOK_SYSROOT}/${qtdir}" ] && [ ! -L "${COOKBOOK_SYSROOT}/${qtdir}" ]; then
rm -rf "${COOKBOOK_SYSROOT}/${qtdir}"
fi
if [ -d "${COOKBOOK_SYSROOT}/usr/${qtdir}" ] && [ ! -e "${COOKBOOK_SYSROOT}/${qtdir}" ]; then
ln -s "usr/${qtdir}" "${COOKBOOK_SYSROOT}/${qtdir}"
fi
done
cookbook_cargo
rm -f CMakeCache.txt
rm -rf CMakeFiles
cmake "${COOKBOOK_SOURCE}/ui" \
-DCMAKE_TOOLCHAIN_FILE="${COOKBOOK_ROOT}/local/recipes/qt/redox-toolchain.cmake" \
-DCMAKE_INSTALL_PREFIX=/usr \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH="${COOKBOOK_SYSROOT}" \
-DQT_NO_PRIVATE_MODULE_WARNING=ON \
-Wno-dev
cmake --build . -j${COOKBOOK_MAKE_JOBS}
cmake --install . --prefix "${COOKBOOK_STAGE}/usr"
mkdir -pv "$COOKBOOK_STAGE/usr/bin"
mkdir -pv "$COOKBOOK_STAGE/usr/share/redbear/greeter"
cp -v "$COOKBOOK_SOURCE/redbear-greeter-compositor" "$COOKBOOK_STAGE/usr/share/redbear/greeter/redbear-greeter-compositor"
chmod 0755 "$COOKBOOK_STAGE/usr/share/redbear/greeter/redbear-greeter-compositor"
cp -v "$COOKBOOK_RECIPE/../../../../local/Assets/images/Red Bear OS loading background.png" "$COOKBOOK_STAGE/usr/share/redbear/greeter/background.png"
cp -v "$COOKBOOK_RECIPE/../../../../local/Assets/images/Red Bear OS icon.png" "$COOKBOOK_STAGE/usr/share/redbear/greeter/icon.png"
ln -svf ../share/redbear/greeter/redbear-greeter-compositor "$COOKBOOK_STAGE/usr/bin/redbear-greeter-compositor"
"""
[package.files]
"/usr/bin/redbear-greeterd" = "redbear-greeterd"
"/usr/bin/redbear-greeter-ui" = "redbear-greeter-ui"
@@ -0,0 +1,13 @@
[package]
name = "redbear-greeter"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "redbear-greeterd"
path = "src/main.rs"
[dependencies]
libc = "0.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
@@ -0,0 +1,24 @@
#!/usr/bin/env sh
export DISPLAY=""
export WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-wayland-0}"
export XDG_SESSION_TYPE=wayland
export LIBSEAT_BACKEND=seatd
export SEATD_SOCK=/run/seatd.sock
export QT_PLUGIN_PATH="${QT_PLUGIN_PATH:-/usr/plugins}"
export QT_QPA_PLATFORM_PLUGIN_PATH="${QT_QPA_PLATFORM_PLUGIN_PATH:-/usr/plugins/platforms}"
export QML2_IMPORT_PATH="${QML2_IMPORT_PATH:-/usr/qml}"
export XCURSOR_THEME="${XCURSOR_THEME:-Pop}"
export XKB_CONFIG_ROOT="${XKB_CONFIG_ROOT:-/usr/share/X11/xkb}"
if [ -z "${XDG_RUNTIME_DIR:-}" ]; then
export XDG_RUNTIME_DIR="/tmp/run/greeter"
fi
mkdir -p "$XDG_RUNTIME_DIR"
if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ] && command -v dbus-launch >/scheme/null 2>&1; then
eval "$(dbus-launch --sh-syntax)"
fi
exec kwin_wayland --replace
@@ -0,0 +1,699 @@
use std::{
env,
fs,
io::{self, BufRead, BufReader, Write},
os::unix::{fs::PermissionsExt, net::{UnixListener, UnixStream}},
path::{Path, PathBuf},
process::{self, Child, Command, ExitStatus},
thread,
time::{Duration, Instant},
};
use serde::{Deserialize, Serialize};
const GREETER_SOCKET_PATH: &str = "/run/redbear-greeterd.sock";
const AUTH_SOCKET_PATH: &str = "/run/redbear-authd.sock";
const BACKGROUND_PATH: &str = "/usr/share/redbear/greeter/background.png";
const ICON_PATH: &str = "/usr/share/redbear/greeter/icon.png";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum GreeterState {
Starting,
GreeterReady,
Authenticating,
LaunchingSession,
SessionRunning,
ReturningToGreeter,
PowerAction,
FatalError,
}
impl GreeterState {
fn as_str(self) -> &'static str {
match self {
GreeterState::Starting => "starting",
GreeterState::GreeterReady => "greeter_ready",
GreeterState::Authenticating => "authenticating",
GreeterState::LaunchingSession => "launching_session",
GreeterState::SessionRunning => "session_running",
GreeterState::ReturningToGreeter => "returning_to_greeter",
GreeterState::PowerAction => "power_action",
GreeterState::FatalError => "fatal_error",
}
}
}
#[derive(Debug)]
struct GreeterDaemon {
listener: UnixListener,
vt: u32,
greeter_user: String,
runtime_dir: PathBuf,
wayland_display: String,
state: GreeterState,
message: String,
compositor: Option<Child>,
ui: Option<Child>,
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 {
"Usage: redbear-greeterd [--help]"
}
fn parse_args() -> Result<(), String> {
let mut args = env::args().skip(1);
match args.next() {
None => Ok(()),
Some(arg) if arg == "--help" || arg == "-h" => Err(String::new()),
Some(arg) => Err(format!("unrecognized argument '{arg}'")),
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum AccountFormat {
Redox,
Unix,
}
fn split_account_fields(line: &str) -> (AccountFormat, Vec<&str>) {
let format = if line.contains(';') {
AccountFormat::Redox
} else {
AccountFormat::Unix
};
let delimiter = match format {
AccountFormat::Redox => ';',
AccountFormat::Unix => ':',
};
(format, line.split(delimiter).collect())
}
fn parse_uid_gid(parts: &[&str], format: AccountFormat) -> Option<(u32, u32)> {
let (uid_index, gid_index) = match format {
AccountFormat::Redox if parts.len() >= 3 => (1, 2),
AccountFormat::Unix if parts.len() >= 4 => (2, 3),
_ => return None,
};
let uid = parts[uid_index].parse::<u32>().ok()?;
let gid = parts[gid_index].parse::<u32>().ok()?;
Some((uid, gid))
}
fn load_uid_gid(username: &str) -> Result<(u32, u32), String> {
let passwd = fs::read_to_string("/etc/passwd").map_err(|err| format!("failed to read /etc/passwd: {err}"))?;
for line in passwd.lines() {
if line.trim().is_empty() || line.starts_with('#') {
continue;
}
let (format, parts) = split_account_fields(line);
if parts.len() < 3 || parts[0] != username {
continue;
}
if let Some((uid, gid)) = parse_uid_gid(&parts, format) {
return Ok((uid, gid));
}
return Err(format!("invalid uid/gid for user '{username}'"));
}
Err(format!("unknown greeter user '{username}'"))
}
fn change_socket_ownership(path: &Path, uid: u32, gid: u32) -> Result<(), String> {
let c_path = std::ffi::CString::new(path.as_os_str().as_encoded_bytes())
.map_err(|_| format!("socket path {} contains interior NUL", path.display()))?;
let result = unsafe { libc::chown(c_path.as_ptr(), uid, gid) };
if result == 0 {
Ok(())
} else {
Err(format!("failed to chown {}: {}", path.display(), io::Error::last_os_error()))
}
}
fn send_auth_request(request: &AuthRequest<'_>) -> Result<AuthResponse, String> {
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}"))?;
stream
.write_all(payload.as_bytes())
.and_then(|_| stream.write_all(b"\n"))
.map_err(|err| format!("failed to write auth request: {err}"))?;
let mut reader = BufReader::new(stream);
let mut line = String::new();
reader
.read_line(&mut line)
.map_err(|err| format!("failed to read auth response: {err}"))?;
serde_json::from_str(line.trim()).map_err(|err| format!("failed to parse auth response: {err}"))
}
impl GreeterDaemon {
fn hello_response(&self) -> GreeterResponse {
GreeterResponse::HelloOk {
background: String::from(BACKGROUND_PATH),
icon: String::from(ICON_PATH),
session_name: String::from("KDE on Wayland"),
state: String::from(self.state.as_str()),
message: self.message.clone(),
}
}
fn new() -> Result<Self, String> {
let vt = env::var("VT")
.ok()
.and_then(|value| value.parse::<u32>().ok())
.unwrap_or(3);
let greeter_user = env::var("REDBEAR_GREETER_USER").unwrap_or_else(|_| String::from("greeter"));
if Path::new(GREETER_SOCKET_PATH).exists() {
fs::remove_file(GREETER_SOCKET_PATH)
.map_err(|err| format!("failed to remove stale greeter socket: {err}"))?;
}
let listener = UnixListener::bind(GREETER_SOCKET_PATH)
.map_err(|err| format!("failed to bind {GREETER_SOCKET_PATH}: {err}"))?;
listener
.set_nonblocking(true)
.map_err(|err| format!("failed to set nonblocking socket mode: {err}"))?;
let (uid, gid) = load_uid_gid(&greeter_user)?;
fs::set_permissions(GREETER_SOCKET_PATH, fs::Permissions::from_mode(0o660))
.map_err(|err| format!("failed to chmod {GREETER_SOCKET_PATH}: {err}"))?;
change_socket_ownership(Path::new(GREETER_SOCKET_PATH), uid, gid)?;
Ok(Self {
listener,
vt,
greeter_user,
runtime_dir: PathBuf::from("/tmp/run/redbear-greeter"),
wayland_display: String::from("wayland-0"),
state: GreeterState::Starting,
message: String::from("Starting greeter"),
compositor: None,
ui: None,
restart_attempts: Vec::new(),
})
}
fn set_state(&mut self, state: GreeterState, message: impl Into<String>) {
self.state = state;
self.message = message.into();
}
fn configure_command(&self, command: &mut Command) {
command.env("QT_PLUGIN_PATH", "/usr/plugins");
command.env("QT_QPA_PLATFORM_PLUGIN_PATH", "/usr/plugins/platforms");
command.env("QML2_IMPORT_PATH", "/usr/qml");
command.env("XCURSOR_THEME", "Pop");
command.env("XKB_CONFIG_ROOT", "/usr/share/X11/xkb");
command.env("WAYLAND_DISPLAY", &self.wayland_display);
}
fn spawn_as_greeter(&self, program: &str) -> Result<Child, String> {
let mut command = Command::new("/usr/bin/redbear-session-launch");
command
.arg("--username")
.arg(&self.greeter_user)
.arg("--mode")
.arg("command")
.arg("--vt")
.arg(self.vt.to_string())
.arg("--runtime-dir")
.arg(&self.runtime_dir)
.arg("--wayland-display")
.arg(&self.wayland_display)
.arg("--command")
.arg(program);
self.configure_command(&mut command);
command
.spawn()
.map_err(|err| format!("failed to spawn {program} as {}: {err}", self.greeter_user))
}
fn wait_for_wayland_socket(&self) -> Result<(), String> {
let socket_path = self.runtime_dir.join(&self.wayland_display);
for _ in 0..60 {
if socket_path.exists() {
return Ok(());
}
thread::sleep(Duration::from_millis(250));
}
Err(format!("timed out waiting for compositor socket {}", socket_path.display()))
}
fn start_surface(&mut self) -> Result<(), String> {
self.set_state(GreeterState::Starting, "Starting greeter surface");
self.compositor = Some(self.spawn_as_greeter("/usr/bin/redbear-greeter-compositor")?);
self.wait_for_wayland_socket()?;
self.ui = Some(self.spawn_as_greeter("/usr/bin/redbear-greeter-ui")?);
self.set_state(GreeterState::GreeterReady, "Ready");
Ok(())
}
fn kill_child(child: &mut Option<Child>) {
if let Some(process) = child.as_mut() {
let _ = process.kill();
let _ = process.wait();
}
*child = None;
}
fn note_restart(&mut self) -> Result<(), String> {
let now = Instant::now();
self.restart_attempts
.retain(|attempt| now.saturating_duration_since(*attempt) <= Duration::from_secs(60));
self.restart_attempts.push(now);
if self.restart_attempts.len() > 3 {
self.set_state(GreeterState::FatalError, "Greeter restart limit reached");
return Err(String::from("greeter restart limit reached; leaving fallback consoles available"));
}
Ok(())
}
fn handle_surface_exit(&mut self, status: ExitStatus) -> Result<(), String> {
self.ui = None;
if status.success() {
self.message = String::from("Greeter UI exited");
} else {
self.message = format!("Greeter UI exited unexpectedly: {status}");
}
self.note_restart()?;
Self::kill_child(&mut self.compositor);
self.start_surface()
}
fn launch_session(&mut self, username: &str) -> Result<(), String> {
self.set_state(GreeterState::LaunchingSession, "Starting session");
Self::kill_child(&mut self.ui);
Self::kill_child(&mut self.compositor);
self.set_state(GreeterState::SessionRunning, "Session running");
let response = send_auth_request(&AuthRequest::StartSession {
request_id: 2,
username,
session: "kde-wayland",
vt: self.vt,
})?;
self.set_state(GreeterState::ReturningToGreeter, "Returning to greeter");
match response {
AuthResponse::SessionResult { ok, message, .. } => {
if !ok {
self.set_state(GreeterState::GreeterReady, message.clone());
}
self.message = message;
}
AuthResponse::Error { message } => self.message = message,
_ => self.message = String::from("Unexpected auth response while starting session"),
}
self.start_surface()
}
fn handle_connection(&mut self, stream: UnixStream) -> Result<(), String> {
let mut reader = BufReader::new(stream);
let mut line = String::new();
reader
.read_line(&mut line)
.map_err(|err| format!("failed to read greeter request: {err}"))?;
let request = serde_json::from_str::<GreeterRequest>(line.trim())
.map_err(|err| format!("invalid greeter request: {err}"))?;
let mut launch_username = None;
let response = match request {
GreeterRequest::Hello { version } => {
if version != 1 {
GreeterResponse::Error {
message: format!("unsupported greeter protocol version {version}"),
}
} else {
self.hello_response()
}
}
GreeterRequest::SubmitLogin { username, password } => {
self.set_state(GreeterState::Authenticating, "Authenticating");
match send_auth_request(&AuthRequest::Authenticate {
request_id: 1,
username: &username,
password: &password,
vt: self.vt,
})? {
AuthResponse::AuthenticateResult { ok, message, .. } => {
if ok {
self.set_state(GreeterState::LaunchingSession, "Starting session");
launch_username = Some(username);
} else {
self.set_state(GreeterState::GreeterReady, message.clone());
}
GreeterResponse::LoginResult {
ok,
state: String::from(self.state.as_str()),
message,
}
}
AuthResponse::Error { message } => {
self.set_state(GreeterState::GreeterReady, message.clone());
GreeterResponse::Error { message }
}
_ => GreeterResponse::Error {
message: String::from("unexpected auth response"),
},
}
}
GreeterRequest::RequestShutdown => {
self.set_state(GreeterState::PowerAction, "Requesting shutdown");
match send_auth_request(&AuthRequest::PowerAction {
request_id: 3,
action: "shutdown",
})? {
AuthResponse::PowerResult { ok, message, .. } => GreeterResponse::ActionResult { ok, message },
AuthResponse::Error { message } => GreeterResponse::Error { message },
_ => GreeterResponse::Error {
message: String::from("unexpected power-action response"),
},
}
}
GreeterRequest::RequestReboot => {
self.set_state(GreeterState::PowerAction, "Requesting reboot");
match send_auth_request(&AuthRequest::PowerAction {
request_id: 4,
action: "reboot",
})? {
AuthResponse::PowerResult { ok, message, .. } => GreeterResponse::ActionResult { ok, message },
AuthResponse::Error { message } => GreeterResponse::Error { message },
_ => GreeterResponse::Error {
message: String::from("unexpected power-action response"),
},
}
}
};
let payload = serde_json::to_string(&response)
.map_err(|err| format!("failed to serialize greeter response: {err}"))?;
let mut stream = reader.into_inner();
stream
.write_all(payload.as_bytes())
.and_then(|_| stream.write_all(b"\n"))
.map_err(|err| format!("failed to write greeter response: {err}"))?;
if let Some(username) = launch_username {
self.launch_session(&username)?;
}
Ok(())
}
fn check_children(&mut self) -> Result<(), String> {
if let Some(process) = self.compositor.as_mut() {
if let Some(status) = process.try_wait().map_err(|err| format!("failed to poll compositor: {err}"))? {
self.compositor = None;
self.note_restart()?;
self.message = format!("Greeter compositor exited unexpectedly: {status}");
Self::kill_child(&mut self.ui);
self.start_surface()?;
return Ok(());
}
}
if let Some(process) = self.ui.as_mut() {
if let Some(status) = process.try_wait().map_err(|err| format!("failed to poll greeter UI: {err}"))? {
return self.handle_surface_exit(status);
}
}
Ok(())
}
fn run(&mut self) -> Result<(), String> {
self.start_surface()?;
loop {
self.check_children()?;
match self.listener.accept() {
Ok((stream, _)) => {
if let Err(err) = self.handle_connection(stream) {
eprintln!("redbear-greeterd: {err}");
}
}
Err(err) if err.kind() == io::ErrorKind::WouldBlock => {
thread::sleep(Duration::from_millis(100));
}
Err(err) => return Err(format!("failed to accept greeter connection: {err}")),
}
}
}
}
fn run() -> Result<(), String> {
match parse_args() {
Ok(()) => {}
Err(err) if err.is_empty() => {
println!("{}", usage());
return Ok(());
}
Err(err) => return Err(err),
}
let mut daemon = GreeterDaemon::new()?;
daemon.run()
}
fn main() {
if let Err(err) = run() {
eprintln!("redbear-greeterd: {err}");
eprintln!("{}", usage());
process::exit(1);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{BufRead, BufReader, Write};
use std::sync::atomic::{AtomicU64, Ordering};
static TEST_SOCKET_COUNTER: AtomicU64 = AtomicU64::new(0);
fn test_daemon() -> GreeterDaemon {
let unique = TEST_SOCKET_COUNTER.fetch_add(1, Ordering::Relaxed);
let socket_path = std::env::temp_dir().join(format!(
"redbear-greeterd-test-{}-{}.sock",
process::id(),
unique
));
let _ = fs::remove_file(&socket_path);
let listener = UnixListener::bind(&socket_path).expect("test listener should bind");
listener
.set_nonblocking(true)
.expect("test listener should become nonblocking");
GreeterDaemon {
listener,
vt: 3,
greeter_user: String::from("greeter"),
runtime_dir: PathBuf::from("/tmp/run/redbear-greeter-test"),
wayland_display: String::from("wayland-0"),
state: GreeterState::Starting,
message: String::from("Starting greeter"),
compositor: None,
ui: None,
restart_attempts: Vec::new(),
}
}
fn send_daemon_request(daemon: &mut GreeterDaemon, request: &str) -> GreeterResponse {
let (mut client, server) = UnixStream::pair().expect("socket pair should open");
client
.write_all(request.as_bytes())
.and_then(|_| client.write_all(b"\n"))
.expect("request should write");
daemon.handle_connection(server).expect("handler should succeed");
let mut line = String::new();
BufReader::new(client)
.read_line(&mut line)
.expect("response should read");
serde_json::from_str(line.trim()).expect("response should parse")
}
#[test]
fn greeter_state_strings_match_protocol_contract() {
assert_eq!(GreeterState::Starting.as_str(), "starting");
assert_eq!(GreeterState::GreeterReady.as_str(), "greeter_ready");
assert_eq!(GreeterState::Authenticating.as_str(), "authenticating");
assert_eq!(GreeterState::LaunchingSession.as_str(), "launching_session");
assert_eq!(GreeterState::SessionRunning.as_str(), "session_running");
assert_eq!(GreeterState::ReturningToGreeter.as_str(), "returning_to_greeter");
assert_eq!(GreeterState::PowerAction.as_str(), "power_action");
assert_eq!(GreeterState::FatalError.as_str(), "fatal_error");
}
#[test]
fn hello_response_uses_installed_asset_paths() {
let mut daemon = test_daemon();
daemon.set_state(GreeterState::GreeterReady, "Ready");
match daemon.hello_response() {
GreeterResponse::HelloOk {
background,
icon,
session_name,
state,
message,
} => {
assert_eq!(background, BACKGROUND_PATH);
assert_eq!(icon, ICON_PATH);
assert_eq!(session_name, "KDE on Wayland");
assert_eq!(state, "greeter_ready");
assert_eq!(message, "Ready");
}
_ => panic!("expected hello_ok response"),
}
}
#[test]
fn note_restart_bounds_repeated_failures() {
let mut daemon = test_daemon();
for _ in 0..3 {
daemon.note_restart().expect("restart should remain bounded");
assert_ne!(daemon.state, GreeterState::FatalError);
}
let error = daemon.note_restart().expect_err("fourth restart should fail");
assert!(error.contains("restart limit"));
assert_eq!(daemon.state, GreeterState::FatalError);
assert_eq!(daemon.message, "Greeter restart limit reached");
}
#[test]
fn handle_connection_rejects_unsupported_protocol_version() {
let mut daemon = test_daemon();
match send_daemon_request(&mut daemon, r#"{"type":"hello","version":99}"#) {
GreeterResponse::Error { message } => {
assert_eq!(message, "unsupported greeter protocol version 99");
}
_ => panic!("expected error response"),
}
}
#[test]
fn handle_connection_rejects_invalid_json_request() {
let mut daemon = test_daemon();
let (mut client, server) = UnixStream::pair().expect("socket pair should open");
client
.write_all(b"not-json\n")
.expect("request should write");
let error = daemon
.handle_connection(server)
.expect_err("invalid request should fail");
assert!(error.contains("invalid greeter request"));
}
#[test]
fn parse_uid_gid_accepts_redox_style_layout() {
assert_eq!(
parse_uid_gid(
&["greeter", "101", "101", "Greeter", "/nonexistent", "/usr/bin/ion"],
AccountFormat::Redox,
),
Some((101, 101))
);
}
#[test]
fn parse_uid_gid_accepts_unix_style_layout() {
assert_eq!(
parse_uid_gid(
&["root", "x", "0", "0", "root", "/root", "/usr/bin/ion"],
AccountFormat::Unix,
),
Some((0, 0))
);
}
#[test]
fn split_account_fields_detects_redox_layout() {
let (format, parts) = split_account_fields("greeter;101;101;Greeter;/nonexistent;/usr/bin/ion");
assert_eq!(format, AccountFormat::Redox);
assert_eq!(parts[0], "greeter");
assert_eq!(parts[2], "101");
}
}
@@ -0,0 +1,29 @@
cmake_minimum_required(VERSION 3.20)
project(redbear-greeter-ui LANGUAGES CXX)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 REQUIRED COMPONENTS Core Gui Qml Quick QuickControls2)
qt_add_executable(redbear-greeter-ui
main.cpp
greeter_backend.cpp
greeter_backend.h
resources.qrc
)
target_compile_options(redbear-greeter-ui PRIVATE -fcf-protection=none)
target_link_options(redbear-greeter-ui PRIVATE -fcf-protection=none)
target_link_libraries(redbear-greeter-ui PRIVATE
Qt6::Core
Qt6::Gui
Qt6::Qml
Qt6::Quick
Qt6::QuickControls2
)
install(TARGETS redbear-greeter-ui RUNTIME DESTINATION bin)
@@ -0,0 +1,152 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
ApplicationWindow {
id: root
visible: true
visibility: Window.FullScreen
color: "#11090a"
title: "Red Bear Greeter"
function submitLogin() {
greeterBackend.submitLogin(usernameField.text, passwordField.text)
}
Rectangle {
anchors.fill: parent
color: "#11090a"
Image {
anchors.fill: parent
source: greeterBackend.backgroundUrl
fillMode: Image.PreserveAspectCrop
asynchronous: true
opacity: 0.88
}
Rectangle {
anchors.fill: parent
color: "#230a0d"
opacity: 0.45
}
}
Pane {
width: Math.min(parent.width * 0.42, 620)
anchors.centerIn: parent
padding: 28
background: Rectangle {
radius: 18
color: "#cc150c0f"
border.color: "#66f7d7d7"
border.width: 1
}
ColumnLayout {
anchors.fill: parent
spacing: 18
Item {
Layout.fillWidth: true
Layout.preferredHeight: 156
Image {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 2
source: greeterBackend.iconUrl
width: 108
height: 108
fillMode: Image.PreserveAspectFit
asynchronous: true
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
spacing: 4
Label {
anchors.horizontalCenter: parent.horizontalCenter
text: "Red Bear OS"
font.pixelSize: 26
font.bold: true
color: "#fff4f4"
}
Label {
anchors.horizontalCenter: parent.horizontalCenter
text: greeterBackend.sessionName
font.pixelSize: 15
color: "#f1c5c5"
}
}
}
TextField {
id: usernameField
Layout.fillWidth: true
placeholderText: "Username"
enabled: !greeterBackend.busy
selectByMouse: true
color: "#fff8f8"
font.pixelSize: 18
onAccepted: passwordField.forceActiveFocus()
}
TextField {
id: passwordField
Layout.fillWidth: true
placeholderText: "Password"
enabled: !greeterBackend.busy
selectByMouse: true
echoMode: TextInput.Password
color: "#fff8f8"
font.pixelSize: 18
onAccepted: root.submitLogin()
}
Label {
Layout.fillWidth: true
wrapMode: Text.Wrap
text: greeterBackend.message
color: greeterBackend.state === "fatal_error" ? "#ffb4b4" : "#ffe7e7"
font.pixelSize: 15
}
BusyIndicator {
Layout.alignment: Qt.AlignHCenter
running: greeterBackend.busy
visible: running
}
RowLayout {
Layout.fillWidth: true
spacing: 12
Button {
Layout.fillWidth: true
text: greeterBackend.busy ? "Working…" : "Log In"
enabled: !greeterBackend.busy
onClicked: root.submitLogin()
}
Button {
text: "Shutdown"
enabled: !greeterBackend.busy
onClicked: greeterBackend.requestShutdown()
}
Button {
text: "Reboot"
enabled: !greeterBackend.busy
onClicked: greeterBackend.requestReboot()
}
}
}
}
Component.onCompleted: usernameField.forceActiveFocus()
}
@@ -0,0 +1,296 @@
#include "greeter_backend.h"
#include <QByteArray>
#include <QCoreApplication>
#include <QJsonDocument>
#include <QJsonObject>
#include <QTimer>
#include <poll.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <cerrno>
#include <cstddef>
#include <cstring>
namespace {
constexpr auto kGreeterSocketPath = "/run/redbear-greeterd.sock";
constexpr auto kConnectTimeoutMs = 1500;
constexpr auto kReadTimeoutMs = 5000;
bool waitForReadable(int fd, int timeoutMs, QString *error) {
pollfd descriptor{};
descriptor.fd = fd;
descriptor.events = POLLIN;
const auto pollResult = ::poll(&descriptor, 1, timeoutMs);
if (pollResult > 0) {
return true;
}
if (pollResult == 0) {
*error = QStringLiteral("timed out waiting for greeter response");
return false;
}
*error = QStringLiteral("failed while waiting for greeter response: %1").arg(QString::fromLocal8Bit(std::strerror(errno)));
return false;
}
}
GreeterBackend::GreeterBackend(QObject *parent) : QObject(parent) {}
QUrl GreeterBackend::backgroundUrl() const {
return m_backgroundUrl;
}
QUrl GreeterBackend::iconUrl() const {
return m_iconUrl;
}
QString GreeterBackend::sessionName() const {
return m_sessionName;
}
QString GreeterBackend::state() const {
return m_state;
}
QString GreeterBackend::message() const {
return m_message;
}
bool GreeterBackend::busy() const {
return m_busy;
}
void GreeterBackend::initialize() {
const auto response = sendRequest(QJsonDocument(QJsonObject{{QStringLiteral("type"), QStringLiteral("hello")},
{QStringLiteral("version"), 1}})
.toJson(QJsonDocument::Compact));
if (!response.transportOk) {
applyError(response.transportError);
return;
}
if (response.type != QStringLiteral("hello_ok")) {
applyError(response.message.isEmpty() ? QStringLiteral("unexpected greeter hello response") : response.message);
return;
}
setGreeting(response.backgroundPath, response.iconPath, response.sessionName);
setStatus(response.state, response.message);
}
void GreeterBackend::submitLogin(const QString &username, const QString &password) {
if (m_busy) {
return;
}
if (username.trimmed().isEmpty() || password.isEmpty()) {
setStatus(QStringLiteral("greeter_ready"), QStringLiteral("Enter both username and password."));
return;
}
setBusy(true);
setStatus(QStringLiteral("authenticating"), QStringLiteral("Authenticating"));
const auto response = sendRequest(QJsonDocument(QJsonObject{{QStringLiteral("type"), QStringLiteral("submit_login")},
{QStringLiteral("username"), username},
{QStringLiteral("password"), password}})
.toJson(QJsonDocument::Compact));
setBusy(false);
if (!response.transportOk) {
applyError(response.transportError);
return;
}
if (response.type == QStringLiteral("login_result")) {
setStatus(response.state, response.message);
if (response.ok) {
QTimer::singleShot(0, qApp, &QCoreApplication::quit);
}
return;
}
applyError(response.message.isEmpty() ? QStringLiteral("unexpected login response") : response.message);
}
void GreeterBackend::requestShutdown() {
if (m_busy) {
return;
}
setBusy(true);
setStatus(QStringLiteral("power_action"), QStringLiteral("Requesting shutdown"));
const auto response = sendRequest(
QJsonDocument(QJsonObject{{QStringLiteral("type"), QStringLiteral("request_shutdown")}})
.toJson(QJsonDocument::Compact));
setBusy(false);
if (!response.transportOk) {
applyError(response.transportError);
return;
}
if (response.type == QStringLiteral("action_result")) {
setStatus(response.ok ? QStringLiteral("power_action") : QStringLiteral("greeter_ready"), response.message);
return;
}
applyError(response.message.isEmpty() ? QStringLiteral("unexpected shutdown response") : response.message);
}
void GreeterBackend::requestReboot() {
if (m_busy) {
return;
}
setBusy(true);
setStatus(QStringLiteral("power_action"), QStringLiteral("Requesting reboot"));
const auto response = sendRequest(
QJsonDocument(QJsonObject{{QStringLiteral("type"), QStringLiteral("request_reboot")}})
.toJson(QJsonDocument::Compact));
setBusy(false);
if (!response.transportOk) {
applyError(response.transportError);
return;
}
if (response.type == QStringLiteral("action_result")) {
setStatus(response.ok ? QStringLiteral("power_action") : QStringLiteral("greeter_ready"), response.message);
return;
}
applyError(response.message.isEmpty() ? QStringLiteral("unexpected reboot response") : response.message);
}
GreeterBackend::Response GreeterBackend::sendRequest(const QByteArray &payload) const {
Response response;
const int fd = ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (fd < 0) {
response.transportError = QStringLiteral("failed to create greeter socket: %1")
.arg(QString::fromLocal8Bit(std::strerror(errno)));
return response;
}
sockaddr_un address{};
address.sun_family = AF_UNIX;
std::strncpy(address.sun_path, kGreeterSocketPath, sizeof(address.sun_path) - 1);
const auto addressSize = static_cast<socklen_t>(offsetof(sockaddr_un, sun_path) + std::strlen(address.sun_path) + 1);
if (::connect(fd, reinterpret_cast<sockaddr *>(&address), addressSize) != 0) {
response.transportError = QStringLiteral("failed to connect to %1: %2")
.arg(QString::fromLatin1(kGreeterSocketPath),
QString::fromLocal8Bit(std::strerror(errno)));
::close(fd);
return response;
}
const auto fullPayload = payload + '\n';
qsizetype written = 0;
while (written < fullPayload.size()) {
const auto chunk = ::write(fd, fullPayload.constData() + written, static_cast<size_t>(fullPayload.size() - written));
if (chunk < 0) {
response.transportError = QStringLiteral("failed to write greeter request: %1")
.arg(QString::fromLocal8Bit(std::strerror(errno)));
::close(fd);
return response;
}
written += chunk;
}
QString waitError;
if (!waitForReadable(fd, kReadTimeoutMs, &waitError)) {
response.transportError = waitError;
::close(fd);
return response;
}
QByteArray reply;
char buffer[1024];
while (reply.indexOf('\n') < 0) {
const auto chunk = ::read(fd, buffer, sizeof(buffer));
if (chunk < 0) {
response.transportError = QStringLiteral("failed to read greeter response: %1")
.arg(QString::fromLocal8Bit(std::strerror(errno)));
::close(fd);
return response;
}
if (chunk == 0) {
break;
}
reply.append(buffer, static_cast<int>(chunk));
if (reply.indexOf('\n') < 0 && !waitForReadable(fd, kConnectTimeoutMs, &waitError)) {
response.transportError = waitError;
::close(fd);
return response;
}
}
::close(fd);
const auto newlineIndex = reply.indexOf('\n');
if (newlineIndex >= 0) {
reply.truncate(newlineIndex);
}
const auto document = QJsonDocument::fromJson(reply);
if (!document.isObject()) {
response.transportError = QStringLiteral("invalid greeter response payload");
return response;
}
const auto object = document.object();
response.transportOk = true;
response.type = object.value(QStringLiteral("type")).toString();
response.ok = object.value(QStringLiteral("ok")).toBool();
response.state = object.value(QStringLiteral("state")).toString();
response.message = object.value(QStringLiteral("message")).toString();
response.sessionName = object.value(QStringLiteral("session_name")).toString();
response.backgroundPath = object.value(QStringLiteral("background")).toString();
response.iconPath = object.value(QStringLiteral("icon")).toString();
if (response.type == QStringLiteral("error") && response.message.isEmpty()) {
response.message = QStringLiteral("greeter returned an unspecified error");
}
return response;
}
void GreeterBackend::setGreeting(const QString &backgroundPath, const QString &iconPath, const QString &sessionName) {
const auto nextBackground = backgroundPath.isEmpty() ? QUrl() : QUrl::fromLocalFile(backgroundPath);
const auto nextIcon = iconPath.isEmpty() ? QUrl() : QUrl::fromLocalFile(iconPath);
const auto nextSessionName = sessionName.isEmpty() ? QStringLiteral("KDE on Wayland") : sessionName;
if (m_backgroundUrl == nextBackground && m_iconUrl == nextIcon && m_sessionName == nextSessionName) {
return;
}
m_backgroundUrl = nextBackground;
m_iconUrl = nextIcon;
m_sessionName = nextSessionName;
emit greetingChanged();
}
void GreeterBackend::setStatus(const QString &state, const QString &message) {
const auto nextState = state.isEmpty() ? QStringLiteral("greeter_ready") : state;
if (m_state == nextState && m_message == message) {
return;
}
m_state = nextState;
m_message = message;
emit statusChanged();
}
void GreeterBackend::setBusy(bool busy) {
if (m_busy == busy) {
return;
}
m_busy = busy;
emit busyChanged();
}
void GreeterBackend::applyError(const QString &message) {
setStatus(QStringLiteral("fatal_error"), message);
}
@@ -0,0 +1,60 @@
#pragma once
#include <QObject>
#include <QUrl>
class GreeterBackend final : public QObject {
Q_OBJECT
Q_PROPERTY(QUrl backgroundUrl READ backgroundUrl NOTIFY greetingChanged)
Q_PROPERTY(QUrl iconUrl READ iconUrl NOTIFY greetingChanged)
Q_PROPERTY(QString sessionName READ sessionName NOTIFY greetingChanged)
Q_PROPERTY(QString state READ state NOTIFY statusChanged)
Q_PROPERTY(QString message READ message NOTIFY statusChanged)
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
public:
explicit GreeterBackend(QObject *parent = nullptr);
[[nodiscard]] QUrl backgroundUrl() const;
[[nodiscard]] QUrl iconUrl() const;
[[nodiscard]] QString sessionName() const;
[[nodiscard]] QString state() const;
[[nodiscard]] QString message() const;
[[nodiscard]] bool busy() const;
Q_INVOKABLE void initialize();
Q_INVOKABLE void submitLogin(const QString &username, const QString &password);
Q_INVOKABLE void requestShutdown();
Q_INVOKABLE void requestReboot();
signals:
void greetingChanged();
void statusChanged();
void busyChanged();
private:
struct Response {
bool transportOk = false;
QString transportError;
QString type;
bool ok = false;
QString state;
QString message;
QString sessionName;
QString backgroundPath;
QString iconPath;
};
[[nodiscard]] Response sendRequest(const QByteArray &payload) const;
void setGreeting(const QString &backgroundPath, const QString &iconPath, const QString &sessionName);
void setStatus(const QString &state, const QString &message);
void setBusy(bool busy);
void applyError(const QString &message);
QUrl m_backgroundUrl;
QUrl m_iconUrl;
QString m_sessionName = QStringLiteral("KDE on Wayland");
QString m_state = QStringLiteral("starting");
QString m_message = QStringLiteral("Connecting to greeter");
bool m_busy = false;
};
@@ -0,0 +1,25 @@
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QQuickStyle>
#include "greeter_backend.h"
int main(int argc, char *argv[]) {
qputenv("QT_QUICK_CONTROLS_STYLE", QByteArrayLiteral("Basic"));
QGuiApplication app(argc, argv);
QQuickStyle::setStyle(QStringLiteral("Basic"));
GreeterBackend backend;
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty(QStringLiteral("greeterBackend"), &backend);
engine.load(QUrl(QStringLiteral("qrc:/Main.qml")));
if (engine.rootObjects().isEmpty()) {
return 1;
}
backend.initialize();
return app.exec();
}
@@ -0,0 +1,5 @@
<RCC>
<qresource prefix="/">
<file>Main.qml</file>
</qresource>
</RCC>
@@ -10,6 +10,7 @@ template = "cargo"
"/usr/bin/redbear-usb-check" = "redbear-usb-check"
"/usr/bin/redbear-bluetooth-battery-check" = "redbear-bluetooth-battery-check"
"/usr/bin/redbear-drm-display-check" = "redbear-drm-display-check"
"/usr/bin/redbear-greeter-check" = "redbear-greeter-check"
"/usr/bin/redbear-phase4-wayland-check" = "redbear-phase4-wayland-check"
"/usr/bin/redbear-phase5-network-check" = "redbear-phase5-network-check"
"/usr/bin/redbear-phase5-wifi-check" = "redbear-phase5-wifi-check"
@@ -59,6 +59,10 @@ path = "src/bin/redbear-phase5-wifi-link-check.rs"
name = "redbear-phase6-kde-check"
path = "src/bin/redbear-phase6-kde-check.rs"
[[bin]]
name = "redbear-greeter-check"
path = "src/bin/redbear-greeter-check.rs"
[[bin]]
name = "redbear-drm-display-check"
path = "src/bin/redbear-drm-display-check.rs"
@@ -0,0 +1,350 @@
use std::{
fs,
io::{BufRead, BufReader, Write},
os::unix::net::UnixStream,
path::Path,
process,
thread,
time::{Duration, Instant},
};
use serde::{Deserialize, Serialize};
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 GREETER_SOCKET: &str = "/run/redbear-greeterd.sock";
const GREETERD_BIN: &str = "/usr/bin/redbear-greeterd";
const GREETER_UI_BIN: &str = "/usr/bin/redbear-greeter-ui";
const AUTHD_BIN: &str = "/usr/bin/redbear-authd";
const SESSION_LAUNCH_BIN: &str = "/usr/bin/redbear-session-launch";
const GREETER_BACKGROUND: &str = "/usr/share/redbear/greeter/background.png";
const GREETER_ICON: &str = "/usr/share/redbear/greeter/icon.png";
const AUTHD_SERVICE: &str = "/usr/lib/init.d/19_redbear-authd.service";
const DISPLAY_SHIM_SERVICE: &str = "/usr/lib/init.d/20_display.service";
const GREETER_SERVICE: &str = "/usr/lib/init.d/20_greeter.service";
const ACTIVATE_CONSOLE_SERVICE: &str = "/usr/lib/init.d/29_activate_console.service";
const CONSOLE_SERVICE: &str = "/usr/lib/init.d/30_console.service";
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,
Invalid { username: String, password: String },
Valid { username: String, password: String },
}
fn parse_mode_from_args<I>(args: I) -> Result<Mode, String>
where
I: IntoIterator<Item = String>,
{
let mut args = args.into_iter();
match args.next() {
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"));
}
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"));
}
Ok(Mode::Valid { username, password })
}
Some(other) => Err(format!("unsupported argument '{other}'")),
}
}
fn parse_mode() -> Result<Mode, String> {
parse_mode_from_args(std::env::args().skip(1))
}
fn send_request(request: &Request<'_>) -> Result<Response, String> {
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)
.map_err(|err| format!("failed to serialize greeter request: {err}"))?;
stream
.write_all(payload.as_bytes())
.and_then(|_| stream.write_all(b"\n"))
.map_err(|err| format!("failed to write greeter request: {err}"))?;
let mut reader = BufReader::new(stream);
let mut line = String::new();
reader
.read_line(&mut line)
.map_err(|err| format!("failed to read greeter response: {err}"))?;
serde_json::from_str(line.trim()).map_err(|err| format!("failed to parse greeter response: {err}"))
}
fn require_path(path: &str) -> Result<(), String> {
if Path::new(path).exists() {
println!("{path}");
Ok(())
} else {
Err(format!("missing {path}"))
}
}
fn wait_for_validation_marker(path: &str, timeout: Duration) -> Result<(), String> {
let start = Instant::now();
while start.elapsed() <= timeout {
if Path::new(path).exists() {
return Ok(());
}
thread::sleep(Duration::from_millis(250));
}
Err(format!("timed out waiting for {path}"))
}
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" => {
println!("GREETER_VALID_READY_MESSAGE={message}");
return Ok(());
}
Ok(_) => {}
Err(_) => {}
}
thread::sleep(Duration::from_millis(250));
}
Err(String::from("timed out waiting for greeter to return to greeter_ready"))
}
fn run_status() -> Result<(), String> {
println!("=== Red Bear Greeter Runtime Check ===");
require_path(GREETERD_BIN)?;
require_path(GREETER_UI_BIN)?;
require_path(AUTHD_BIN)?;
require_path(SESSION_LAUNCH_BIN)?;
require_path(GREETER_BACKGROUND)?;
require_path(GREETER_ICON)?;
require_path(AUTHD_SERVICE)?;
require_path(DISPLAY_SHIM_SERVICE)?;
require_path(GREETER_SERVICE)?;
require_path(ACTIVATE_CONSOLE_SERVICE)?;
require_path(CONSOLE_SERVICE)?;
require_path(DEBUG_CONSOLE_SERVICE)?;
require_path(GREETER_SOCKET)?;
match send_request(&Request::Hello { version: 1 })? {
Response::HelloOk {
background,
icon,
session_name,
state,
message,
} => {
println!("GREETER_BACKGROUND={background}");
println!("GREETER_ICON={icon}");
println!("GREETER_SESSION={session_name}");
println!("GREETER_STATE={state}");
println!("GREETER_MESSAGE={message}");
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")),
}
}
fn run_invalid(username: &str, password: &str) -> Result<(), String> {
match send_request(&Request::SubmitLogin { username, password })? {
Response::LoginResult { ok, state, message } => {
println!("GREETER_INVALID_STATE={state}");
println!("GREETER_INVALID_MESSAGE={message}");
if ok {
Err(String::from("invalid login unexpectedly succeeded"))
} else {
println!("GREETER_INVALID=ok");
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")),
}
}
fn run_valid(username: &str, password: &str) -> Result<(), String> {
let _ = fs::remove_file(VALIDATION_REQUEST);
let _ = fs::remove_file(VALIDATION_SUCCESS);
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 } => {
println!("GREETER_VALID_STATE={state}");
println!("GREETER_VALID_MESSAGE={message}");
if !ok {
let _ = fs::remove_file(VALIDATION_REQUEST);
return Err(String::from("valid login unexpectedly failed"));
}
}
Response::Error { message } => {
let _ = fs::remove_file(VALIDATION_REQUEST);
return Err(format!("valid-login request failed: {message}"));
}
Response::Other => {
let _ = fs::remove_file(VALIDATION_REQUEST);
return Err(String::from("unexpected greeter response for valid login"));
}
Response::HelloOk { .. } => {
let _ = fs::remove_file(VALIDATION_REQUEST);
return Err(String::from("unexpected hello response for valid login"));
}
}
wait_for_validation_marker(VALIDATION_SUCCESS, Duration::from_secs(30))?;
println!("GREETER_VALID_SESSION=started");
wait_for_greeter_ready(Duration::from_secs(30))?;
let _ = fs::remove_file(VALIDATION_REQUEST);
let _ = fs::remove_file(VALIDATION_SUCCESS);
println!("GREETER_VALID=ok");
Ok(())
}
fn main() {
let mode = match parse_mode() {
Ok(mode) => mode,
Err(err) if err.is_empty() => {
println!("{USAGE}");
process::exit(0);
}
Err(err) => {
eprintln!("{PROGRAM}: {err}");
eprintln!("{USAGE}");
process::exit(1);
}
};
let result = match mode {
Mode::Status => run_status(),
Mode::Invalid { username, password } => run_invalid(&username, &password),
Mode::Valid { username, password } => run_valid(&username, &password),
};
if let Err(err) = result {
eprintln!("{PROGRAM}: {err}");
process::exit(1);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_mode_defaults_to_status() {
assert_eq!(parse_mode_from_args(Vec::<String>::new()).expect("status mode should parse"), Mode::Status);
}
#[test]
fn parse_mode_accepts_invalid_login_arguments() {
assert_eq!(
parse_mode_from_args(vec![
String::from("--invalid"),
String::from("alice"),
String::from("wrong"),
])
.expect("invalid-login mode should parse"),
Mode::Invalid {
username: String::from("alice"),
password: String::from("wrong"),
}
);
}
#[test]
fn parse_mode_accepts_valid_login_arguments() {
assert_eq!(
parse_mode_from_args(vec![
String::from("--valid"),
String::from("alice"),
String::from("password"),
])
.expect("valid-login mode should parse"),
Mode::Valid {
username: String::from("alice"),
password: String::from("password"),
}
);
}
#[test]
fn parse_mode_rejects_extra_valid_arguments() {
assert_eq!(
parse_mode_from_args(vec![
String::from("--valid"),
String::from("alice"),
String::from("password"),
String::from("extra"),
]),
Err(String::from("unexpected extra arguments after --valid USER PASSWORD"))
);
}
#[test]
fn parse_mode_rejects_extra_invalid_arguments() {
assert_eq!(
parse_mode_from_args(vec![
String::from("--invalid"),
String::from("alice"),
String::from("wrong"),
String::from("extra"),
]),
Err(String::from("unexpected extra arguments after --invalid USER PASSWORD"))
);
}
#[test]
fn parse_mode_rejects_unknown_flags() {
assert_eq!(
parse_mode_from_args(vec![String::from("--bogus")]),
Err(String::from("unsupported argument '--bogus'"))
);
}
}
@@ -49,6 +49,9 @@ fn run() -> Result<(), String> {
if !stdout.contains("discovery_source=") {
return Err("iommu self-test did not report discovery source".to_string());
}
if !stdout.contains("dmar_present=") {
return Err("iommu self-test did not report DMAR presence state".to_string());
}
if !stdout.contains("units_initialized_now=") {
return Err("iommu self-test did not report initialized unit count".to_string());
}
@@ -1,6 +1,10 @@
use std::fs::OpenOptions;
use std::os::unix::fs::OpenOptionsExt;
use std::path::Path;
use std::process::{self, Command};
use syscall::O_NONBLOCK;
use redbear_hwutils::parse_args;
const PROGRAM: &str = "redbear-phase-ps2-check";
@@ -8,7 +12,14 @@ const USAGE: &str =
"Usage: redbear-phase-ps2-check\n\nRun the bounded PS/2 and serio proof check inside the guest.";
fn require_path(path: &str) -> Result<(), String> {
if Path::new(path).exists() {
if Path::new(path).exists()
|| OpenOptions::new()
.read(true)
.write(true)
.custom_flags(O_NONBLOCK as i32)
.open(path)
.is_ok()
{
println!("present={path}");
Ok(())
} else {
@@ -1,4 +1,4 @@
use std::path::Path;
use std::{env, path::{Path, PathBuf}};
use std::process::{self, Command};
use redbear_hwutils::parse_args;
@@ -51,6 +51,25 @@ fn require_wayland_smoke_marker() -> Result<(), String> {
Err("qt6-wayland-smoke did not leave a success marker".to_string())
}
fn require_wayland_socket() -> Result<(), String> {
let runtime_dir = env::var("XDG_RUNTIME_DIR")
.ok()
.filter(|value| !value.is_empty())
.unwrap_or_else(|| "/tmp/run/user/0".to_string());
let display = env::var("WAYLAND_DISPLAY")
.ok()
.filter(|value| !value.is_empty())
.unwrap_or_else(|| "wayland-0".to_string());
let socket = PathBuf::from(runtime_dir).join(display);
if socket.exists() {
println!("{}", socket.display());
Ok(())
} else {
Err(format!("missing Wayland socket {}", socket.display()))
}
}
fn run() -> Result<(), String> {
parse_args(PROGRAM, USAGE, std::env::args()).map_err(|err| {
if err.is_empty() {
@@ -66,6 +85,7 @@ fn run() -> Result<(), String> {
require_path("/usr/bin/qt6-plugin-check")?;
require_path("/usr/bin/qt6-wayland-smoke")?;
require_path("/home/root/.wayland-session.started")?;
require_wayland_socket()?;
require_wayland_smoke_marker()?;
let status = Command::new("redbear-info")
@@ -0,0 +1,8 @@
[source]
path = "source"
[build]
template = "cargo"
[package.files]
"/usr/bin/redbear-session-launch" = "redbear-session-launch"
@@ -0,0 +1,11 @@
[package]
name = "redbear-session-launch"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "redbear-session-launch"
path = "src/main.rs"
[dependencies]
libc = "0.2"
@@ -0,0 +1,536 @@
use std::{
collections::{BTreeMap, HashMap},
env,
ffi::CString,
fs,
io,
os::unix::process::CommandExt,
path::{Path, PathBuf},
process::{self, Command},
};
#[derive(Clone, Debug, PartialEq, Eq)]
struct Account {
username: String,
uid: u32,
gid: u32,
home: String,
shell: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct GroupEntry {
gid: u32,
members: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum LaunchMode {
Session,
Command { program: String, args: Vec<String> },
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct Args {
username: String,
vt: u32,
session: String,
runtime_dir: Option<PathBuf>,
wayland_display: String,
mode: LaunchMode,
}
fn usage() -> &'static str {
"Usage: redbear-session-launch --username USER [--mode session|command] [--session kde-wayland] [--vt N] [--runtime-dir PATH] [--wayland-display NAME] [--command PROGRAM [ARGS...]]"
}
fn parse_args_from<I>(args: I) -> Result<Args, String>
where
I: IntoIterator<Item = String>,
{
let mut args = args.into_iter();
let mut username = None;
let mut vt = 3_u32;
let mut session = String::from("kde-wayland");
let mut runtime_dir = None;
let mut wayland_display = String::from("wayland-0");
let mut mode = String::from("session");
let mut command = None;
while let Some(arg) = args.next() {
match arg.as_str() {
"--help" | "-h" => return Err(String::new()),
"--username" => username = Some(args.next().ok_or_else(|| String::from("missing value after --username"))?),
"--vt" => {
let value = args.next().ok_or_else(|| String::from("missing value after --vt"))?;
vt = value.parse().map_err(|_| format!("invalid VT '{value}'"))?;
}
"--session" => session = args.next().ok_or_else(|| String::from("missing value after --session"))?,
"--runtime-dir" => {
runtime_dir = Some(PathBuf::from(
args.next().ok_or_else(|| String::from("missing value after --runtime-dir"))?,
));
}
"--wayland-display" => {
wayland_display = args
.next()
.ok_or_else(|| String::from("missing value after --wayland-display"))?;
}
"--mode" => mode = args.next().ok_or_else(|| String::from("missing value after --mode"))?,
"--command" => {
let program = args.next().ok_or_else(|| String::from("missing program after --command"))?;
let rest = args.collect::<Vec<_>>();
command = Some((program, rest));
break;
}
other => return Err(format!("unrecognized argument '{other}'")),
}
}
let username = username.ok_or_else(|| String::from("--username is required"))?;
let mode = match mode.as_str() {
"session" => LaunchMode::Session,
"command" => {
let (program, args) = command.ok_or_else(|| String::from("--command is required when --mode=command"))?;
LaunchMode::Command { program, args }
}
other => return Err(format!("unsupported launch mode '{other}'")),
};
Ok(Args {
username,
vt,
session,
runtime_dir,
wayland_display,
mode,
})
}
fn parse_args() -> Result<Args, String> {
parse_args_from(env::args().skip(1))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum AccountFormat {
Redox,
Unix,
}
fn split_account_fields(line: &str) -> (AccountFormat, Vec<&str>) {
let format = if line.contains(';') {
AccountFormat::Redox
} else {
AccountFormat::Unix
};
let delimiter = match format {
AccountFormat::Redox => ';',
AccountFormat::Unix => ':',
};
(format, line.split(delimiter).collect::<Vec<_>>())
}
fn parse_passwd(contents: &str) -> Result<HashMap<String, Account>, String> {
let mut accounts = HashMap::new();
for (index, raw_line) in contents.lines().enumerate() {
let line = raw_line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let (format, parts) = split_account_fields(line);
let (uid_index, gid_index, home_index, shell_index) = match format {
AccountFormat::Redox if parts.len() >= 6 => (1, 2, 4, 5),
AccountFormat::Unix if parts.len() >= 7 => (2, 3, 5, 6),
AccountFormat::Redox => return Err(format!("invalid Redox passwd entry on line {}", index + 1)),
AccountFormat::Unix => return Err(format!("invalid passwd entry on line {}", index + 1)),
};
let uid = parts[uid_index]
.parse::<u32>()
.map_err(|_| format!("invalid uid on line {}", index + 1))?;
let gid = parts[gid_index]
.parse::<u32>()
.map_err(|_| format!("invalid gid on line {}", index + 1))?;
accounts.insert(
parts[0].to_string(),
Account {
username: parts[0].to_string(),
uid,
gid,
home: parts[home_index].to_string(),
shell: parts[shell_index].to_string(),
},
);
}
Ok(accounts)
}
fn parse_groups(contents: &str) -> Result<Vec<GroupEntry>, String> {
let mut groups = Vec::new();
for (index, raw_line) in contents.lines().enumerate() {
let line = raw_line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let (_format, parts) = split_account_fields(line);
if parts.len() < 4 {
return Err(format!("invalid group entry on line {}", index + 1));
}
let gid = parts[2]
.parse::<u32>()
.map_err(|_| format!("invalid group gid on line {}", index + 1))?;
let members = if parts[3].is_empty() {
Vec::new()
} else {
parts[3].split(',').map(str::to_string).collect::<Vec<_>>()
};
groups.push(GroupEntry { gid, members });
}
Ok(groups)
}
fn load_account(username: &str) -> Result<Account, String> {
let passwd = fs::read_to_string("/etc/passwd").map_err(|err| format!("failed to read /etc/passwd: {err}"))?;
let accounts = parse_passwd(&passwd)?;
accounts
.get(username)
.cloned()
.ok_or_else(|| format!("unknown user '{username}'"))
}
fn load_supplementary_groups(username: &str, primary_gid: u32) -> Result<Vec<u32>, String> {
let Ok(group_contents) = fs::read_to_string("/etc/group") else {
return Ok(vec![primary_gid]);
};
let mut groups = parse_groups(&group_contents)?
.into_iter()
.filter(|entry| entry.gid == primary_gid || entry.members.iter().any(|member| member == username))
.map(|entry| entry.gid)
.collect::<Vec<_>>();
groups.sort_unstable();
groups.dedup();
if groups.is_empty() {
groups.push(primary_gid);
}
Ok(groups)
}
fn default_runtime_dir(uid: u32) -> PathBuf {
if Path::new("/run/user").exists() {
PathBuf::from(format!("/run/user/{uid}"))
} else {
PathBuf::from(format!("/tmp/run/user/{uid}"))
}
}
fn ensure_runtime_dir(path: &Path, uid: u32, gid: u32) -> Result<(), String> {
fs::create_dir_all(path).map_err(|err| format!("failed to create runtime dir {}: {err}", path.display()))?;
let c_path = CString::new(path.as_os_str().as_encoded_bytes())
.map_err(|_| format!("runtime dir {} contains interior NUL", path.display()))?;
let result = unsafe { libc::chown(c_path.as_ptr(), uid, gid) };
if result != 0 {
return Err(format!("failed to chown runtime dir {}: {}", path.display(), io::Error::last_os_error()));
}
fs::set_permissions(path, std::os::unix::fs::PermissionsExt::from_mode(0o700))
.map_err(|err| format!("failed to set runtime dir permissions on {}: {err}", path.display()))
}
fn env_value(keys: &[&str]) -> Option<String> {
keys.iter().find_map(|key| env::var(key).ok())
}
fn build_environment(account: &Account, args: &Args, runtime_dir: &Path) -> BTreeMap<String, String> {
let mut values = BTreeMap::new();
values.insert(String::from("HOME"), account.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_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"));
values.insert(String::from("XDG_VTNR"), args.vt.to_string());
values.insert(String::from("LIBSEAT_BACKEND"), String::from("seatd"));
values.insert(String::from("SEATD_SOCK"), String::from("/run/seatd.sock"));
values.insert(String::from("DISPLAY"), String::new());
values.insert(String::from("XDG_SESSION_TYPE"), String::from("wayland"));
if let Some(theme) = env_value(&["XCURSOR_THEME"]) {
values.insert(String::from("XCURSOR_THEME"), theme);
}
if let Some(root) = env_value(&["XKB_CONFIG_ROOT"]) {
values.insert(String::from("XKB_CONFIG_ROOT"), root);
}
if let Some(path) = env_value(&["QT_PLUGIN_PATH"]) {
values.insert(String::from("QT_PLUGIN_PATH"), path);
}
if let Some(path) = env_value(&["QT_QPA_PLATFORM_PLUGIN_PATH"]) {
values.insert(String::from("QT_QPA_PLATFORM_PLUGIN_PATH"), path);
}
if let Some(path) = env_value(&["QML2_IMPORT_PATH"]) {
values.insert(String::from("QML2_IMPORT_PATH"), path);
}
match args.mode {
LaunchMode::Session => {
values.insert(String::from("XDG_CURRENT_DESKTOP"), String::from("KDE"));
values.insert(String::from("KDE_FULL_SESSION"), String::from("true"));
}
LaunchMode::Command { .. } => {}
}
values
}
#[cfg(not(target_os = "redox"))]
fn apply_groups(groups: &[u32]) -> io::Result<()> {
let raw_groups = groups.iter().map(|gid| *gid as libc::gid_t).collect::<Vec<_>>();
let result = unsafe { libc::setgroups(raw_groups.len(), raw_groups.as_ptr()) };
if result == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
#[cfg(target_os = "redox")]
fn apply_groups(_groups: &[u32]) -> io::Result<()> {
Ok(())
}
fn command_for(args: &Args) -> Result<(String, Vec<String>), String> {
match &args.mode {
LaunchMode::Session => {
if args.session != "kde-wayland" {
return Err(format!("unsupported session '{}'", args.session));
}
if Path::new("/usr/bin/dbus-run-session").exists() {
Ok((
String::from("/usr/bin/dbus-run-session"),
vec![String::from("--"), String::from("/usr/bin/redbear-kde-session")],
))
} else {
Ok((String::from("/usr/bin/redbear-kde-session"), Vec::new()))
}
}
LaunchMode::Command { program, args } => Ok((program.clone(), args.clone())),
}
}
fn run() -> Result<(), String> {
let args = match parse_args() {
Ok(parsed) => parsed,
Err(err) if err.is_empty() => {
println!("{}", usage());
return Ok(());
}
Err(err) => return Err(err),
};
let account = load_account(&args.username)?;
let groups = load_supplementary_groups(&account.username, account.gid)?;
let runtime_dir = args
.runtime_dir
.clone()
.unwrap_or_else(|| default_runtime_dir(account.uid));
ensure_runtime_dir(&runtime_dir, account.uid, account.gid)?;
let envs = build_environment(&account, &args, &runtime_dir);
let (program, program_args) = command_for(&args)?;
let group_clone = groups.clone();
let mut command = Command::new(&program);
command.args(&program_args);
command.env_clear();
command.envs(&envs);
command.uid(account.uid);
command.gid(account.gid);
unsafe {
command.pre_exec(move || apply_groups(&group_clone));
}
let error = command.exec();
Err(format!("failed to exec {program}: {error}"))
}
fn main() {
if let Err(err) = run() {
eprintln!("redbear-session-launch: {err}");
eprintln!("{}", usage());
process::exit(1);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_args_accepts_command_mode() {
let parsed = parse_args_from(vec![
String::from("--username"),
String::from("greeter"),
String::from("--mode"),
String::from("command"),
String::from("--vt"),
String::from("7"),
String::from("--runtime-dir"),
String::from("/tmp/greeter"),
String::from("--wayland-display"),
String::from("wayland-7"),
String::from("--command"),
String::from("/usr/bin/redbear-greeter-ui"),
String::from("--fullscreen"),
])
.expect("command mode should parse");
assert_eq!(parsed.username, "greeter");
assert_eq!(parsed.vt, 7);
assert_eq!(parsed.runtime_dir, Some(PathBuf::from("/tmp/greeter")));
assert_eq!(parsed.wayland_display, "wayland-7");
assert_eq!(
parsed.mode,
LaunchMode::Command {
program: String::from("/usr/bin/redbear-greeter-ui"),
args: vec![String::from("--fullscreen")],
}
);
}
#[test]
fn parse_args_requires_command_when_mode_is_command() {
assert_eq!(
parse_args_from(vec![
String::from("--username"),
String::from("greeter"),
String::from("--mode"),
String::from("command"),
]),
Err(String::from("--command is required when --mode=command"))
);
}
#[test]
fn parse_args_rejects_unknown_mode() {
assert_eq!(
parse_args_from(vec![
String::from("--username"),
String::from("user"),
String::from("--mode"),
String::from("bogus"),
]),
Err(String::from("unsupported launch mode 'bogus'"))
);
}
#[test]
fn parse_passwd_accepts_basic_entries() {
let accounts = parse_passwd("root:x:0:0:root:/root:/usr/bin/ion\nuser:x:1000:1000:User:/home/user:/usr/bin/ion\n")
.expect("passwd should parse");
assert_eq!(accounts["root"].uid, 0);
assert_eq!(accounts["user"].home, "/home/user");
}
#[test]
fn parse_passwd_accepts_redox_style_layout() {
let accounts = parse_passwd("greeter;101;101;Greeter;/nonexistent;/usr/bin/ion\n")
.expect("redox passwd layout should parse");
let greeter = accounts.get("greeter").expect("greeter entry should exist");
assert_eq!(greeter.uid, 101);
assert_eq!(greeter.gid, 101);
assert_eq!(greeter.home, "/nonexistent");
assert_eq!(greeter.shell, "/usr/bin/ion");
}
#[test]
fn parse_groups_collects_members() {
let groups = parse_groups("sudo:x:1:user,root\nusers:x:1000:user\n").expect("group should parse");
assert_eq!(groups[0].gid, 1);
assert_eq!(groups[0].members, vec![String::from("user"), String::from("root")]);
}
#[test]
fn parse_groups_accepts_redox_style_layout() {
let groups = parse_groups("greeter;x;101;greeter\n").expect("redox group should parse");
assert_eq!(groups[0].gid, 101);
assert_eq!(groups[0].members, vec![String::from("greeter")]);
}
#[test]
fn build_environment_sets_kde_session_values() {
let account = Account {
username: String::from("user"),
uid: 1000,
gid: 1000,
home: String::from("/home/user"),
shell: String::from("/usr/bin/ion"),
};
let args = Args {
username: String::from("user"),
vt: 3,
session: String::from("kde-wayland"),
runtime_dir: None,
wayland_display: String::from("wayland-0"),
mode: LaunchMode::Session,
};
let envs = build_environment(&account, &args, Path::new("/run/user/1000"));
assert_eq!(envs["XDG_CURRENT_DESKTOP"], "KDE");
assert_eq!(envs["KDE_FULL_SESSION"], "true");
assert_eq!(envs["XDG_VTNR"], "3");
}
#[test]
fn build_environment_omits_kde_session_values_for_command_mode() {
let account = Account {
username: String::from("greeter"),
uid: 101,
gid: 101,
home: String::from("/nonexistent"),
shell: String::from("/usr/bin/ion"),
};
let args = Args {
username: String::from("greeter"),
vt: 3,
session: String::from("kde-wayland"),
runtime_dir: None,
wayland_display: String::from("wayland-0"),
mode: LaunchMode::Command {
program: String::from("/usr/bin/redbear-greeter-ui"),
args: Vec::new(),
},
};
let envs = build_environment(&account, &args, Path::new("/tmp/run/greeter"));
assert!(!envs.contains_key("XDG_CURRENT_DESKTOP"));
assert!(!envs.contains_key("KDE_FULL_SESSION"));
assert_eq!(envs["XDG_SESSION_TYPE"], "wayland");
}
#[test]
fn command_for_rejects_unknown_session_name() {
let args = Args {
username: String::from("user"),
vt: 3,
session: String::from("plasma-x11"),
runtime_dir: None,
wayland_display: String::from("wayland-0"),
mode: LaunchMode::Session,
};
assert_eq!(
command_for(&args),
Err(String::from("unsupported session 'plasma-x11'"))
);
}
}
@@ -11,5 +11,6 @@ path = "src/main.rs"
zbus = { version = "5", default-features = false, features = ["tokio"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
libredox = "0.1"
redox-syscall = { package = "redox_syscall", version = "0.7" }
@@ -0,0 +1,176 @@
use std::{
fs,
io::{BufRead, BufReader},
os::unix::{fs::PermissionsExt, net::UnixListener},
path::Path,
};
use serde::Deserialize;
use crate::runtime_state::SharedRuntime;
pub const CONTROL_SOCKET_PATH: &str = "/run/redbear-sessiond-control.sock";
#[derive(Debug, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum ControlMessage {
SetSession {
username: String,
uid: u32,
vt: u32,
leader: u32,
state: String,
},
ResetSession {
vt: u32,
},
}
fn apply_message(runtime: &SharedRuntime, message: ControlMessage) {
let Ok(mut runtime) = runtime.write() else {
eprintln!("redbear-sessiond: runtime state is poisoned");
return;
};
match message {
ControlMessage::SetSession {
username,
uid,
vt,
leader,
state,
} => {
runtime.username = username;
runtime.uid = uid;
runtime.vt = vt;
runtime.leader = leader;
runtime.state = state;
runtime.active = true;
}
ControlMessage::ResetSession { vt } => {
runtime.username = String::from("root");
runtime.uid = 0;
runtime.vt = vt;
runtime.leader = std::process::id();
runtime.state = String::from("closing");
runtime.active = true;
}
}
}
pub fn start_control_socket(runtime: SharedRuntime) {
std::thread::spawn(move || {
if Path::new(CONTROL_SOCKET_PATH).exists() {
if let Err(err) = fs::remove_file(CONTROL_SOCKET_PATH) {
eprintln!("redbear-sessiond: failed to remove stale control socket: {err}");
return;
}
}
let listener = match UnixListener::bind(CONTROL_SOCKET_PATH) {
Ok(listener) => listener,
Err(err) => {
eprintln!("redbear-sessiond: failed to bind control socket: {err}");
return;
}
};
if let Err(err) = fs::set_permissions(CONTROL_SOCKET_PATH, fs::Permissions::from_mode(0o600)) {
eprintln!("redbear-sessiond: failed to chmod control socket: {err}");
}
for stream in listener.incoming() {
let Ok(stream) = stream else {
continue;
};
let mut reader = BufReader::new(stream);
let mut line = String::new();
if reader.read_line(&mut line).is_err() {
continue;
}
match serde_json::from_str::<ControlMessage>(line.trim()) {
Ok(message) => apply_message(&runtime, message),
Err(err) => eprintln!("redbear-sessiond: invalid control message: {err}"),
}
}
});
}
#[cfg(test)]
mod tests {
use super::*;
use crate::runtime_state::shared_runtime;
#[test]
fn set_session_message_updates_runtime_state() {
let runtime = shared_runtime();
apply_message(
&runtime,
ControlMessage::SetSession {
username: String::from("user"),
uid: 1000,
vt: 7,
leader: 4242,
state: String::from("active"),
},
);
let runtime = runtime.read().expect("runtime lock should remain healthy");
assert_eq!(runtime.username, "user");
assert_eq!(runtime.uid, 1000);
assert_eq!(runtime.vt, 7);
assert_eq!(runtime.leader, 4242);
assert_eq!(runtime.state, "active");
assert!(runtime.active);
}
#[test]
fn reset_session_message_restores_root_scaffold() {
let runtime = shared_runtime();
apply_message(
&runtime,
ControlMessage::SetSession {
username: String::from("user"),
uid: 1000,
vt: 7,
leader: 4242,
state: String::from("active"),
},
);
apply_message(&runtime, ControlMessage::ResetSession { vt: 3 });
let runtime = runtime.read().expect("runtime lock should remain healthy");
assert_eq!(runtime.username, "root");
assert_eq!(runtime.uid, 0);
assert_eq!(runtime.vt, 3);
assert_eq!(runtime.state, "closing");
assert!(runtime.active);
}
#[test]
fn control_message_json_matches_expected_shape() {
let message = serde_json::from_str::<ControlMessage>(
r#"{"type":"set_session","username":"user","uid":1000,"vt":3,"leader":99,"state":"online"}"#,
)
.expect("control message json should parse");
match message {
ControlMessage::SetSession {
username,
uid,
vt,
leader,
state,
} => {
assert_eq!(username, "user");
assert_eq!(uid, 1000);
assert_eq!(vt, 3);
assert_eq!(leader, 99);
assert_eq!(state, "online");
}
ControlMessage::ResetSession { .. } => panic!("expected set_session message"),
}
}
}
@@ -1,4 +1,12 @@
use std::{collections::HashMap, fs::File, io};
use std::{
collections::HashMap,
fs::{self, File, OpenOptions},
io,
path::{Path, PathBuf},
};
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
#[derive(Clone, Debug)]
pub struct DeviceMap {
@@ -28,13 +36,11 @@ impl DeviceMap {
return Some(path.clone());
}
match (major, minor) {
(13, minor) if minor >= 68 => Some(format!("/dev/input/event{}", minor - 64)),
_ => None,
}
self.find_dynamic_path(major, minor)
.or_else(|| self.fallback_path(major, minor))
}
pub fn open_device(&self, major: u32, minor: u32) -> io::Result<File> {
pub fn open_device(&self, major: u32, minor: u32) -> io::Result<(String, File)> {
let Some(path) = self.resolve(major, minor) else {
return Err(io::Error::new(
io::ErrorKind::NotFound,
@@ -42,6 +48,118 @@ impl DeviceMap {
));
};
File::open(path)
let file = OpenOptions::new()
.read(true)
.write(true)
.open(&path)
.or_else(|_| OpenOptions::new().read(true).open(&path))
.or_else(|_| OpenOptions::new().write(true).open(&path))?;
Ok((path, file))
}
fn fallback_path(&self, major: u32, minor: u32) -> Option<String> {
match (major, minor) {
(13, minor) if minor >= 64 => {
let path = format!("/dev/input/event{}", minor - 64);
Path::new(&path).exists().then_some(path)
}
(226, minor) => {
let path = format!("/scheme/drm/card{minor}");
Path::new(&path).exists().then_some(path)
}
_ => None,
}
}
fn find_dynamic_path(&self, major: u32, minor: u32) -> Option<String> {
for path in candidate_paths() {
if path_matches_device(&path, major, minor) {
return Some(path.to_string_lossy().into_owned());
}
}
None
}
}
fn candidate_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
paths.extend(read_dir_paths("/dev/input", |name| name.starts_with("event")));
paths.extend(read_dir_paths("/scheme/drm", |name| name.starts_with("card")));
for direct in ["/dev/fb0", "/scheme/null", "/scheme/zero", "/scheme/rand"] {
let path = PathBuf::from(direct);
if path.exists() {
paths.push(path);
}
}
paths
}
fn read_dir_paths(dir: &str, include: impl Fn(&str) -> bool) -> Vec<PathBuf> {
let mut paths = Vec::new();
let Ok(entries) = fs::read_dir(dir) else {
return paths;
};
for entry in entries.flatten() {
let path = entry.path();
let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
continue;
};
if include(name) {
paths.push(path);
}
}
paths.sort();
paths
}
#[cfg(unix)]
fn path_matches_device(path: &Path, major: u32, minor: u32) -> bool {
let Ok(metadata) = fs::metadata(path) else {
return false;
};
let rdev = metadata.rdev();
dev_major(rdev) == major && dev_minor(rdev) == minor
}
#[cfg(not(unix))]
fn path_matches_device(_path: &Path, _major: u32, _minor: u32) -> bool {
false
}
fn dev_major(device: u64) -> u32 {
(((device >> 31 >> 1) & 0xfffff000) | ((device >> 8) & 0x00000fff)) as u32
}
fn dev_minor(device: u64) -> u32 {
(((device >> 12) & 0xffffff00) | (device & 0x000000ff)) as u32
}
#[cfg(test)]
mod tests {
use super::{dev_major, dev_minor};
fn make_dev(major: u64, minor: u64) -> u64 {
((major & 0xfffff000) << 32)
| ((major & 0x00000fff) << 8)
| ((minor & 0xffffff00) << 12)
| (minor & 0x000000ff)
}
#[test]
fn splits_compound_dev_numbers() {
let device = make_dev(226, 3);
assert_eq!(dev_major(device), 226);
assert_eq!(dev_minor(device), 3);
let event = make_dev(13, 67);
assert_eq!(dev_major(event), 13);
assert_eq!(dev_minor(event), 67);
}
}
@@ -1,6 +1,8 @@
mod acpi_watcher;
mod control;
mod device_map;
mod manager;
mod runtime_state;
mod seat;
mod session;
@@ -15,6 +17,7 @@ use device_map::DeviceMap;
use manager::LoginManager;
use seat::LoginSeat;
use session::LoginSession;
use runtime_state::shared_runtime;
use tokio::runtime::Builder as RuntimeBuilder;
use zbus::{
Address,
@@ -26,7 +29,7 @@ const BUS_NAME: &str = "org.freedesktop.login1";
const MANAGER_PATH: &str = "/org/freedesktop/login1";
const SESSION_PATH: &str = "/org/freedesktop/login1/session/c1";
const SEAT_PATH: &str = "/org/freedesktop/login1/seat/seat0";
const USER_PATH: &str = "/org/freedesktop/login1/user/0";
const USER_PATH: &str = "/org/freedesktop/login1/user/current";
enum Command {
Run,
@@ -113,10 +116,11 @@ async fn run_daemon() -> Result<(), Box<dyn Error>> {
let session_path = parse_object_path(SESSION_PATH)?;
let seat_path = parse_object_path(SEAT_PATH)?;
let user_path = parse_object_path(USER_PATH)?;
let runtime = shared_runtime();
let session = LoginSession::new(seat_path.clone(), user_path, DeviceMap::new());
let seat = LoginSeat::new(session_path.clone());
let manager = LoginManager::new(session_path, seat_path);
let session = LoginSession::new(seat_path.clone(), user_path, DeviceMap::new(), runtime.clone());
let seat = LoginSeat::new(session_path.clone(), runtime.clone());
let manager = LoginManager::new(session_path, seat_path, runtime.clone());
match system_connection_builder()?
.name(BUS_NAME)?
@@ -128,6 +132,7 @@ async fn run_daemon() -> Result<(), Box<dyn Error>> {
{
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()));
wait_for_shutdown().await?;
drop(connection);
@@ -5,20 +5,20 @@ use zbus::{
zvariant::OwnedObjectPath,
};
use crate::runtime_state::SharedRuntime;
#[derive(Clone, Debug)]
pub struct LoginManager {
session_id: String,
runtime: SharedRuntime,
session_path: OwnedObjectPath,
seat_id: String,
seat_path: OwnedObjectPath,
}
impl LoginManager {
pub fn new(session_path: OwnedObjectPath, seat_path: OwnedObjectPath) -> Self {
pub fn new(session_path: OwnedObjectPath, seat_path: OwnedObjectPath, runtime: SharedRuntime) -> Self {
Self {
session_id: String::from("c1"),
runtime,
session_path,
seat_id: String::from("seat0"),
seat_path,
}
}
@@ -27,7 +27,11 @@ impl LoginManager {
#[interface(name = "org.freedesktop.login1.Manager")]
impl LoginManager {
fn get_session(&self, id: &str) -> fdo::Result<OwnedObjectPath> {
if id == self.session_id {
let runtime = self
.runtime
.read()
.map_err(|_| fdo::Error::Failed(String::from("login1 runtime state is poisoned")))?;
if id == runtime.session_id {
return Ok(self.session_path.clone());
}
@@ -35,17 +39,25 @@ impl LoginManager {
}
fn list_sessions(&self) -> fdo::Result<Vec<(String, u32, String, String, OwnedObjectPath)>> {
let runtime = self
.runtime
.read()
.map_err(|_| fdo::Error::Failed(String::from("login1 runtime state is poisoned")))?;
Ok(vec![(
self.session_id.clone(),
0,
String::from("root"),
self.seat_id.clone(),
runtime.session_id.clone(),
runtime.uid,
runtime.username.clone(),
runtime.seat_id.clone(),
self.session_path.clone(),
)])
}
fn get_seat(&self, id: &str) -> fdo::Result<OwnedObjectPath> {
if id == self.seat_id {
let runtime = self
.runtime
.read()
.map_err(|_| fdo::Error::Failed(String::from("login1 runtime state is poisoned")))?;
if id == runtime.seat_id {
return Ok(self.seat_path.clone());
}
@@ -0,0 +1,34 @@
use std::sync::{Arc, RwLock};
#[derive(Clone, Debug)]
pub struct SessionRuntime {
pub session_id: String,
pub seat_id: String,
pub username: String,
pub uid: u32,
pub vt: u32,
pub leader: u32,
pub state: String,
pub active: bool,
}
impl Default for SessionRuntime {
fn default() -> Self {
Self {
session_id: String::from("c1"),
seat_id: String::from("seat0"),
username: String::from("root"),
uid: 0,
vt: 3,
leader: std::process::id(),
state: String::from("online"),
active: true,
}
}
}
pub type SharedRuntime = Arc<RwLock<SessionRuntime>>;
pub fn shared_runtime() -> SharedRuntime {
Arc::new(RwLock::new(SessionRuntime::default()))
}
@@ -2,20 +2,22 @@ use std::sync::Mutex;
use zbus::{fdo, interface, zvariant::OwnedObjectPath};
use crate::runtime_state::SharedRuntime;
#[derive(Debug)]
pub struct LoginSeat {
id: String,
session_id: String,
session_path: OwnedObjectPath,
runtime: SharedRuntime,
last_requested_vt: Mutex<u32>,
}
impl LoginSeat {
pub fn new(session_path: OwnedObjectPath) -> Self {
pub fn new(session_path: OwnedObjectPath, runtime: SharedRuntime) -> Self {
Self {
id: String::from("seat0"),
session_id: String::from("c1"),
session_path,
runtime,
last_requested_vt: Mutex::new(1),
}
}
@@ -46,12 +48,24 @@ impl LoginSeat {
#[zbus(property(emits_changed_signal = "const"), name = "ActiveSession")]
fn active_session(&self) -> (String, OwnedObjectPath) {
(self.session_id.clone(), 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")]
fn sessions(&self) -> Vec<(String, OwnedObjectPath)> {
vec![(self.session_id.clone(), self.session_path.clone())]
vec![(
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")]
@@ -13,16 +13,14 @@ use zbus::{
};
use crate::device_map::DeviceMap;
use crate::runtime_state::SharedRuntime;
#[derive(Debug)]
pub struct LoginSession {
id: String,
seat_id: String,
seat_path: OwnedObjectPath,
user_uid: u32,
user_path: OwnedObjectPath,
leader: u32,
device_map: DeviceMap,
runtime: SharedRuntime,
controlled: Mutex<bool>,
taken_devices: Mutex<HashSet<(u32, u32)>>,
}
@@ -32,15 +30,13 @@ impl LoginSession {
seat_path: OwnedObjectPath,
user_path: OwnedObjectPath,
device_map: DeviceMap,
runtime: SharedRuntime,
) -> Self {
Self {
id: String::from("c1"),
seat_id: String::from("seat0"),
seat_path,
user_uid: 0,
user_path,
leader: process::id(),
device_map,
runtime,
controlled: Mutex::new(false),
taken_devices: Mutex::new(HashSet::new()),
}
@@ -57,21 +53,35 @@ impl LoginSession {
.lock()
.map_err(|_| fdo::Error::Failed(String::from("login1 device state is poisoned")))
}
fn runtime(&self) -> fdo::Result<crate::runtime_state::SessionRuntime> {
self.runtime
.read()
.map(|runtime| runtime.clone())
.map_err(|_| fdo::Error::Failed(String::from("login1 runtime state is poisoned")))
}
}
#[interface(name = "org.freedesktop.login1.Session")]
impl LoginSession {
fn activate(&self) -> fdo::Result<()> {
eprintln!("redbear-sessiond: Activate requested for session {}", self.id);
eprintln!("redbear-sessiond: Activate requested for session {}", self.runtime()?.session_id);
Ok(())
}
fn take_control(&self, force: bool) -> fdo::Result<()> {
let mut controlled = self.control_state()?;
let runtime = self.runtime()?;
if *controlled && !force {
return Err(fdo::Error::Failed(format!(
"session {} is already under control",
runtime.session_id
)));
}
*controlled = true;
eprintln!(
"redbear-sessiond: TakeControl requested for session {} (force={force})",
self.id
runtime.session_id
);
Ok(())
}
@@ -79,34 +89,56 @@ impl LoginSession {
fn release_control(&self) -> fdo::Result<()> {
let mut controlled = self.control_state()?;
*controlled = false;
eprintln!("redbear-sessiond: ReleaseControl requested for session {}", self.id);
self.taken_devices()?.clear();
eprintln!("redbear-sessiond: ReleaseControl requested for session {}", self.runtime()?.session_id);
Ok(())
}
fn take_device(&self, major: u32, minor: u32) -> fdo::Result<OwnedFd> {
let file = self
let runtime = self.runtime()?;
if !*self.control_state()? {
return Err(fdo::Error::AccessDenied(format!(
"session {} must TakeControl before TakeDevice",
runtime.session_id
)));
}
let mut taken_devices = self.taken_devices()?;
if taken_devices.contains(&(major, minor)) {
return Err(fdo::Error::Failed(format!(
"device ({major}, {minor}) is already taken for session {}",
runtime.session_id
)));
}
let (path, file) = self
.device_map
.open_device(major, minor)
.map_err(|err| fdo::Error::Failed(format!("TakeDevice({major}, {minor}) failed: {err}")))?;
let mut taken_devices = self.taken_devices()?;
taken_devices.insert((major, minor));
let owned_fd: StdOwnedFd = file.into();
eprintln!(
"redbear-sessiond: TakeDevice granted for session {} -> ({major}, {minor})",
self.id
"redbear-sessiond: TakeDevice granted for session {} -> ({major}, {minor}) at {}",
runtime.session_id, path
);
Ok(OwnedFd::from(owned_fd))
}
fn release_device(&self, major: u32, minor: u32) -> fdo::Result<()> {
let runtime = self.runtime()?;
let mut taken_devices = self.taken_devices()?;
taken_devices.remove(&(major, minor));
if !taken_devices.remove(&(major, minor)) {
return Err(fdo::Error::Failed(format!(
"device ({major}, {minor}) was not taken for session {}",
runtime.session_id
)));
}
eprintln!(
"redbear-sessiond: ReleaseDevice requested for session {} -> ({major}, {minor})",
self.id
runtime.session_id
);
Ok(())
}
@@ -114,14 +146,14 @@ impl LoginSession {
fn pause_device_complete(&self, major: u32, minor: u32) -> fdo::Result<()> {
eprintln!(
"redbear-sessiond: PauseDeviceComplete received for session {} -> ({major}, {minor})",
self.id
self.runtime()?.session_id
);
Ok(())
}
#[zbus(property(emits_changed_signal = "const"), name = "Active")]
fn active(&self) -> bool {
true
self.runtime().map(|runtime| runtime.active).unwrap_or(true)
}
#[zbus(property(emits_changed_signal = "const"), name = "Remote")]
@@ -156,32 +188,40 @@ impl LoginSession {
#[zbus(property(emits_changed_signal = "const"), name = "Id")]
fn id(&self) -> String {
self.id.clone()
self.runtime().map(|runtime| runtime.session_id).unwrap_or_else(|_| String::from("c1"))
}
#[zbus(property(emits_changed_signal = "const"), name = "State")]
fn state(&self) -> String {
String::from("online")
self.runtime().map(|runtime| runtime.state).unwrap_or_else(|_| String::from("online"))
}
#[zbus(property(emits_changed_signal = "const"), name = "Seat")]
fn seat(&self) -> (String, OwnedObjectPath) {
(self.seat_id.clone(), self.seat_path.clone())
(
self.runtime()
.map(|runtime| runtime.seat_id)
.unwrap_or_else(|_| String::from("seat0")),
self.seat_path.clone(),
)
}
#[zbus(property(emits_changed_signal = "const"), name = "User")]
fn user(&self) -> (u32, OwnedObjectPath) {
(self.user_uid, self.user_path.clone())
(
self.runtime().map(|runtime| runtime.uid).unwrap_or(0),
self.user_path.clone(),
)
}
#[zbus(property(emits_changed_signal = "const"), name = "VTNr")]
fn vt_nr(&self) -> u32 {
1
self.runtime().map(|runtime| runtime.vt).unwrap_or(3)
}
#[zbus(property(emits_changed_signal = "const"), name = "Leader")]
fn leader(&self) -> u32 {
self.leader
self.runtime().map(|runtime| runtime.leader).unwrap_or(process::id())
}
#[zbus(property(emits_changed_signal = "const"), name = "Audit")]
@@ -191,7 +231,7 @@ impl LoginSession {
#[zbus(property(emits_changed_signal = "const"), name = "TTY")]
fn tty(&self) -> String {
String::new()
format!("tty{}", self.runtime().map(|runtime| runtime.vt).unwrap_or(3))
}
#[zbus(property(emits_changed_signal = "const"), name = "RemoteUser")]
@@ -13,6 +13,7 @@ libredox = { version = "0.1", features = ["call", "std"] }
log = { version = "0.4", features = ["std"] }
redox-scheme = "0.11"
syscall = { package = "redox_syscall", version = "0.7", features = ["std"] }
redox-driver-sys = { path = "../../../drivers/redox-driver-sys/source" }
[target.'cfg(target_os = "redox")'.dependencies]
redox-driver-sys = { path = "../../../drivers/redox-driver-sys/source", features = ["redox"] }
@@ -10,10 +10,9 @@ use std::process::Command;
pub(crate) static TEST_ENV_LOCK: std::sync::LazyLock<std::sync::Mutex<()>> =
std::sync::LazyLock::new(|| std::sync::Mutex::new(()));
use redox_driver_sys::pci::{parse_device_info_from_config_space, PciLocation};
#[cfg(target_os = "redox")]
use redox_driver_sys::pci::PciDevice;
#[cfg(target_os = "redox")]
use redox_driver_sys::pci::PciLocation;
#[derive(Clone, Debug)]
struct ParsedPciLocation {
@@ -862,7 +861,7 @@ fn detect_intel_wifi_interfaces(
device_id,
subsystem_id,
firmware_family,
transport_status: transport_status_from_config(&config),
transport_status: transport_status_from_config(&location, &config),
ucode_candidates,
selected_ucode,
pnvm_candidate,
@@ -945,7 +944,7 @@ fn intel_firmware_candidates(
)
}
fn transport_status_from_config(config: &[u8]) -> String {
fn transport_status_from_config(location: &ParsedPciLocation, config: &[u8]) -> String {
let command = u16::from_le_bytes([config[0x04], config[0x05]]);
let bar0 = u32::from_le_bytes([config[0x10], config[0x11], config[0x12], config[0x13]]);
let irq_pin = config[0x3D];
@@ -954,13 +953,38 @@ fn transport_status_from_config(config: &[u8]) -> String {
let bus_master = (command & 0x4) != 0;
let bar_present = bar0 != 0;
let irq_present = irq_pin != 0;
let interrupt_support = parse_device_info_from_config_space(
PciLocation {
segment: location.segment,
bus: location.bus,
device: location.device,
function: location.function,
},
config,
)
.map(|info| {
let support = info.interrupt_support();
if support.as_str() == "none" && irq_present {
"legacy".to_string()
} else {
support.as_str().to_string()
}
})
.unwrap_or_else(|| {
if irq_present {
"legacy".to_string()
} else {
"none".to_string()
}
});
format!(
"transport=pci memory_enabled={} bus_master={} bar0_present={} irq_pin_present={}",
"transport=pci memory_enabled={} bus_master={} bar0_present={} irq_pin_present={} interrupt_support={}",
if memory_enabled { "yes" } else { "no" },
if bus_master { "yes" } else { "no" },
if bar_present { "yes" } else { "no" },
if irq_present { "yes" } else { "no" }
if irq_present { "yes" } else { "no" },
interrupt_support
)
}
@@ -1174,7 +1198,8 @@ fn read_transport_status(config_path: &PathBuf) -> Result<String, String> {
config_path.display()
));
}
Ok(transport_status_from_config(&config))
let location = parse_location_from_config_path(config_path)?;
Ok(transport_status_from_config(&location, &config))
}
fn program_transport_bits(config_path: &PathBuf) -> Result<(), String> {
@@ -1286,6 +1311,38 @@ mod tests {
.transport_status("wlan0")
.contains("memory_enabled=yes"));
assert!(backend.transport_status("wlan0").contains("bus_master=yes"));
assert!(backend
.transport_status("wlan0")
.contains("interrupt_support=legacy"));
}
#[test]
fn transport_status_reports_interrupt_support_from_shared_pci_parser() {
let location = ParsedPciLocation {
segment: 0,
bus: 0,
device: 0x14,
function: 3,
};
let mut cfg = vec![0u8; 256];
cfg[0x00] = 0x86;
cfg[0x01] = 0x80;
cfg[0x02] = 0x25;
cfg[0x03] = 0x27;
cfg[0x04] = 0x06;
cfg[0x06] = 0x10;
cfg[0x0A] = 0x80;
cfg[0x0B] = 0x02;
cfg[0x0E] = 0x00;
cfg[0x34] = 0x50;
cfg[0x3C] = 11;
cfg[0x50] = 0x05;
cfg[0x51] = 0x00;
let status = transport_status_from_config(&location, &cfg);
assert!(status.contains("memory_enabled=yes"));
assert!(status.contains("bus_master=yes"));
assert!(status.contains("interrupt_support=msi"));
}
#[test]