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:
@@ -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 {
|
||||
|
||||
+21
-1
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user