moonlock/src/power.rs
nevaforget 817a9547ad Rewrite moonlock from Python to Rust (v0.4.0)
Complete rewrite of the Wayland lockscreen from Python/PyGObject to
Rust/gtk4-rs for memory safety in security-critical PAM code and
consistency with the moonset/moongreet Rust ecosystem.

Modules: main, lockscreen, auth (PAM FFI), fingerprint (fprintd D-Bus),
config, i18n, users, power. 37 unit tests.

Security: PAM conversation callback with Zeroizing password, panic hook
that never unlocks, root check, ext-session-lock-v1 compositor policy,
absolute loginctl path, avatar symlink rejection.
2026-03-27 23:09:54 +01:00

53 lines
2.1 KiB
Rust

// ABOUTME: Power actions — reboot and shutdown via loginctl.
// ABOUTME: Wrappers around system commands for the lockscreen UI.
use std::fmt;
use std::process::Command;
#[derive(Debug)]
pub enum PowerError {
CommandFailed { action: &'static str, message: String },
Timeout { action: &'static str },
}
impl fmt::Display for PowerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PowerError::CommandFailed { action, message } => write!(f, "{action} failed: {message}"),
PowerError::Timeout { action } => write!(f, "{action} timed out"),
}
}
}
impl std::error::Error for PowerError {}
fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(), PowerError> {
let child = Command::new(program)
.args(args)
.spawn()
.map_err(|e| PowerError::CommandFailed { action, message: e.to_string() })?;
let output = child.wait_with_output()
.map_err(|e| PowerError::CommandFailed { action, message: e.to_string() })?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(PowerError::CommandFailed {
action, message: format!("exit code {}: {}", output.status, stderr.trim()),
});
}
Ok(())
}
pub fn reboot() -> Result<(), PowerError> { run_command("reboot", "/usr/bin/loginctl", &["reboot"]) }
pub fn shutdown() -> Result<(), PowerError> { run_command("shutdown", "/usr/bin/loginctl", &["poweroff"]) }
#[cfg(test)]
mod tests {
use super::*;
#[test] fn power_error_display() { assert_eq!(PowerError::CommandFailed { action: "reboot", message: "fail".into() }.to_string(), "reboot failed: fail"); }
#[test] fn timeout_display() { assert_eq!(PowerError::Timeout { action: "shutdown" }.to_string(), "shutdown timed out"); }
#[test] fn missing_binary() { assert!(run_command("test", "nonexistent-xyz", &[]).is_err()); }
#[test] fn nonzero_exit() { assert!(run_command("test", "false", &[]).is_err()); }
#[test] fn success() { assert!(run_command("test", "true", &[]).is_ok()); }
}