| 1 | //! Power management via systemd-logind |
| 2 | //! |
| 3 | //! Implements shutdown, reboot, and suspend via D-Bus calls to logind. |
| 4 | |
| 5 | use anyhow::{Context, Result}; |
| 6 | use zbus::blocking::Connection; |
| 7 | |
| 8 | /// Power action to perform |
| 9 | #[derive(Debug, Clone, Copy)] |
| 10 | pub enum PowerAction { |
| 11 | Shutdown, |
| 12 | Reboot, |
| 13 | Suspend, |
| 14 | } |
| 15 | |
| 16 | impl PowerAction { |
| 17 | /// Get the logind method name for this action |
| 18 | fn method_name(&self) -> &'static str { |
| 19 | match self { |
| 20 | PowerAction::Shutdown => "PowerOff", |
| 21 | PowerAction::Reboot => "Reboot", |
| 22 | PowerAction::Suspend => "Suspend", |
| 23 | } |
| 24 | } |
| 25 | } |
| 26 | |
| 27 | /// Execute a power action via logind |
| 28 | pub fn execute(action: PowerAction) -> Result<()> { |
| 29 | let conn = Connection::system().context("Failed to connect to system D-Bus")?; |
| 30 | |
| 31 | // Call org.freedesktop.login1.Manager.<Action>(interactive: bool) |
| 32 | // interactive=false means no polkit prompt |
| 33 | conn.call_method( |
| 34 | Some("org.freedesktop.login1"), |
| 35 | "/org/freedesktop/login1", |
| 36 | Some("org.freedesktop.login1.Manager"), |
| 37 | action.method_name(), |
| 38 | &(false,), // interactive = false |
| 39 | ) |
| 40 | .context(format!("Failed to execute {:?}", action))?; |
| 41 | |
| 42 | Ok(()) |
| 43 | } |
| 44 | |
| 45 | /// Async version of execute using zbus async API |
| 46 | pub async fn execute_async(action: PowerAction) -> Result<()> { |
| 47 | let conn = zbus::Connection::system() |
| 48 | .await |
| 49 | .context("Failed to connect to system D-Bus")?; |
| 50 | |
| 51 | conn.call_method( |
| 52 | Some("org.freedesktop.login1"), |
| 53 | "/org/freedesktop/login1", |
| 54 | Some("org.freedesktop.login1.Manager"), |
| 55 | action.method_name(), |
| 56 | &(false,), |
| 57 | ) |
| 58 | .await |
| 59 | .context(format!("Failed to execute {:?}", action))?; |
| 60 | |
| 61 | Ok(()) |
| 62 | } |
| 63 | |
| 64 | /// Check if a power action is allowed (can be performed without authentication) |
| 65 | pub async fn can_execute(action: PowerAction) -> Result<bool> { |
| 66 | let conn = zbus::Connection::system() |
| 67 | .await |
| 68 | .context("Failed to connect to system D-Bus")?; |
| 69 | |
| 70 | let can_method = match action { |
| 71 | PowerAction::Shutdown => "CanPowerOff", |
| 72 | PowerAction::Reboot => "CanReboot", |
| 73 | PowerAction::Suspend => "CanSuspend", |
| 74 | }; |
| 75 | |
| 76 | let reply: zbus::Message = conn |
| 77 | .call_method( |
| 78 | Some("org.freedesktop.login1"), |
| 79 | "/org/freedesktop/login1", |
| 80 | Some("org.freedesktop.login1.Manager"), |
| 81 | can_method, |
| 82 | &(), |
| 83 | ) |
| 84 | .await |
| 85 | .context(format!("Failed to check {:?} capability", action))?; |
| 86 | |
| 87 | let result: String = reply.body().deserialize().context("Failed to parse reply")?; |
| 88 | |
| 89 | // Returns "yes", "no", "challenge" (needs auth), or "na" (not applicable) |
| 90 | Ok(result == "yes") |
| 91 | } |
| 92 |