Have remote commands return a success/failure message

This commit is contained in:
Brian Tarricone 2022-05-14 14:55:22 -07:00
parent aafe026092
commit 8394d45d1a
8 changed files with 181 additions and 34 deletions

1
Cargo.lock generated
View File

@ -273,6 +273,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"bscreensaver-util", "bscreensaver-util",
"clap", "clap",
"nix",
"xcb", "xcb",
] ]

View File

@ -6,4 +6,5 @@ edition = "2021"
[dependencies] [dependencies]
bscreensaver-util = { path = "../util" } bscreensaver-util = { path = "../util" }
clap = "3" clap = "3"
nix = "0.23"
xcb = { git = "https://github.com/rust-x-bindings/rust-xcb", rev = "d09b5f91bc07d56673f1bc0d6c7ecd72b5ff7b3e" } xcb = { git = "https://github.com/rust-x-bindings/rust-xcb", rev = "d09b5f91bc07d56673f1bc0d6c7ecd72b5ff7b3e" }

View File

@ -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 xcb::{x, Xid};
use bscreensaver_util::{create_atom, BSCREENSAVER_WM_CLASS}; 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_RESTART_ATOM_NAME: &[u8] = b"BSCREENSAVER_RESTART";
const BSCREENSAVER_EXIT_ATOM_NAME: &[u8] = b"BSCREENSAVER_EXIT"; const BSCREENSAVER_EXIT_ATOM_NAME: &[u8] = b"BSCREENSAVER_EXIT";
const BSCREENSAVER_RESULT_ATOM_NANE: &[u8] = b"BSCREENSAVER_RESULT";
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum BCommand { pub enum BCommand {
Blank, Blank,
@ -36,14 +39,20 @@ impl BCommand {
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
X(xcb::Error), X(xcb::Error),
Sys(nix::errno::Errno),
NotRunning, NotRunning,
CommandError,
Timeout,
} }
impl fmt::Display for Error { impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { 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::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)> { fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self { match self {
Self::X(err) => Some(err), Self::X(err) => Some(err),
Self::Sys(err) => Some(err),
Self::NotRunning => None, Self::NotRunning => None,
Self::CommandError => None,
Self::Timeout => None,
} }
} }
} }
@ -75,6 +87,12 @@ impl From<xcb::ConnError> for Error {
} }
} }
impl From<nix::errno::Errno> for Error {
fn from(error: nix::errno::Errno) -> Self {
Self::Sys(error)
}
}
pub fn create_command_window(conn: &xcb::Connection, screen: &x::Screen) -> Result<x::Window, Error> { pub fn create_command_window(conn: &xcb::Connection, screen: &x::Screen) -> Result<x::Window, Error> {
let mut cookies = Vec::new(); let mut cookies = Vec::new();
@ -126,12 +144,33 @@ pub fn create_command_window(conn: &xcb::Connection, screen: &x::Screen) -> Resu
Ok(msg_win) 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<Duration>) -> Result<(), Error> {
let (conn, _) = xcb::Connection::connect(None)?; let (conn, _) = xcb::Connection::connect(None)?;
let setup = conn.get_setup(); let setup = conn.get_setup();
let msg_window_id_atom = create_atom(&conn, COMMAND_WINDOW_ID_ATOM_NAME)?; 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() { for screen in setup.roots() {
let cookie = conn.send_request(&x::GetProperty { let cookie = conn.send_request(&x::GetProperty {
delete: false, delete: false,
@ -158,12 +197,31 @@ pub fn bscreensaver_command(command: BCommand) -> Result<(), Error> {
}); });
let reply = conn.wait_for_reply(cookie)?; let reply = conn.wait_for_reply(cookie)?;
if reply.value::<u8>() == COMMAND_WINDOW_WM_NAME { if reply.value::<u8>() == COMMAND_WINDOW_WM_NAME {
break 'outer Ok(window); break 'outer Ok((screen, window));
} }
} }
break Err(Error::NotRunning); break Err(Error::NotRunning);
}?; }?;
let command_atom = create_atom(&conn, command.atom_name())?; 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 { let res = conn.send_and_check_request(&x::SendEvent {
propagate: false, propagate: false,
@ -172,13 +230,54 @@ pub fn bscreensaver_command(command: BCommand) -> Result<(), Error> {
event: &x::ClientMessageEvent::new( event: &x::ClientMessageEvent::new(
msg_window_id, msg_window_id,
command_atom, command_atom,
x::ClientMessageData::Data32([0; 5]) x::ClientMessageData::Data32([
reply_window.resource_id(),
0,
0,
0,
0,
])
), ),
}); });
match res { match res {
Err(xcb::ProtocolError::X(x::Error::Window(x::WindowError { .. }), _)) => Err(Error::NotRunning), Err(xcb::ProtocolError::X(x::Error::Window(x::WindowError { .. }), _)) => Err(Error::NotRunning)?,
Err(err) => Err(Error::X(xcb::Error::Protocol(err))), Err(err) => Err(Error::X(xcb::Error::Protocol(err)))?,
Ok(_) => Ok(()), 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)?;
} }
} }

