Have remote commands return a success/failure message
This commit is contained in:
parent
aafe026092
commit
8394d45d1a
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -273,6 +273,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"bscreensaver-util",
|
||||
"clap",
|
||||
"nix",
|
||||
"xcb",
|
||||
]
|
||||
|
||||
|
@ -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" }
|
||||
|
@ -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)?;
|
||||
}
|
||||
}
|
||||
|
@ -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(_) => (),
|
||||
}
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user