diff --git a/Cargo.lock b/Cargo.lock index d1d087a..3eec34a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -273,6 +273,7 @@ version = "0.1.0" dependencies = [ "bscreensaver-util", "clap", + "nix", "xcb", ] diff --git a/command/Cargo.toml b/command/Cargo.toml index 7fc0325..6587e63 100644 --- a/command/Cargo.toml +++ b/command/Cargo.toml @@ -6,4 +6,5 @@ edition = "2021" [dependencies] bscreensaver-util = { path = "../util" } clap = "3" +nix = "0.23" xcb = { git = "https://github.com/rust-x-bindings/rust-xcb", rev = "d09b5f91bc07d56673f1bc0d6c7ecd72b5ff7b3e" } diff --git a/command/src/lib.rs b/command/src/lib.rs index fb13abd..7640a29 100644 --- a/command/src/lib.rs +++ b/command/src/lib.rs @@ -1,4 +1,5 @@ -use std::{error::Error as StdError, fmt}; +use nix::poll::{poll, PollFd, PollFlags}; +use std::{cmp, error::Error as StdError, fmt, time::{Duration, Instant}, os::unix::prelude::AsRawFd}; use xcb::{x, Xid}; use bscreensaver_util::{create_atom, BSCREENSAVER_WM_CLASS}; @@ -12,6 +13,8 @@ const BSCREENSAVER_DEACTIVATE_ATOM_NAME: &[u8] = b"BSCREENSAVER_DEACTIVATE"; const BSCREENSAVER_RESTART_ATOM_NAME: &[u8] = b"BSCREENSAVER_RESTART"; const BSCREENSAVER_EXIT_ATOM_NAME: &[u8] = b"BSCREENSAVER_EXIT"; +const BSCREENSAVER_RESULT_ATOM_NANE: &[u8] = b"BSCREENSAVER_RESULT"; + #[derive(Debug, Clone, Copy)] pub enum BCommand { Blank, @@ -36,14 +39,20 @@ impl BCommand { #[derive(Debug)] pub enum Error { X(xcb::Error), + Sys(nix::errno::Errno), NotRunning, + CommandError, + Timeout, } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::X(err) => write!(f, "{}", err), + Self::X(err) => write!(f, "X error: {}", err), + Self::Sys(err) => write!(f, "System error: {}", err), Self::NotRunning => write!(f, "bscreensaver is not running"), + Self::CommandError => write!(f, "bscreensaver returned an error"), + Self::Timeout => write!(f, "Timed out waiting for a response"), } } } @@ -52,7 +61,10 @@ impl StdError for Error { fn source(&self) -> Option<&(dyn StdError + 'static)> { match self { Self::X(err) => Some(err), + Self::Sys(err) => Some(err), Self::NotRunning => None, + Self::CommandError => None, + Self::Timeout => None, } } } @@ -75,6 +87,12 @@ impl From for Error { } } +impl From for Error { + fn from(error: nix::errno::Errno) -> Self { + Self::Sys(error) + } +} + pub fn create_command_window(conn: &xcb::Connection, screen: &x::Screen) -> Result { let mut cookies = Vec::new(); @@ -126,12 +144,33 @@ pub fn create_command_window(conn: &xcb::Connection, screen: &x::Screen) -> Resu Ok(msg_win) } -pub fn bscreensaver_command(command: BCommand) -> Result<(), Error> { +pub fn bscreensaver_command_response(conn: &xcb::Connection, reply_window: x::Window, is_success: bool) -> Result<(), Error> { + let message_type = create_atom(conn, BSCREENSAVER_RESULT_ATOM_NANE)?; + conn.send_and_check_request(&x::SendEvent { + propagate: false, + destination: x::SendEventDest::Window(reply_window), + event_mask: x::EventMask::STRUCTURE_NOTIFY, + event: &x::ClientMessageEvent::new( + reply_window, + message_type, + x::ClientMessageData::Data32([ + if is_success { 1 } else { 0 }, + 0, + 0, + 0, + 0, + ]) + ), + })?; + Ok(()) +} + +pub fn bscreensaver_command(command: BCommand, timeout: Option) -> Result<(), Error> { let (conn, _) = xcb::Connection::connect(None)?; let setup = conn.get_setup(); let msg_window_id_atom = create_atom(&conn, COMMAND_WINDOW_ID_ATOM_NAME)?; - let msg_window_id = 'outer: loop { + let (msg_window_screen, msg_window_id) = 'outer: loop { for screen in setup.roots() { let cookie = conn.send_request(&x::GetProperty { delete: false, @@ -158,12 +197,31 @@ pub fn bscreensaver_command(command: BCommand) -> Result<(), Error> { }); let reply = conn.wait_for_reply(cookie)?; if reply.value::() == COMMAND_WINDOW_WM_NAME { - break 'outer Ok(window); + break 'outer Ok((screen, window)); } } break Err(Error::NotRunning); }?; let command_atom = create_atom(&conn, command.atom_name())?; + let result_atom = create_atom(&conn, BSCREENSAVER_RESULT_ATOM_NANE)?; + + let reply_window = conn.generate_id(); + conn.send_and_check_request(&x::CreateWindow { + depth: x::COPY_FROM_PARENT as u8, + wid: reply_window, + parent: msg_window_screen.root(), + x: -50, + y: -50, + width: 1, + height: 1, + border_width: 0, + class: x::WindowClass::InputOnly, + visual: x::COPY_FROM_PARENT, + value_list: &[ + x::Cw::OverrideRedirect(true), + x::Cw::EventMask(x::EventMask::STRUCTURE_NOTIFY), + ], + })?; let res = conn.send_and_check_request(&x::SendEvent { propagate: false, @@ -172,13 +230,54 @@ pub fn bscreensaver_command(command: BCommand) -> Result<(), Error> { event: &x::ClientMessageEvent::new( msg_window_id, command_atom, - x::ClientMessageData::Data32([0; 5]) + x::ClientMessageData::Data32([ + reply_window.resource_id(), + 0, + 0, + 0, + 0, + ]) ), }); match res { - Err(xcb::ProtocolError::X(x::Error::Window(x::WindowError { .. }), _)) => Err(Error::NotRunning), - Err(err) => Err(Error::X(xcb::Error::Protocol(err))), - Ok(_) => Ok(()), + Err(xcb::ProtocolError::X(x::Error::Window(x::WindowError { .. }), _)) => Err(Error::NotRunning)?, + Err(err) => Err(Error::X(xcb::Error::Protocol(err)))?, + Ok(_) => (), + } + + let start = Instant::now(); + 'outer1: loop { + while let Some(event) = conn.poll_for_event()? { + match event { + xcb::Event::X(x::Event::ClientMessage(ev)) if ev.r#type() == result_atom => { + match ev.data() { + x::ClientMessageData::Data32(data) => { + if data[0] != 1 { + break 'outer1 Err(Error::CommandError); + } + }, + _ => break 'outer1 Err(Error::CommandError), + } + break 'outer1 Ok(()); + }, + _ => (), + } + } + + let poll_timeout = timeout.map(|to| { + let since_start = start.elapsed(); + if since_start > to { + 0i32 + } else { + cmp::max(i32::MAX as u128, (to - since_start).as_millis()) as i32 + } + }).unwrap_or(-1); + if poll_timeout == 0 { + break 'outer1 Err(Error::Timeout); + } + + let mut pfds = vec![PollFd::new(conn.as_raw_fd(), PollFlags::POLLIN)]; + poll(pfds.as_mut_slice(), poll_timeout)?; } } diff --git a/command/src/main.rs b/command/src/main.rs index dd824ef..51ebb68 100644 --- a/command/src/main.rs +++ b/command/src/main.rs @@ -1,5 +1,5 @@ use clap::{Arg, Command}; -use std::{env, io, process::exit}; +use std::{env, io, process::exit, time::Duration}; use bscreensaver_command::{BCommand, Error, bscreensaver_command}; @@ -56,7 +56,7 @@ fn main() -> io::Result<()> { exit(1); }; - match bscreensaver_command(command) { + match bscreensaver_command(command, Some(Duration::from_secs(4))) { Err(Error::NotRunning) => { eprintln!("bscreensaver is not running"); exit(1); @@ -65,6 +65,10 @@ fn main() -> io::Result<()> { eprintln!("Failed to communicate with X server: {}", err); exit(1); }, + Err(err) => { + eprintln!("Failed to communicate with X server: {}", err); + exit(1); + }, Ok(_) => (), } diff --git a/dbus-service/src/main.rs b/dbus-service/src/main.rs index 61305ee..7ab2208 100644 --- a/dbus-service/src/main.rs +++ b/dbus-service/src/main.rs @@ -214,7 +214,7 @@ async fn heartbeat_task(state_mtx: Arc>) -> anyhow::Result<()> { } drop(state); task::block_on(async { - if let Err(err) = bscreensaver_command(BCommand::Deactivate) { + 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"); diff --git a/locker/src/main.rs b/locker/src/main.rs index 945d0df..3be2e80 100644 --- a/locker/src/main.rs +++ b/locker/src/main.rs @@ -22,10 +22,10 @@ use std::{ process::{exit, Child, Command, Stdio}, time::{Duration, Instant}, }; -use xcb::{randr, x, xfixes, xinput, Xid}; +use xcb::{randr, x, xfixes, xinput, Xid, XidNew}; use xcb_xembed::embedder::Embedder; -use bscreensaver_command::{BCommand, create_command_window}; +use bscreensaver_command::{BCommand, create_command_window, bscreensaver_command_response}; use bscreensaver_util::{*, settings::Configuration}; use pidfd::{CreatePidFd, PidFd}; @@ -543,22 +543,60 @@ fn handle_xcb_events<'a>(conn: &'a xcb::Connection, state: &mut State<'a>, comma // TODO: implement some sort of hysteresis/debouncing for mouse motion state.last_user_activity = Instant::now(); }, - xcb::Event::X(x::Event::ClientMessage(ev)) => match ev.r#type() { - b if b == command_atoms.blank => blank_screen(conn, state)?, - l if l == command_atoms.lock => lock_screen(conn, state)?, - d if d == command_atoms.deactivate => { - state.last_user_activity = Instant::now(); - match state.blanker_state { - BlankerState::Idle => (), - BlankerState::Blanked => unblank_screen(conn, state)?, - BlankerState::Locked => if state.unlock_dialog.is_none() { - state.unlock_dialog = Some(start_unlock_dialog(conn, state, None)?); + xcb::Event::X(x::Event::ClientMessage(ev)) => { + let res = match ev.r#type() { + b if b == command_atoms.blank => Some(blank_screen(conn, state)), + l if l == command_atoms.lock => Some(lock_screen(conn, state)), + d if d == command_atoms.deactivate => { + state.last_user_activity = Instant::now(); + match state.blanker_state { + BlankerState::Idle => Some(Ok(())), + BlankerState::Blanked => Some(unblank_screen(conn, state)), + BlankerState::Locked => { + if state.unlock_dialog.is_none() { + match start_unlock_dialog(conn, state, None) { + Ok(unlock_dialog) => { + state.unlock_dialog = Some(unlock_dialog); + Some(Ok(())) + }, + Err(err) => Some(Err(err)), + } + } else { + Some(Ok(())) + } + }, + } + }, + r if r == command_atoms.restart => Some(restart_daemon(state)), + e if e == command_atoms.exit => Some(exit_daemon(state)), + _ => None, + }; + + if let Some(res) = res { + let is_success = if let Err(err) = res { + warn!("Failed to handle remote command {}: {}", ev.r#type().resource_id(), err); + false + } else { + true + }; + + let reply_window = match ev.data() { + x::ClientMessageData::Data32(data) => { + match unsafe { x::Window::new(data[0]) } { + x::WINDOW_NONE => None, + wid => Some(wid), + } }, + _ => None, + }; + if let Some(reply_window) = reply_window { + if let Err(err) = bscreensaver_command_response(conn, reply_window, is_success) { + info!("Failed to send command response: {}", err); + } + } else { + debug!("Command sender did not include a reply window"); } - }, - r if r == command_atoms.restart => restart_daemon(state)?, - e if e == command_atoms.exit => exit_daemon(state)?, - _ => (), + } }, xcb::Event::X(x::Event::MapNotify(ev)) if ev.window() == unlock_dialog_window(&state) => { debug!("Unlock dialog mapped, requesting focus"); @@ -710,10 +748,7 @@ fn start_unlock_dialog<'a>(conn: &'a xcb::Connection, state: &State<'a>, trigger child_out.read_exact(&mut xid_buf)?; let client_window = { let wid: u32 = ((xid_buf[0] as u32) << 24) | ((xid_buf[1] as u32) << 16) | ((xid_buf[2] as u32) << 8) | (xid_buf[3] as u32); - unsafe { - use xcb::XidNew; - x::Window::new(wid) - } + unsafe { x::Window::new(wid) } }; debug!("Dialog process created plug window 0x{:x}", client_window.resource_id()); diff --git a/systemd/src/main.rs b/systemd/src/main.rs index ec3bc61..5f48671 100644 --- a/systemd/src/main.rs +++ b/systemd/src/main.rs @@ -2,7 +2,7 @@ use async_std::task; use futures::{future::FutureExt, pin_mut, select, AsyncReadExt, StreamExt}; use log::{debug, error, info, warn}; use logind_zbus::manager::{InhibitType, ManagerProxy}; -use std::{os::unix::io::AsRawFd, process::exit}; +use std::{os::unix::io::AsRawFd, process::exit, time::Duration}; use zbus::Connection; use bscreensaver_command::{bscreensaver_command, BCommand}; @@ -85,7 +85,7 @@ async fn dbus_task() -> anyhow::Result<()> { async fn do_bscreensaver_command(command: BCommand) -> anyhow::Result<()> { task::block_on(async { - bscreensaver_command(command) + bscreensaver_command(command, Some(Duration::from_secs(4))) })?; Ok(()) } diff --git a/util/src/lib.rs b/util/src/lib.rs index 46d1273..30ec6af 100644 --- a/util/src/lib.rs +++ b/util/src/lib.rs @@ -16,6 +16,13 @@ pub fn opt_contains(o: &Option, v: &T) -> bool { o.as_ref().filter(|ov| ov == &v).is_some() } +pub fn result_contains(res: &Result, v: &T) -> bool { + match res { + Ok(ok) if ok == v => true, + _ => false, + } +} + pub fn create_atom(conn: &xcb::Connection, name: &[u8]) -> xcb::Result { let cookie = conn.send_request(&x::InternAtom { only_if_exists: false,