View File

@ -1,5 +1,5 @@
use clap::{Arg, Command}; 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}; use bscreensaver_command::{BCommand, Error, bscreensaver_command};
@ -56,7 +56,7 @@ fn main() -> io::Result<()> {
exit(1); exit(1);
}; };
match bscreensaver_command(command) { match bscreensaver_command(command, Some(Duration::from_secs(4))) {
Err(Error::NotRunning) => { Err(Error::NotRunning) => {
eprintln!("bscreensaver is not running"); eprintln!("bscreensaver is not running");
exit(1); exit(1);
@ -65,6 +65,10 @@ fn main() -> io::Result<()> {
eprintln!("Failed to communicate with X server: {}", err); eprintln!("Failed to communicate with X server: {}", err);
exit(1); exit(1);
}, },
Err(err) => {
eprintln!("Failed to communicate with X server: {}", err);
exit(1);
},
Ok(_) => (), Ok(_) => (),
} }

View File

@ -214,7 +214,7 @@ async fn heartbeat_task(state_mtx: Arc<Mutex<State>>) -> anyhow::Result<()> {
} }
drop(state); drop(state);
task::block_on(async { 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); warn!("Failed to deactivate screen lock: {}", err);
} else { } else {
debug!("Successfully issued deactivate heartbeat"); debug!("Successfully issued deactivate heartbeat");

View File

@ -22,10 +22,10 @@ use std::{
process::{exit, Child, Command, Stdio}, process::{exit, Child, Command, Stdio},
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use xcb::{randr, x, xfixes, xinput, Xid}; use xcb::{randr, x, xfixes, xinput, Xid, XidNew};
use xcb_xembed::embedder::Embedder; 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 bscreensaver_util::{*, settings::Configuration};
use pidfd::{CreatePidFd, PidFd}; 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 // TODO: implement some sort of hysteresis/debouncing for mouse motion
state.last_user_activity = Instant::now(); state.last_user_activity = Instant::now();
}, },
xcb::Event::X(x::Event::ClientMessage(ev)) => match ev.r#type() { xcb::Event::X(x::Event::ClientMessage(ev)) => {
b if b == command_atoms.blank => blank_screen(conn, state)?, let res = match ev.r#type() {
l if l == command_atoms.lock => lock_screen(conn, state)?, b if b == command_atoms.blank => Some(blank_screen(conn, state)),
d if d == command_atoms.deactivate => { l if l == command_atoms.lock => Some(lock_screen(conn, state)),
state.last_user_activity = Instant::now(); d if d == command_atoms.deactivate => {
match state.blanker_state { state.last_user_activity = Instant::now();
BlankerState::Idle => (), match state.blanker_state {
BlankerState::Blanked => unblank_screen(conn, state)?, BlankerState::Idle => Some(Ok(())),
BlankerState::Locked => if state.unlock_dialog.is_none() { BlankerState::Blanked => Some(unblank_screen(conn, state)),
state.unlock_dialog = Some(start_unlock_dialog(conn, state, None)?); 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) => { xcb::Event::X(x::Event::MapNotify(ev)) if ev.window() == unlock_dialog_window(&state) => {
debug!("Unlock dialog mapped, requesting focus"); 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)?; child_out.read_exact(&mut xid_buf)?;
let client_window = { 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); 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 { unsafe { x::Window::new(wid) }
use xcb::XidNew;
x::Window::new(wid)
}
}; };
debug!("Dialog process created plug window 0x{:x}", client_window.resource_id()); debug!("Dialog process created plug window 0x{:x}", client_window.resource_id());

View File

@ -2,7 +2,7 @@ use async_std::task;
use futures::{future::FutureExt, pin_mut, select, AsyncReadExt, StreamExt}; use futures::{future::FutureExt, pin_mut, select, AsyncReadExt, StreamExt};
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
use logind_zbus::manager::{InhibitType, ManagerProxy}; 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 zbus::Connection;
use bscreensaver_command::{bscreensaver_command, BCommand}; 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<()> { async fn do_bscreensaver_command(command: BCommand) -> anyhow::Result<()> {
task::block_on(async { task::block_on(async {
bscreensaver_command(command) bscreensaver_command(command, Some(Duration::from_secs(4)))
})?; })?;
Ok(()) Ok(())
} }

View File

@ -16,6 +16,13 @@ pub fn opt_contains<T: PartialEq>(o: &Option<T>, v: &T) -> bool {
o.as_ref().filter(|ov| ov == &v).is_some() o.as_ref().filter(|ov| ov == &v).is_some()
} }
pub fn result_contains<T: PartialEq, E>(res: &Result<T, E>, v: &T) -> bool {
match res {
Ok(ok) if ok == v => true,
_ => false,
}
}
pub fn create_atom(conn: &xcb::Connection, name: &[u8]) -> xcb::Result<x::Atom> { pub fn create_atom(conn: &xcb::Connection, name: &[u8]) -> xcb::Result<x::Atom> {
let cookie = conn.send_request(&x::InternAtom { let cookie = conn.send_request(&x::InternAtom {
only_if_exists: false, only_if_exists: false,