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 = [
"bscreensaver-util",
"clap",
"nix",
"xcb",
]

View File

@ -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" }

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 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<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> {
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<Duration>) -> 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::<u8>() == 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)?;
}
}

View File

@ -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(_) => (),
}

View File

@ -214,7 +214,7 @@ async fn heartbeat_task(state_mtx: Arc<Mutex<State>>) -> 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");

View File

@ -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)?,
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 => (),
BlankerState::Blanked => unblank_screen(conn, state)?,
BlankerState::Locked => if state.unlock_dialog.is_none() {
state.unlock_dialog = Some(start_unlock_dialog(conn, state, None)?);
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 => restart_daemon(state)?,
e if e == command_atoms.exit => exit_daemon(state)?,
_ => (),
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");
}
}
},
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());

View File

@ -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(())
}

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()
}
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> {
let cookie = conn.send_request(&x::InternAtom {
only_if_exists: false,