//! P5-46 / P6-1 — System power management (call continuity). //! //! Prevents the system from sleeping / turning off the display while a voice or //! video call is active, then releases the request when the call ends. The web //! client calls `set_call_active(true|false)` from `useTauriCallPower` as the //! call-embed atom transitions. //! //! Windows: `SetThreadExecutionState`. The request is per-thread and persists //! until cleared, so we run every set/clear on the **main thread** (via //! `run_on_main_thread`) to guarantee the clear cancels the matching set even //! though Tauri commands otherwise run on a pool thread. //! //! Linux (P6-1): `org.freedesktop.ScreenSaver` `Inhibit`/`UnInhibit` over the //! session bus (zbus, blocking API). The inhibit cookie returned by `Inhibit` //! is stored in Tauri managed state (`ScreenSaverInhibit`) so the later //! `UnInhibit` can release exactly that request. The owning D-Bus **connection** //! is stored alongside the cookie and kept alive for the inhibit's whole //! duration: `org.freedesktop.ScreenSaver` auto-releases an inhibit the instant //! the connection that took it disappears, so a per-call function-local //! connection would drop the inhibit immediately. The one connection is opened //! lazily on first inhibit and reused for the matching `UnInhibit`. All D-Bus //! failures are logged and swallowed — a missing/absent screensaver service must //! never break a call. //! //! macOS is out of scope for P6-1 (would use `IOPMAssertionCreate`) and stays a //! no-op; the command stays cross-platform so the web side is unconditional. use tauri::AppHandle; /// The long-lived screensaver-inhibit state (Linux only). Both fields live /// behind ONE mutex so the connection and the cookie it produced can never /// desync: `conn` is the session-bus connection that *owns* the inhibit, and /// `cookie` is the handle returned by `Inhibit`. The connection is opened once /// (lazily, on the first inhibit) and reused for the matching `UnInhibit`; /// keeping it alive here is what stops the screensaver service from /// auto-releasing the inhibit. #[cfg(target_os = "linux")] #[derive(Default)] struct InhibitState { conn: Option, cookie: Option, } /// Tauri managed state wrapper. Registered in `setup()` and read by /// `set_call_active` via `AppHandle::state()`. #[cfg(target_os = "linux")] pub struct ScreenSaverInhibit(std::sync::Mutex); /// Register the Linux screensaver-inhibit managed state. No-op elsewhere. /// Called once from `native::setup()`. pub fn setup(app: &AppHandle) -> tauri::Result<()> { #[cfg(target_os = "linux")] { use tauri::Manager; app.manage(ScreenSaverInhibit(std::sync::Mutex::new( InhibitState::default(), ))); } #[cfg(not(target_os = "linux"))] let _ = app; Ok(()) } #[tauri::command] pub fn set_call_active(app: AppHandle, active: bool) { #[cfg(target_os = "windows")] { let _ = app.run_on_main_thread(move || { use windows::Win32::System::Power::{ SetThreadExecutionState, ES_CONTINUOUS, ES_DISPLAY_REQUIRED, ES_SYSTEM_REQUIRED, }; let flags = if active { ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED } else { // Clearing to ES_CONTINUOUS alone releases the sleep/display // requirement while leaving no lingering per-thread state. ES_CONTINUOUS }; // Safety: FFI call with no pointers; returns the previous state, // which we don't need. unsafe { SetThreadExecutionState(flags); } }); } #[cfg(target_os = "linux")] { use tauri::Manager; // Serialize access to the stored connection+cookie for the duration of // the D-Bus round-trip. This command is the only touch point, so holding // the lock across the (short, blocking) call cannot deadlock. let state = app.state::(); let mut inner = match state.0.lock() { Ok(guard) => guard, Err(e) => { eprintln!("power: ScreenSaverInhibit mutex poisoned: {e}"); return; } }; if active { // Only take a new inhibit if one isn't already held, so repeated // set_call_active(true) calls don't leak cookies. if inner.cookie.is_none() { // Lazily open the ONE long-lived session connection. Because the // screensaver service auto-releases an inhibit when the owning // connection disappears, this connection must outlive the // inhibit — it stays in managed state and is reused below for // UnInhibit. Never reopened once established. if inner.conn.is_none() { match zbus::blocking::Connection::session() { Ok(conn) => inner.conn = Some(conn), Err(e) => { eprintln!("power: D-Bus session connection failed: {e}"); return; } } } // Scope the connection borrow so it ends before we mutate // `inner.cookie` (both go through the MutexGuard's Deref, which // borrows the whole guard, so the borrows must not overlap). let res: zbus::Result = { let conn = inner .conn .as_ref() .expect("connection set immediately above"); match zbus::blocking::Proxy::new( conn, "org.freedesktop.ScreenSaver", "/org/freedesktop/ScreenSaver", "org.freedesktop.ScreenSaver", ) { Ok(proxy) => proxy.call("Inhibit", &("Lotus Chat", "In a Lotus Chat call")), Err(e) => { eprintln!("power: ScreenSaver proxy init failed: {e}"); return; } } }; match res { Ok(cookie) => inner.cookie = Some(cookie), Err(e) => eprintln!("power: ScreenSaver Inhibit failed: {e}"), } } } else if let Some(cookie) = inner.cookie.take() { // Release on the SAME connection that took the inhibit. If it's // somehow gone the inhibit was already auto-released, so nothing to do. if let Some(conn) = inner.conn.as_ref() { match zbus::blocking::Proxy::new( conn, "org.freedesktop.ScreenSaver", "/org/freedesktop/ScreenSaver", "org.freedesktop.ScreenSaver", ) { Ok(proxy) => { let res: zbus::Result<()> = proxy.call("UnInhibit", &(cookie,)); if let Err(e) = res { eprintln!("power: ScreenSaver UnInhibit failed: {e}"); } } Err(e) => eprintln!("power: ScreenSaver proxy init failed: {e}"), } } } } #[cfg(not(any(target_os = "windows", target_os = "linux")))] { // No-op on other platforms (see module docs). Bind the args so the // signature stays identical cross-platform and no unused warnings fire. let _ = (&app, active); } }