use async_std::{fs::File, prelude::*, sync::{Arc, Mutex}, task}; use bscreensaver_util::init_logging; use futures::{future::FutureExt, pin_mut, select}; use log::{debug, error, info, trace, warn}; use std::{io, process::exit, time::{Duration, Instant}}; use zbus::{interface, fdo::{self, DBusProxy, RequestNameFlags}, names::{BusName, UniqueName, WellKnownName}, ConnectionBuilder, MessageHeader}; use bscreensaver_command::{bscreensaver_command, BCommand}; use bscreensaver_util::opt_contains; const OUR_DBUS_NAME: &str = "org.freedesktop.ScreenSaver"; const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(45); struct Inhibitor { cookie: u32, app_name: String, peer: Option>, } struct State { inhibitors: Vec, } struct ScreenSaver { state: Arc>, } #[interface(name = "org.freedesktop.ScreenSaver")] impl ScreenSaver { async fn inhibit( &mut self, #[zbus(header)] hdr: MessageHeader<'_>, app_name: &str, reason: &str ) -> fdo::Result { debug!("Handling inhibit for app {}: {}", app_name, reason); if app_name.trim().is_empty() { return Err(fdo::Error::InvalidArgs("Application name is blank".to_string())); } else if reason.trim().is_empty() { return Err(fdo::Error::InvalidArgs("Reason is blank".to_string())); } // Firefox tries to inhibit when only audio is playing, so ignore that if reason.contains("audio") && !reason.contains("video") { info!("Ignoring audio-only inhibit from app {}", app_name); return Ok(0); } let peer = hdr.sender(); let cookie = rand_u32().await .map_err(|err| fdo::Error::IOError(err.to_string()))?; self.state.lock().await.inhibitors.push(Inhibitor { cookie, app_name: app_name.to_string(), peer: peer.map(|s| s.to_owned()), }); Ok(cookie) } async fn un_inhibit(&mut self, cookie: u32) -> fdo::Result<()> { let mut state = self.state.lock().await; let before = state.inhibitors.len(); state.inhibitors.retain(|inhibitor| { if inhibitor.cookie == cookie { info!("Uninhibit received from {} for cookie {}", inhibitor.app_name, cookie); false } else { true } }); if before == state.inhibitors.len() { info!("No inhibitor found with cookie {}", cookie); } Ok(()) } } #[derive(Clone, Copy, PartialEq)] enum DpmsState { Unknown, WasDisabled, WeDisabled, WeEnabled, } struct DpmsHandling { conn: Option, state: DpmsState, } impl DpmsHandling { fn init() -> Self { xcb::Connection::connect_with_extensions(None, &[xcb::Extension::Dpms],&[]) .map(|(conn, _)| Some(conn)) .unwrap_or_else(|err| match err { xcb::ConnError::ClosedExtNotSupported => None, _ => { warn!("Failed to connect to X display; we will not handle DPMS on inhibit: {}", err); None }, }) .and_then(|conn| { let cookie = conn.send_request(&xcb::dpms::Capable {}); conn.wait_for_reply(cookie) .map(|capable| { if capable.capable() { Some(Self { conn: Some(conn), state: DpmsState::Unknown, }) } else { None } }) .unwrap_or_else(|err| { warn!("Failed to check if X server is DPMS-capable: {}", err); None }) }) .unwrap_or_else(|| Self { conn: None, state: DpmsState::Unknown, }) } fn maybe_disable_dpms(&mut self) { if let Some(conn) = &self.conn { let cookie = conn.send_request(&xcb::dpms::Info {}); match conn.wait_for_reply(cookie) { Err(err) => warn!("Failed to query DPMS state: {}", err), Ok(info) if info.state() => match conn.send_and_check_request(&xcb::dpms::Disable {}) { Err(err) => warn!("Failed to disable DPMS: {}", err), Ok(_) => { debug!("Successfully disabled DPMS"); self.state = DpmsState::WeDisabled; }, } Ok(_) => { debug!("DPMS was already disabled"); if self.state != DpmsState::WeDisabled { self.state = DpmsState::WasDisabled; } }, } } } fn maybe_enable_dpms(&mut self) { if let Some(conn) = &self.conn { if self.state == DpmsState::WeDisabled { match conn.send_and_check_request(&xcb::dpms::Enable {}) { Err(err) => warn!("Failed to enable DPMS: {}", err), Ok(_) => { debug!("Successfully enabled DPMS"); self.state = DpmsState::WeEnabled; }, } } else { debug!("We didn't disable DPMS, so we're not going to re-enable it"); } } } } #[async_std::main] async fn main() { init_logging("BSCREENSAVER_DBUS_SERVICE_LOG"); let state = Arc::new(Mutex::new(State { inhibitors: Vec::new(), })); let xcb_handle = task::spawn(xcb_task()).fuse(); let dbus_handle = task::spawn(dbus_task(Arc::clone(&state))).fuse(); let heartbeat_handle = task::spawn(heartbeat_task(Arc::clone(&state))).fuse(); pin_mut!(xcb_handle, dbus_handle, heartbeat_handle); let res = loop { select! { _ = xcb_handle => { info!("Lost connection to X server; quitting"); break Ok(()); }, res = dbus_handle => { match res { Err(err) => error!("Lost connection to the system bus: {}", err), Ok(_) => error!("DBus task exited normally; this should not happen!"), } break Err(()); }, res = heartbeat_handle => { match res { Err(err) => error!("Heartbeat task terminated with error: {}", err), Ok(_) => error!("Heartbeat task exited normally; this should not happen!"), } break Err(()); } }; }; if let Err(_) = res { exit(1); } } async fn xcb_task() -> anyhow::Result<()> { let (xcb_conn, _) = task::block_on(async { xcb::Connection::connect(None) })?; let mut xcb_conn = async_xcb::AsyncConnection::new(xcb_conn)?; // We need to drain the XCB connection periodically. Even though we have not // asked for any events, we'll still get stuff like MappingNotify if the keyboard // settings change. loop { let mut buf = [0u8; 512]; xcb_conn.read(&mut buf).await?; } } async fn dbus_task(state: Arc>) -> anyhow::Result<()> { let org_fdo_screensaver = ScreenSaver { state: Arc::clone(&state) }; let screensaver = ScreenSaver { state: Arc::clone(&state) }; let dbus_conn = ConnectionBuilder::session()? .serve_at("/org/freedesktop/ScreenSaver", org_fdo_screensaver)? .serve_at("/ScreenSaver", screensaver)? .build() .await?; let our_unique_name = dbus_conn.unique_name().unwrap(); let dbus_proxy = DBusProxy::new(&dbus_conn).await?; dbus_proxy.request_name( WellKnownName::from_static_str(OUR_DBUS_NAME)?, RequestNameFlags::AllowReplacement | RequestNameFlags::ReplaceExisting | RequestNameFlags::DoNotQueue ).await?; let mut name_owner_changed_stream = dbus_proxy.receive_name_owner_changed().await?; loop { if let Some(name_owner_changed) = name_owner_changed_stream.next().await { let args = name_owner_changed.args()?; match args.name() { BusName::WellKnown(name) if name == OUR_DBUS_NAME => { if args.new_owner().is_none() || opt_contains(&args.new_owner(), &our_unique_name) { info!("Lost bus name {}; quitting", OUR_DBUS_NAME); exit(0); } }, BusName::Unique(name) => { if args.new_owner().is_none() { state.lock().await.inhibitors.retain(|inhibitor| { if inhibitor.peer.as_ref().filter(|n| n == &name).is_some() { info!("Canceling inhibit from {}, as the client has disappeared", inhibitor.app_name); false } else { true } }); } }, _ => (), } } } } async fn heartbeat_task(state_mtx: Arc>) -> anyhow::Result<()> { let mut last_heartbeat: Option = None; let mut dpms_handling = task::block_on(async { DpmsHandling::init() }); loop { let state = state_mtx.lock().await; let next_heartbeat = if state.inhibitors.is_empty() { HEARTBEAT_INTERVAL } else { if let Some(lh) = last_heartbeat { let since_last = Instant::now().duration_since(lh); if since_last < HEARTBEAT_INTERVAL { HEARTBEAT_INTERVAL - since_last } else { Duration::ZERO } } else { Duration::ZERO } }; drop(state); task::sleep(next_heartbeat).await; debug!("Heartbeat timeout expired"); let state = state_mtx.lock().await; if !state.inhibitors.is_empty() { task::block_on(async { dpms_handling.maybe_disable_dpms() }); if last_heartbeat.is_none() || last_heartbeat.as_ref().filter(|lh| lh.elapsed() < HEARTBEAT_INTERVAL).is_none() { trace!("About to deactivate; active inhibitors:"); for inhibitor in &state.inhibitors { trace!(" {}: {}", inhibitor.cookie, inhibitor.app_name); } drop(state); task::block_on(async { if let Err(err) = bscreensaver_command(BCommand::Deactivate, Some(Duration::from_secs(4))) { warn!("Failed to deactivate screen lock: {}", err); } else { debug!("Successfully issued deactivate heartbeat"); last_heartbeat = Some(Instant::now()); } }); } } else { task::block_on(async { dpms_handling.maybe_enable_dpms() }); } } } async fn rand_u32() -> io::Result { let mut f = File::open("/dev/urandom").await?; let mut buf = [0u8; 4]; f.read_exact(&mut buf).await?; Ok(((buf[0] as u32) << 24) | ((buf[1] as u32) << 16) | ((buf[2] as u32) << 8) | (buf[3] as u32)) }