From 63a176c26e5d8c552055da6d8f3a5fa2201ccfaa Mon Sep 17 00:00:00 2001 From: "Brian J. Tarricone" Date: Tue, 24 May 2022 19:52:21 -0700 Subject: [PATCH] Major refactor of locker Moves the meat of the screensaver into its own file, and separates out the subservice stuff. --- locker/src/lib.rs | 28 ++ locker/src/main.rs | 640 ++++--------------------------------- locker/src/screensaver.rs | 492 ++++++++++++++++++++++++++++ locker/src/subservice.rs | 100 ++++++ xcb-xembed/src/embedder.rs | 12 +- 5 files changed, 697 insertions(+), 575 deletions(-) create mode 100644 locker/src/lib.rs create mode 100644 locker/src/screensaver.rs create mode 100644 locker/src/subservice.rs diff --git a/locker/src/lib.rs b/locker/src/lib.rs new file mode 100644 index 0000000..57ed625 --- /dev/null +++ b/locker/src/lib.rs @@ -0,0 +1,28 @@ +pub(crate) mod monitor; +pub(crate) mod pidfd; +pub mod screensaver; +pub mod subservice; + +use log::{debug, info}; +use xcb::{x, XidNew}; + +use bscreensaver_command::bscreensaver_command_response; + +pub fn send_command_reply(conn: &xcb::Connection, ev: &x::ClientMessageEvent, is_success: bool) { + 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"); + } +} diff --git a/locker/src/main.rs b/locker/src/main.rs index edbe0af..9117d73 100644 --- a/locker/src/main.rs +++ b/locker/src/main.rs @@ -1,6 +1,3 @@ -mod monitor; -mod pidfd; - use clap::{Arg, Command as ClapCommand}; use log::{debug, error, info, trace, warn}; use nix::{ @@ -15,58 +12,21 @@ use std::{ env, ffi::CString, fs::read_link, - io::{self, Read}, - os::{ - unix::io::AsRawFd, - unix::process::ExitStatusExt, - }, - process::{exit, Child, Command, Stdio}, - time::{Duration, Instant}, + io, + os::unix::io::AsRawFd, + rc::Rc, + process::exit, + sync::Mutex, + time::{Duration, Instant}, path::PathBuf, }; -use xcb::{randr, x, xfixes, xinput, Xid, XidNew}; -use xcb_xembed::embedder::Embedder; +use xcb::{randr, x, xfixes, xinput}; -use bscreensaver_command::{BCommand, create_command_window, bscreensaver_command_response}; +use bscreensaver::{screensaver::{BlankerState, CommandHandlers, Screensaver}, subservice::Subservices}; use bscreensaver_util::{*, settings::Configuration}; -use monitor::*; -use pidfd::{CreatePidFd, PidFd}; const BLANKED_ARG: &str = "blanked"; const LOCKED_ARG: &str = "locked"; -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] -enum BlankerState { - Idle = 0, - Blanked = 1, - Locked = 2, -} - -struct UnlockDialog<'a> { - monitor: Monitor, - embedder: Embedder<'a>, - event_to_forward: Option, - child: Child, - child_pidfd: PidFd, -} - -struct State<'a> { - config: Configuration, - monitors: Vec, - dbus_service: Option<(Child, PidFd)>, - systemd_service: Option<(Child, PidFd)>, - last_user_activity: Instant, - blanker_state: BlankerState, - unlock_dialog: Option>, -} - -struct CommandAtoms { - blank: x::Atom, - lock: x::Atom, - deactivate: x::Atom, - restart: x::Atom, - exit: x::Atom, -} - macro_rules! maybe_add_fd { ($pfds:expr, $fd:expr) => { if let Some(fd) = $fd { @@ -89,12 +49,12 @@ fn main() -> anyhow::Result<()> { .arg( Arg::new("blanked") .long(BLANKED_ARG) - .help("Starts up in the blanked state") + .help("Starts up in the blanked screensaver") ) .arg( Arg::new("locked") .long(LOCKED_ARG) - .help("Starts up in the blanked and locked state") + .help("Starts up in the blanked and locked screensaver") ) .get_matches(); @@ -113,36 +73,31 @@ fn main() -> anyhow::Result<()> { init_randr(&conn)?; xkb::x11::setup(&conn, xkb::x11::MIN_MAJOR_XKB_VERSION, xkb::x11::MIN_MINOR_XKB_VERSION, xkb::x11::NO_FLAGS) .map_err(|_| anyhow::anyhow!("Failed to initialize XKB extension"))?; - create_command_window(&conn, &screen)?; - let command_atoms = CommandAtoms { - blank: create_atom(&conn, BCommand::Blank.atom_name())?, - lock: create_atom(&conn, BCommand::Lock.atom_name())?, - deactivate: create_atom(&conn, BCommand::Deactivate.atom_name())?, - restart: create_atom(&conn, BCommand::Restart.atom_name())?, - exit: create_atom(&conn, BCommand::Exit.atom_name())?, + let helper_dir = PathBuf::from(env!("HELPER_DIR")); + + let subservices = Rc::new(Mutex::new(Subservices::start_all(&helper_dir)?)); + let command_handlers = { + let subservices1 = Rc::clone(&subservices); + let subservices2 = Rc::clone(&subservices); + CommandHandlers { + restart_handler: &move |screensaver, conn, trigger| { + restart_daemon(screensaver, &mut subservices1.lock().unwrap(), trigger.map(|t| (conn, t))) + }, + exit_handler: &move |screensaver, conn, trigger| { + exit_daemon(screensaver, &mut subservices2.lock().unwrap(), trigger.map(|t| (conn, t))) + }, + } }; - - let mut state = State { - config, - monitors: Monitor::set_up_all(&conn)?, - dbus_service: None, - systemd_service: None, - last_user_activity: Instant::now(), - blanker_state: BlankerState::Idle, - unlock_dialog: None, - }; - - start_dbus_service(&mut state)?; - start_systemd_service(&mut state)?; + let mut screensaver = Screensaver::new(&config, &helper_dir, &command_handlers, &conn, screen)?; if args.is_present(LOCKED_ARG) { - match lock_screen(&conn, &mut state) { + match screensaver.lock_screen(&conn) { Err(err) => error!("POSSIBLY FAILED TO LOCK SCREEN ON STARTUP: {}", err), Ok(_) => debug!("Got --{} arg; screen locked on startup", LOCKED_ARG), } } else if args.is_present(BLANKED_ARG) { - match lock_screen(&conn, &mut state) { + match screensaver.blank_screen(&conn) { Err(err) => warn!("Possibly failed to blank screen on startup: {}", err), Ok(_) => debug!("Got --{} arg; screen locked on startup", BLANKED_ARG), } @@ -155,11 +110,12 @@ fn main() -> anyhow::Result<()> { allow_exposures: x::Exposures::NotAllowed, }); + loop { - if let Err(err) = handle_xcb_events(&conn, &mut state, &command_atoms) { + if let Err(err) = screensaver.handle_xcb_events(&conn) { if conn.has_error().is_err() { error!("Lost connection to X server; attempting to restart"); - restart_daemon(&mut state, None)?; + (command_handlers.restart_handler)(&mut screensaver, &conn, None)?; } warn!("Error handling event: {}", err); } @@ -167,16 +123,16 @@ fn main() -> anyhow::Result<()> { let mut pfds = Vec::new(); pfds.push(PollFd::new(signal_fd.as_raw_fd(), PollFlags::POLLIN)); pfds.push(PollFd::new(conn.as_raw_fd(), PollFlags::POLLIN)); - let dbus_service_fd = maybe_add_fd!(&mut pfds, state.dbus_service.as_ref().map(|ds| ds.1.as_raw_fd())); - let systemd_service_fd = maybe_add_fd!(&mut pfds, state.systemd_service.as_ref().map(|ds| ds.1.as_raw_fd())); - let dialog_fd = maybe_add_fd!(&mut pfds, state.unlock_dialog.as_ref().map(|ud| ud.child_pidfd.as_raw_fd())); + let dbus_service_fd = maybe_add_fd!(&mut pfds, subservices.lock().unwrap().dbus_service().map(|ds| ds.pidfd().as_raw_fd())); + let systemd_service_fd = maybe_add_fd!(&mut pfds, subservices.lock().unwrap().systemd_service().map(|ds| ds.pidfd().as_raw_fd())); + let dialog_fd = maybe_add_fd!(&mut pfds, screensaver.unlock_dialog_pidfd().map(|udpfd| udpfd.as_raw_fd())); - let since_last_activity = Instant::now().duration_since(state.last_user_activity); - let poll_timeout = match state.blanker_state { - BlankerState::Idle if since_last_activity > state.config.lock_timeout - state.config.blank_before_locking => Some(Duration::ZERO), - BlankerState::Idle => Some(state.config.lock_timeout - state.config.blank_before_locking - since_last_activity), - BlankerState::Blanked if since_last_activity > state.config.lock_timeout => Some(Duration::ZERO), - BlankerState::Blanked => Some(state.config.lock_timeout - since_last_activity), + let since_last_activity = Instant::now().duration_since(screensaver.last_user_activity()); + let poll_timeout = match screensaver.blanker_state() { + BlankerState::Idle if since_last_activity > config.lock_timeout - config.blank_before_locking => Some(Duration::ZERO), + BlankerState::Idle => Some(config.lock_timeout - config.blank_before_locking - since_last_activity), + BlankerState::Blanked if since_last_activity > config.lock_timeout => Some(Duration::ZERO), + BlankerState::Blanked => Some(config.lock_timeout - since_last_activity), BlankerState::Locked => None, }; let poll_timeout = poll_timeout.map(|pt| if pt.as_millis() > i32::MAX as u128 { @@ -192,18 +148,18 @@ fn main() -> anyhow::Result<()> { for pfd in pfds { if pfd.revents().filter(|pf| pf.contains(PollFlags::POLLIN)).is_some() { let result = match pfd.as_raw_fd() { - fd if fd == signal_fd.as_raw_fd() => handle_signals(&mut state, &mut signal_fd), - fd if fd == conn.as_raw_fd() => handle_xcb_events(&conn, &mut state, &command_atoms), - fd if opt_contains(&dbus_service_fd, &fd) => handle_subservice_quit(state.dbus_service.take(), "DBus", || start_dbus_service(&mut state)), - fd if opt_contains(&systemd_service_fd, &fd) => handle_subservice_quit(state.systemd_service.take(), "systemd", || start_systemd_service(&mut state)), - fd if opt_contains(&dialog_fd, &fd) => handle_unlock_dialog_quit(&conn, &mut state), + fd if fd == signal_fd.as_raw_fd() => handle_signals(&mut screensaver, &mut subservices.lock().unwrap(), &mut signal_fd), + fd if fd == conn.as_raw_fd() => screensaver.handle_xcb_events(&conn), + fd if opt_contains(&dbus_service_fd, &fd) => subservices.lock().unwrap().handle_quit(), + fd if opt_contains(&systemd_service_fd, &fd) => subservices.lock().unwrap().handle_quit(), + fd if opt_contains(&dialog_fd, &fd) => screensaver.handle_unlock_dialog_quit(&conn), _ => Ok(()), }; if let Err(err) = result { if conn.has_error().is_err() { error!("Lost connection to X server; atempting to restart"); - restart_daemon(&mut state, None)?; + (command_handlers.restart_handler)(&mut screensaver, &conn, None)?; } warn!("Error handling event: {}", err); } @@ -211,16 +167,16 @@ fn main() -> anyhow::Result<()> { } } - let since_last_activity = Instant::now().duration_since(state.last_user_activity); + let since_last_activity = Instant::now().duration_since(screensaver.last_user_activity()); - if state.blanker_state < BlankerState::Blanked && since_last_activity > state.config.lock_timeout - state.config.blank_before_locking { - if let Err(err) = blank_screen(&conn, &mut state) { + if screensaver.blanker_state() < BlankerState::Blanked && since_last_activity > config.lock_timeout - config.blank_before_locking { + if let Err(err) = screensaver.blank_screen(&conn) { error!("POSSIBLY FAILED TO BLANK SCREEN: {}", err); } } - if state.blanker_state < BlankerState::Locked && since_last_activity > state.config.lock_timeout { - if let Err(err) = lock_screen(&conn, &mut state) { + if screensaver.blanker_state() < BlankerState::Locked && since_last_activity > config.lock_timeout { + if let Err(err) = screensaver.lock_screen(&conn) { error!("POSSIBLY FAILED TO LOCK SCREEN: {}", err); } } @@ -312,481 +268,43 @@ fn init_randr(conn: &xcb::Connection) -> anyhow::Result<()> { Ok(()) } -fn start_subservice(binary_name: &str) -> anyhow::Result<(Child, PidFd)> { - let child = Command::new(format!("{}/{}", env!("HELPER_DIR"), binary_name)) - .spawn()?; - let pidfd = child.create_pidfd()?; - - Ok((child, pidfd)) -} - -fn start_dbus_service(state: &mut State) -> anyhow::Result<()> { - state.dbus_service = Some(start_subservice("bscreensaver-dbus-service")?); - Ok(()) -} - -fn start_systemd_service(state: &mut State) -> anyhow::Result<()> { - state.systemd_service = Some(start_subservice("bscreensaver-systemd")?); - Ok(()) -} - -fn handle_subservice_quit(service: Option<(Child, PidFd)>, name: &str, start_subservice: F) -> anyhow::Result<()> -where - F: FnOnce() -> anyhow::Result<()> -{ - if let Some(mut service) = service { - if let Some(status) = service.0.try_wait().ok().flatten() { - if !status.success() { - warn!("{} service exited abnormally ({}{}); restarting", name, - status.code().map(|c| format!("code {}", c)).unwrap_or("".to_string()), - status.signal().map(|s| format!("signal {}", s)).unwrap_or("".to_string()) - ); - start_subservice()?; - } - } else { - info!("{} service didn't seem to actually quit", name); - } - } else { - info!("{} service wasn't running; starting it", name); - start_subservice()?; - } - Ok(()) -} - -fn handle_signals(state: &mut State, signal_fd: &mut SignalFd) -> anyhow::Result<()> { +fn handle_signals(screensaver: &mut Screensaver, subservices: &mut Subservices, signal_fd: &mut SignalFd) -> anyhow::Result<()> { match signal_fd.read_signal()? { None => (), - Some(info) if info.ssi_signo == Signal::SIGHUP as u32 => restart_daemon(state, None)?, - Some(info) if info.ssi_signo == Signal::SIGINT as u32 => exit_daemon(state, None)?, - Some(info) if info.ssi_signo == Signal::SIGQUIT as u32 => exit_daemon(state, None)?, - Some(info) if info.ssi_signo == Signal::SIGTERM as u32 => exit_daemon(state, None)?, + Some(info) if info.ssi_signo == Signal::SIGHUP as u32 => restart_daemon(screensaver, subservices, None)?, + Some(info) if info.ssi_signo == Signal::SIGINT as u32 => exit_daemon(screensaver, subservices, None)?, + Some(info) if info.ssi_signo == Signal::SIGQUIT as u32 => exit_daemon(screensaver, subservices, None)?, + Some(info) if info.ssi_signo == Signal::SIGTERM as u32 => exit_daemon(screensaver, subservices, None)?, Some(info) => trace!("Unexpected signal {}", info.ssi_signo), } Ok(()) } -fn handle_xcb_events<'a>(conn: &'a xcb::Connection, state: &mut State<'a>, command_atoms: &CommandAtoms) -> anyhow::Result<()> { - loop { - if let Some(event) = conn.poll_for_event()? { - let embedder_handled = if let Some(mut unlock_dialog) = state.unlock_dialog.take() { - match unlock_dialog.embedder.event(&event) { - Err(err) => { - // XXX: should we assume unlock dialog is dead here? - warn!("Error sending event to unlock dialog: {}", err); - false - }, - Ok(handled) => { - state.unlock_dialog = Some(unlock_dialog); - handled - }, - } - } else { - false - }; - - // Allow the code below to handle key presses regardless of whether or not - // the embedder handled it. - let embedder_handled = match event { - xcb::Event::X(x::Event::KeyPress(_)) => false, - _ => embedder_handled, - }; - - if !embedder_handled { - match event { - xcb::Event::RandR(randr::Event::Notify(ev)) => { - debug!("Got xrandr notify event: {:#?}", ev); - for monitor in &state.monitors { - destroy_window(&conn, monitor.unlock_window)?; - destroy_window(&conn, monitor.blanker_window)?; - destroy_gc(&conn, monitor.black_gc)?; - } - state.monitors = Monitor::set_up_all(conn)?; - match state.blanker_state { - BlankerState::Idle => (), - BlankerState::Blanked => { - state.blanker_state = BlankerState::Idle; - blank_screen(conn, state)?; - }, - BlankerState::Locked => { - state.blanker_state = BlankerState::Idle; - lock_screen(conn, state)?; - }, - } - }, - xcb::Event::Input(_) => { - // TODO: implement some sort of hysteresis/debouncing for mouse motion - state.last_user_activity = Instant::now(); - }, - 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, Some((conn, &ev)))), - e if e == command_atoms.exit => Some(exit_daemon(state, Some((conn, &ev)))), - _ => 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 - }; - do_command_reply(conn, &ev, is_success); - } - }, - xcb::Event::X(x::Event::MapNotify(ev)) if ev.window() == unlock_dialog_window(&state) => { - debug!("Unlock dialog mapped, requesting focus"); - if let Some(ref mut unlock_dialog) = &mut state.unlock_dialog { - let _ = unlock_dialog.embedder.activate_client(); - if let Err(err) = unlock_dialog.embedder.focus_client(xcb_xembed::XEmbedFocus::Current) { - warn!("Failed to focus unlock dialog: {}", err); - } - if let Some(event_to_forward) = unlock_dialog.event_to_forward.take() { - let _ = unlock_dialog.embedder.event(&event_to_forward); - } - } - }, - xcb::Event::X(x::Event::ConfigureNotify(ev)) if ev.window() == embedder_window(&state) => { - if let Some(unlock_dialog) = &state.unlock_dialog { - let monitor = &unlock_dialog.monitor; - let x = std::cmp::max(0, monitor.x as i32 + monitor.width as i32 / 2 - ev.width() as i32 / 2); - let y = std::cmp::max(0, monitor.y as i32 + monitor.height as i32 / 2 - ev.height() as i32 / 2); - if x != ev.x() as i32 || y != ev.y() as i32 { - conn.send_and_check_request(&x::ConfigureWindow { - window: unlock_dialog.embedder.embedder_window(), - value_list: &[ - x::ConfigWindow::X(x), - x::ConfigWindow::Y(y), - ], - })?; - } - } - }, - xcb::Event::X(x::Event::Expose(ev)) => { - if let Some(monitor) = state.monitors.iter().find(|m| ev.window() == m.blanker_window || ev.window() == m.unlock_window) { - debug!("got expose for {} at {}x{}+{}+{}", ev.window().resource_id(), ev.width(), ev.height(), ev.x(), ev.y()); - conn.send_and_check_request(&x::PolyFillRectangle { - drawable: x::Drawable::Window(ev.window()), - gc: monitor.black_gc, - rectangles: &[ - x::Rectangle { - x: ev.x() as i16, - y: ev.y() as i16, - width: ev.width(), - height: ev.height(), - }, - ], - })?; - } - }, - ev @ xcb::Event::X(x::Event::MotionNotify(_)) => handle_user_activity(conn, state, ev)?, - xcb::Event::X(x::Event::KeyPress(ev)) => { - if let Err(err) = keysym_for_keypress(conn, &ev).and_then(|keysym| match keysym { - Some(keysym) if keysym == xkb::key::XF86MonBrightnessUp && state.config.handle_brightness_keys => state.monitors.iter().map(|monitor| monitor.brightness_up(conn)).collect(), - Some(keysym) if keysym == xkb::key::XF86MonBrightnessDown && state.config.handle_brightness_keys => state.monitors.iter().map(|monitor| monitor.brightness_down(conn)).collect(), - _ => Ok(()), - }) { - warn!("Failed to handle key press {}: {}", ev.detail(), err); - } - handle_user_activity(conn, state, xcb::Event::X(x::Event::KeyPress(ev)))?; - }, - ev => trace!("Got other event: {:#?}", ev), - } - } - } else { - break; - } - } - - Ok(()) -} - -fn handle_user_activity<'a>(conn: &'a xcb::Connection, state: &mut State<'a>, ev: xcb::Event) -> anyhow::Result<()> { - match state.blanker_state { - BlankerState::Idle => Ok(()), - BlankerState::Blanked => unblank_screen(conn, state), - BlankerState::Locked => match &state.unlock_dialog { - None => { - state.unlock_dialog = match start_unlock_dialog(&conn, state, Some(ev)) { - Err(err) => { - error!("Unable to start unlock dialog: {}", err); - None - }, - Ok(unlock_dialog) => Some(unlock_dialog), - }; - Ok(()) - }, - Some(unlock_dialog) => { - let mut cookies = Vec::new(); - for win in [unlock_dialog.monitor.blanker_window, unlock_dialog.embedder.embedder_window(), unlock_dialog.embedder.client_window()] { - cookies.push(conn.send_request_checked(&x::ConfigureWindow { - window: win, - value_list: &[ - x::ConfigWindow::StackMode(x::StackMode::Above), - ], - })); - } - for cookie in cookies { - conn.check_request(cookie)?; - } - Ok(()) - }, - } - } -} - -fn do_command_reply(conn: &xcb::Connection, ev: &x::ClientMessageEvent, is_success: bool) { - 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"); - } -} - -fn embedder_window(state: &State) -> x::Window { - state.unlock_dialog.as_ref().map(|ud| ud.embedder.embedder_window()).unwrap_or(x::WINDOW_NONE) -} - -fn unlock_dialog_window(state: &State) -> x::Window { - state.unlock_dialog.as_ref().map(|ud| ud.embedder.client_window()).unwrap_or(x::WINDOW_NONE) -} - -fn handle_unlock_dialog_quit(conn: &xcb::Connection, state: &mut State) -> anyhow::Result<()> { - if let Some(mut unlock_dialog) = state.unlock_dialog.take() { - match unlock_dialog.child.try_wait() { - Err(err) => { - warn!("Failed to check unlock dialog's status: {}", err); - state.unlock_dialog = Some(unlock_dialog); - }, - Ok(Some(status)) if status.success() => { - info!("Authentication succeeded"); - unlock_screen(conn, state)?; - } - Ok(Some(status)) if status.signal().is_some() => { - if let Some(signum) = status.signal() { - warn!("Unlock dialog crashed with signal {}", signum); - } - hide_cursor(conn, state); - }, - Ok(Some(_)) => hide_cursor(conn, state), // auth failed, dialog has quit, do nothing - Ok(None) => state.unlock_dialog = Some(unlock_dialog), // dialog still running - } - } - Ok(()) -} - -fn start_unlock_dialog<'a>(conn: &'a xcb::Connection, state: &State<'a>, trigger_event: Option) -> anyhow::Result> { - let mut pointer_monitor = None; - for monitor in &state.monitors { - let cookie = conn.send_request(&x::QueryPointer { - window: monitor.root, - }); - let reply = conn.wait_for_reply(cookie)?; - let px = reply.root_x() as i32; - let py = reply.root_y() as i32; - if reply.same_screen() - && px >= monitor.x as i32 && px < monitor.x as i32 + monitor.width as i32 - && py >= monitor.y as i32 && py < monitor.y as i32 + monitor.height as i32 - { - pointer_monitor = Some(monitor); - break; - } - } - let pointer_monitor = pointer_monitor.unwrap_or_else(|| { - warn!("Unable to determine which monitor pointer is on; using first one"); - state.monitors.iter().nth(0).unwrap() - }); - - for monitor in &state.monitors { - monitor.show_cursor(conn); - } - - let trigger_event = match trigger_event { - Some(xcb::Event::X(x::Event::KeyPress(ev))) => match keysym_for_keypress(conn, &ev) { - Err(err) => { - warn!("Failed to get keysym for key press event: {}", err); - Some(xcb::Event::X(x::Event::KeyPress(ev))) - }, - Ok(Some(keysym)) if keysym == xkb::key::KP_Enter || keysym == xkb::key::ISO_Enter || keysym == xkb::key::Return || keysym == xkb::key::Escape => - // don't forward an or to the dialog, as that will make it activate/close immediately - None, - _ => Some(xcb::Event::X(x::Event::KeyPress(ev))), - }, - te => te, - }; - - let mut child = Command::new(format!("{}/{}", env!("HELPER_DIR"), state.config.dialog_backend.binary_name())) - .stdout(Stdio::piped()) - .spawn()?; - - let child_pidfd = child.create_pidfd()?; - let mut child_out = child.stdout.take().unwrap(); - - let mut xid_buf: [u8; 4] = [0; 4]; - 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 { x::Window::new(wid) } - }; - debug!("Dialog process created plug window 0x{:x}", client_window.resource_id()); - - let cookie = conn.send_request(&x::GetWindowAttributes { - window: client_window, - }); - let reply = conn.wait_for_reply(cookie)?; - if !reply.your_event_mask().contains(x::EventMask::STRUCTURE_NOTIFY) { - conn.send_and_check_request(&x::ChangeWindowAttributes { - window: client_window, - value_list: &[ - x::Cw::EventMask(reply.your_event_mask() | x::EventMask::STRUCTURE_NOTIFY), - ], - })?; - } - - let unlock_window = pointer_monitor.unlock_window; - let embedder = Embedder::start(conn, unlock_window, client_window)?; - - Ok(UnlockDialog { - monitor: *pointer_monitor, - embedder, - event_to_forward: trigger_event, - child, - child_pidfd, - }) -} - -fn hide_cursor(conn: &xcb::Connection, state: &State) { - for monitor in &state.monitors { - monitor.hide_cursor(conn); - } -} - -fn blank_screen(conn: &xcb::Connection, state: &mut State) -> anyhow::Result<()> { - if state.blanker_state >= BlankerState::Blanked { - return Ok(()) - } - - info!("Blanking"); - - for monitor in &state.monitors { - monitor.blank(conn)?; - } - - state.blanker_state = BlankerState::Blanked; - - Ok(()) -} - -fn unblank_screen(conn: &xcb::Connection, state: &mut State) -> anyhow::Result<()> { - if state.blanker_state != BlankerState::Blanked { - return Ok(()); - } - - info!("Unblanking"); - - for monitor in &state.monitors { - monitor.unblank(conn)?; - } - - state.blanker_state = BlankerState::Idle; - - Ok(()) -} - -fn lock_screen(conn: &xcb::Connection, state: &mut State) -> anyhow::Result<()> { - blank_screen(conn, state)?; - if state.blanker_state >= BlankerState::Locked { - return Ok(()); - } - - info!("Locking"); - - for monitor in &state.monitors { - monitor.lock(conn)?; - } - - state.blanker_state = BlankerState::Locked; - - Ok(()) -} - -fn unlock_screen(conn: &xcb::Connection, state: &mut State) -> anyhow::Result<()> { - if state.blanker_state != BlankerState::Locked { - return Ok(()); - } - - info!("Unlocking"); - - for monitor in &state.monitors { - monitor.unlock(conn)?; - } - - state.blanker_state = BlankerState::Blanked; - unblank_screen(conn, state)?; - - Ok(()) -} - -fn restart_daemon(state: &mut State, trigger: Option<(&xcb::Connection, &x::ClientMessageEvent)>) -> anyhow::Result<()> { +fn restart_daemon(screensaver: &mut Screensaver, subservices: &mut Subservices, trigger: Option<(&xcb::Connection, &x::ClientMessageEvent)>) -> anyhow::Result<()> { info!("Restarting"); let exe = read_link("/proc/self/exe")?; let exe = CString::new(exe.to_str().ok_or(io::Error::new(io::ErrorKind::InvalidData, "Path cannot be converted to str".to_string()))?)?; - let argv = match state.blanker_state { + let argv = match screensaver.blanker_state() { BlankerState::Idle => vec![], BlankerState::Blanked => vec![CString::new("--".to_string() + BLANKED_ARG)?], BlankerState::Locked => vec![CString::new("--".to_string() + LOCKED_ARG)?], }; - if let Err(err) = kill_child_processes(state) { - warn!("Failed to kill child processes: {}", err); - } + kill_child_processes(screensaver, subservices); match unsafe { fork() } { Err(err) => { error!("Failed to fork: {}", err); if let Some((conn, ev)) = trigger { - do_command_reply(conn, ev, false); + bscreensaver::send_command_reply(conn, ev, false); } Err(err)?; }, Ok(ForkResult::Parent { .. }) => { if let Some((conn, ev)) = trigger { - do_command_reply(conn, ev, true); + bscreensaver::send_command_reply(conn, ev, true); } exit(0); }, @@ -801,42 +319,16 @@ fn restart_daemon(state: &mut State, trigger: Option<(&xcb::Connection, &x::Clie Ok(()) } -fn exit_daemon(state: &mut State, trigger: Option<(&xcb::Connection, &x::ClientMessageEvent)>) -> anyhow::Result<()> { +fn exit_daemon(screensaver: &mut Screensaver, subservices: &mut Subservices, trigger: Option<(&xcb::Connection, &x::ClientMessageEvent)>) -> anyhow::Result<()> { info!("Quitting"); - if let Err(err) = kill_child_processes(state) { - warn!("Failed to kill child processes: {}", err); - } + kill_child_processes(screensaver, subservices); if let Some((conn, ev)) = trigger { - do_command_reply(conn, ev, true); + bscreensaver::send_command_reply(conn, ev, true); } exit(0); } -fn kill_child_processes(state: &mut State) -> anyhow::Result<()> { - if let Some(mut unlock_dialog) = state.unlock_dialog.take() { - unlock_dialog.embedder.end().ok(); - unlock_dialog.child.kill().ok(); - unlock_dialog.child.try_wait().ok(); - } - if let Some(mut dbus_service) = state.dbus_service.take() { - dbus_service.0.kill().ok(); - dbus_service.0.try_wait().ok(); - } - if let Some(mut systemd_service) = state.systemd_service.take() { - systemd_service.0.kill().ok(); - systemd_service.0.try_wait().ok(); - } - - Ok(()) -} - -fn keysym_for_keypress(conn: &xcb::Connection, ev: &x::KeyPressEvent) -> anyhow::Result> { - let ctx = xkb::Context::new(xkb::context::Flags::NO_FLAGS); - let device = xkb::x11::device(conn) - .map_err(|_| anyhow::anyhow!("Failed to get xkb device"))?; - let keymap = xkb::x11::keymap(conn, device, &ctx, xkb::keymap::compile::Flags::NO_FLAGS) - .map_err(|_| anyhow::anyhow!("Failed to get xkb keymap"))?; - let state = xkb::x11::state(conn, device, &keymap) - .map_err(|_| anyhow::anyhow!("Failed to get xkb state"))?; - Ok(state.key(ev.detail()).sym()) +fn kill_child_processes(screensaver: &mut Screensaver, subservices: &mut Subservices) { + screensaver.stop_unlock_dialog(); + subservices.stop_all(); } diff --git a/locker/src/screensaver.rs b/locker/src/screensaver.rs new file mode 100644 index 0000000..f369c49 --- /dev/null +++ b/locker/src/screensaver.rs @@ -0,0 +1,492 @@ +use log::{debug, error, info, trace, warn}; +use std::{ + io::Read, + os::unix::process::ExitStatusExt, + process::{Child, Command, Stdio}, + time::Instant, path::{PathBuf, Path}, +}; +use xcb::{randr, x, Xid, XidNew}; +use xcb_xembed::embedder::Embedder; + +use bscreensaver_command::{BCommand, create_command_window}; +use bscreensaver_util::{*, settings::Configuration}; + +use crate::{ + monitor::*, + pidfd::{CreatePidFd, PidFd}, +}; + +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +pub enum BlankerState { + Idle = 0, + Blanked = 1, + Locked = 2, +} + +struct UnlockDialog<'a> { + monitor: Monitor, + embedder: Embedder<'a>, + event_to_forward: Option, + child: Child, + child_pidfd: PidFd, +} + +impl<'a> Drop for UnlockDialog<'a> { + fn drop(&mut self) { + let _ = self.child.kill(); + let _ = self.child.try_wait(); + } +} + +type CommandHandler = dyn Fn(&mut Screensaver, &xcb::Connection, Option<&x::ClientMessageEvent>) -> anyhow::Result<()>; + +pub struct CommandHandlers<'a> { + pub restart_handler: &'a CommandHandler, + pub exit_handler: &'a CommandHandler, +} + +struct CommandAtoms { + blank: x::Atom, + lock: x::Atom, + deactivate: x::Atom, + restart: x::Atom, + exit: x::Atom, +} + +pub struct Screensaver<'a> { + config: &'a Configuration, + helper_dir: PathBuf, + command_atoms: CommandAtoms, + command_handlers: &'a CommandHandlers<'a>, + blanker_state: BlankerState, + monitors: Vec, + last_user_activity: Instant, + unlock_dialog: Option>, +} + +impl<'a> Screensaver<'a> { + pub fn new>(config: &'a Configuration, helper_dir: P, command_handlers: &'a CommandHandlers<'a>, conn: &xcb::Connection, screen: &x::Screen) -> anyhow::Result { + let _ = create_command_window(conn, screen)?; + let command_atoms = CommandAtoms { + blank: create_atom(&conn, BCommand::Blank.atom_name())?, + lock: create_atom(&conn, BCommand::Lock.atom_name())?, + deactivate: create_atom(&conn, BCommand::Deactivate.atom_name())?, + restart: create_atom(&conn, BCommand::Restart.atom_name())?, + exit: create_atom(&conn, BCommand::Exit.atom_name())?, + }; + + Ok(Self { + config, + helper_dir: helper_dir.as_ref().to_path_buf(), + command_atoms, + command_handlers, + blanker_state: BlankerState::Idle, + monitors: Monitor::set_up_all(&conn)?, + last_user_activity: Instant::now(), + unlock_dialog: None, + }) + } + + pub fn blanker_state(&self) -> BlankerState { + self.blanker_state + } + + pub fn last_user_activity(&self) -> Instant { + self.last_user_activity + } + + pub fn unlock_dialog_pidfd(&self) -> Option<&PidFd> { + self.unlock_dialog.as_ref().map(|ud| &ud.child_pidfd) + } + + pub fn handle_xcb_events(&mut self, conn: &'a xcb::Connection) -> anyhow::Result<()> { + loop { + if let Some(event) = conn.poll_for_event()? { + let embedder_handled = if let Some(mut unlock_dialog) = self.unlock_dialog.take() { + match unlock_dialog.embedder.event(&event) { + Err(err) => { + // XXX: should we assume unlock dialog is dead here? + warn!("Error sending event to unlock dialog: {}", err); + false + }, + Ok(handled) => { + self.unlock_dialog = Some(unlock_dialog); + handled + }, + } + } else { + false + }; + + // Allow the code below to handle key presses regardless of whether or not + // the embedder handled it. + let embedder_handled = match event { + xcb::Event::X(x::Event::KeyPress(_)) => false, + _ => embedder_handled, + }; + + if !embedder_handled { + match event { + xcb::Event::RandR(randr::Event::Notify(ev)) => { + debug!("Got xrandr notify event: {:#?}", ev); + for monitor in &self.monitors { + destroy_window(&conn, monitor.unlock_window)?; + destroy_window(&conn, monitor.blanker_window)?; + destroy_gc(&conn, monitor.black_gc)?; + } + self.monitors = Monitor::set_up_all(conn)?; + match self.blanker_state { + BlankerState::Idle => (), + BlankerState::Blanked => { + self.blanker_state = BlankerState::Idle; + self.blank_screen(conn)?; + }, + BlankerState::Locked => { + self.blanker_state = BlankerState::Idle; + self.lock_screen(conn)?; + }, + } + }, + xcb::Event::Input(_) => { + // TODO: implement some sort of hysteresis/debouncing for mouse motion + self.last_user_activity = Instant::now(); + }, + xcb::Event::X(x::Event::ClientMessage(ev)) => { + let res = match ev.r#type() { + b if b == self.command_atoms.blank => Some(self.blank_screen(conn)), + l if l == self.command_atoms.lock => Some(self.lock_screen(conn)), + d if d == self.command_atoms.deactivate => { + self.last_user_activity = Instant::now(); + match self.blanker_state { + BlankerState::Idle => Some(Ok(())), + BlankerState::Blanked => Some(self.unblank_screen(conn)), + BlankerState::Locked => { + if self.unlock_dialog.is_none() { + match self.start_unlock_dialog(conn, None) { + Ok(unlock_dialog) => { + self.unlock_dialog = Some(unlock_dialog); + Some(Ok(())) + }, + Err(err) => Some(Err(err)), + } + } else { + Some(Ok(())) + } + }, + } + }, + r if r == self.command_atoms.restart => Some((self.command_handlers.restart_handler)(self, conn, Some(&ev))), + e if e == self.command_atoms.exit => Some((self.command_handlers.exit_handler)(self, conn, Some(&ev))), + _ => 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 + }; + crate::send_command_reply(conn, &ev, is_success); + } + }, + xcb::Event::X(x::Event::MapNotify(ev)) if ev.window() == self.unlock_dialog_window() => { + debug!("Unlock dialog mapped, requesting focus"); + if let Some(ref mut unlock_dialog) = &mut self.unlock_dialog { + let _ = unlock_dialog.embedder.activate_client(); + if let Err(err) = unlock_dialog.embedder.focus_client(xcb_xembed::XEmbedFocus::Current) { + warn!("Failed to focus unlock dialog: {}", err); + } + if let Some(event_to_forward) = unlock_dialog.event_to_forward.take() { + let _ = unlock_dialog.embedder.event(&event_to_forward); + } + } + }, + xcb::Event::X(x::Event::ConfigureNotify(ev)) if ev.window() == self.embedder_window() => { + if let Some(unlock_dialog) = &self.unlock_dialog { + let monitor = &unlock_dialog.monitor; + let x = std::cmp::max(0, monitor.x as i32 + monitor.width as i32 / 2 - ev.width() as i32 / 2); + let y = std::cmp::max(0, monitor.y as i32 + monitor.height as i32 / 2 - ev.height() as i32 / 2); + if x != ev.x() as i32 || y != ev.y() as i32 { + conn.send_and_check_request(&x::ConfigureWindow { + window: unlock_dialog.embedder.embedder_window(), + value_list: &[ + x::ConfigWindow::X(x), + x::ConfigWindow::Y(y), + ], + })?; + } + } + }, + xcb::Event::X(x::Event::Expose(ev)) => { + if let Some(monitor) = self.monitors.iter().find(|m| ev.window() == m.blanker_window || ev.window() == m.unlock_window) { + debug!("got expose for {} at {}x{}+{}+{}", ev.window().resource_id(), ev.width(), ev.height(), ev.x(), ev.y()); + conn.send_and_check_request(&x::PolyFillRectangle { + drawable: x::Drawable::Window(ev.window()), + gc: monitor.black_gc, + rectangles: &[ + x::Rectangle { + x: ev.x() as i16, + y: ev.y() as i16, + width: ev.width(), + height: ev.height(), + }, + ], + })?; + } + }, + ev @ xcb::Event::X(x::Event::MotionNotify(_)) => self.handle_user_activity(conn, ev)?, + xcb::Event::X(x::Event::KeyPress(ev)) => { + if let Err(err) = keysym_for_keypress(conn, &ev).and_then(|keysym| match keysym { + Some(keysym) if keysym == xkb::key::XF86MonBrightnessUp && self.config.handle_brightness_keys => self.monitors.iter().map(|monitor| monitor.brightness_up(conn)).collect(), + Some(keysym) if keysym == xkb::key::XF86MonBrightnessDown && self.config.handle_brightness_keys => self.monitors.iter().map(|monitor| monitor.brightness_down(conn)).collect(), + _ => Ok(()), + }) { + warn!("Failed to handle key press {}: {}", ev.detail(), err); + } + self.handle_user_activity(conn, xcb::Event::X(x::Event::KeyPress(ev)))?; + }, + ev => trace!("Got other event: {:#?}", ev), + } + } + } else { + break; + } + } + + Ok(()) + } + + fn handle_user_activity(&mut self, conn: &'a xcb::Connection, ev: xcb::Event) -> anyhow::Result<()> { + match self.blanker_state { + BlankerState::Idle => Ok(()), + BlankerState::Blanked => self.unblank_screen(conn), + BlankerState::Locked => match &self.unlock_dialog { + None => { + self.unlock_dialog = match self.start_unlock_dialog(&conn, Some(ev)) { + Err(err) => { + error!("Unable to start unlock dialog: {}", err); + None + }, + Ok(unlock_dialog) => Some(unlock_dialog), + }; + Ok(()) + }, + Some(unlock_dialog) => { + let mut cookies = Vec::new(); + for win in [unlock_dialog.monitor.blanker_window, unlock_dialog.embedder.embedder_window(), unlock_dialog.embedder.client_window()] { + cookies.push(conn.send_request_checked(&x::ConfigureWindow { + window: win, + value_list: &[ + x::ConfigWindow::StackMode(x::StackMode::Above), + ], + })); + } + for cookie in cookies { + conn.check_request(cookie)?; + } + Ok(()) + }, + } + } + } + + fn embedder_window(&self) -> x::Window { + self.unlock_dialog.as_ref().map(|ud| ud.embedder.embedder_window()).unwrap_or(x::WINDOW_NONE) + } + + fn unlock_dialog_window(&self) -> x::Window { + self.unlock_dialog.as_ref().map(|ud| ud.embedder.client_window()).unwrap_or(x::WINDOW_NONE) + } + + pub fn handle_unlock_dialog_quit(&mut self, conn: &xcb::Connection) -> anyhow::Result<()> { + if let Some(mut unlock_dialog) = self.unlock_dialog.take() { + match unlock_dialog.child.try_wait() { + Err(err) => { + warn!("Failed to check unlock dialog's status: {}", err); + self.unlock_dialog = Some(unlock_dialog); + }, + Ok(Some(status)) if status.success() => { + info!("Authentication succeeded"); + self.unlock_screen(conn)?; + } + Ok(Some(status)) if status.signal().is_some() => { + if let Some(signum) = status.signal() { + warn!("Unlock dialog crashed with signal {}", signum); + } + self.hide_cursor(conn); + }, + Ok(Some(_)) => self.hide_cursor(conn), // auth failed, dialog has quit, do nothing + Ok(None) => self.unlock_dialog = Some(unlock_dialog), // dialog still running + } + } + Ok(()) + } + + fn start_unlock_dialog(&self, conn: &'a xcb::Connection, trigger_event: Option) -> anyhow::Result> { + let mut pointer_monitor = None; + for monitor in &self.monitors { + let cookie = conn.send_request(&x::QueryPointer { + window: monitor.root, + }); + let reply = conn.wait_for_reply(cookie)?; + let px = reply.root_x() as i32; + let py = reply.root_y() as i32; + if reply.same_screen() + && px >= monitor.x as i32 && px < monitor.x as i32 + monitor.width as i32 + && py >= monitor.y as i32 && py < monitor.y as i32 + monitor.height as i32 + { + pointer_monitor = Some(monitor); + break; + } + } + let pointer_monitor = pointer_monitor.unwrap_or_else(|| { + warn!("Unable to determine which monitor pointer is on; using first one"); + self.monitors.iter().nth(0).unwrap() + }); + + for monitor in &self.monitors { + monitor.show_cursor(conn); + } + + let trigger_event = match trigger_event { + Some(xcb::Event::X(x::Event::KeyPress(ev))) => match keysym_for_keypress(conn, &ev) { + Err(err) => { + warn!("Failed to get keysym for key press event: {}", err); + Some(xcb::Event::X(x::Event::KeyPress(ev))) + }, + Ok(Some(keysym)) if keysym == xkb::key::KP_Enter || keysym == xkb::key::ISO_Enter || keysym == xkb::key::Return || keysym == xkb::key::Escape => + // don't forward an or to the dialog, as that will make it activate/close immediately + None, + _ => Some(xcb::Event::X(x::Event::KeyPress(ev))), + }, + te => te, + }; + + let mut child = Command::new(self.helper_dir.join(self.config.dialog_backend.binary_name())) + .stdout(Stdio::piped()) + .spawn()?; + + let child_pidfd = child.create_pidfd()?; + let mut child_out = child.stdout.take().unwrap(); + + let mut xid_buf: [u8; 4] = [0; 4]; + 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 { x::Window::new(wid) } + }; + debug!("Dialog process created plug window 0x{:x}", client_window.resource_id()); + + let cookie = conn.send_request(&x::GetWindowAttributes { + window: client_window, + }); + let reply = conn.wait_for_reply(cookie)?; + if !reply.your_event_mask().contains(x::EventMask::STRUCTURE_NOTIFY) { + conn.send_and_check_request(&x::ChangeWindowAttributes { + window: client_window, + value_list: &[ + x::Cw::EventMask(reply.your_event_mask() | x::EventMask::STRUCTURE_NOTIFY), + ], + })?; + } + + let unlock_window = pointer_monitor.unlock_window; + let embedder = Embedder::start(conn, unlock_window, client_window)?; + + Ok(UnlockDialog { + monitor: *pointer_monitor, + embedder, + event_to_forward: trigger_event, + child, + child_pidfd, + }) + } + + pub fn stop_unlock_dialog(&mut self) { + let _ = self.unlock_dialog.take(); + } + + pub fn blank_screen(&mut self, conn: &xcb::Connection) -> anyhow::Result<()> { + if self.blanker_state >= BlankerState::Blanked { + return Ok(()) + } + + info!("Blanking"); + + for monitor in &self.monitors { + monitor.blank(conn)?; + } + + self.blanker_state = BlankerState::Blanked; + + Ok(()) + } + + fn unblank_screen(&mut self, conn: &xcb::Connection) -> anyhow::Result<()> { + if self.blanker_state != BlankerState::Blanked { + return Ok(()); + } + + info!("Unblanking"); + + for monitor in &self.monitors { + monitor.unblank(conn)?; + } + + self.blanker_state = BlankerState::Idle; + + Ok(()) + } + + pub fn lock_screen(&mut self, conn: &xcb::Connection) -> anyhow::Result<()> { + self.blank_screen(conn)?; + if self.blanker_state >= BlankerState::Locked { + return Ok(()); + } + + info!("Locking"); + + for monitor in &self.monitors { + monitor.lock(conn)?; + } + + self.blanker_state = BlankerState::Locked; + + Ok(()) + } + + fn unlock_screen(&mut self, conn: &xcb::Connection) -> anyhow::Result<()> { + if self.blanker_state != BlankerState::Locked { + return Ok(()); + } + + info!("Unlocking"); + + for monitor in &self.monitors { + monitor.unlock(conn)?; + } + + self.blanker_state = BlankerState::Blanked; + self.unblank_screen(conn)?; + + Ok(()) + } + + fn hide_cursor(&self, conn: &xcb::Connection) { + for monitor in &self.monitors { + monitor.hide_cursor(conn); + } + } +} + +fn keysym_for_keypress(conn: &xcb::Connection, ev: &x::KeyPressEvent) -> anyhow::Result> { + let ctx = xkb::Context::new(xkb::context::Flags::NO_FLAGS); + let device = xkb::x11::device(conn) + .map_err(|_| anyhow::anyhow!("Failed to get xkb device"))?; + let keymap = xkb::x11::keymap(conn, device, &ctx, xkb::keymap::compile::Flags::NO_FLAGS) + .map_err(|_| anyhow::anyhow!("Failed to get xkb keymap"))?; + let state = xkb::x11::state(conn, device, &keymap) + .map_err(|_| anyhow::anyhow!("Failed to get xkb state"))?; + Ok(state.key(ev.detail()).sym()) +} diff --git a/locker/src/subservice.rs b/locker/src/subservice.rs new file mode 100644 index 0000000..90d9950 --- /dev/null +++ b/locker/src/subservice.rs @@ -0,0 +1,100 @@ +use log::{trace, warn}; +use std::{ + os::unix::process::ExitStatusExt, + process::{Child, Command}, path::{Path, PathBuf}, +}; + +use crate::pidfd::{CreatePidFd, PidFd}; + +pub struct Subservice { + path: PathBuf, + child: Child, + pidfd: PidFd, +} + +impl Subservice { + pub fn start, S: AsRef>(helper_dir: P, binary_name: S) -> anyhow::Result { + let path = helper_dir.as_ref().join(binary_name.as_ref()); + Self::start_internal(path) + } + + fn start_internal>(path: P) -> anyhow::Result { + let child = Command::new(path.as_ref()) + .spawn()?; + let pidfd = child.create_pidfd()?; + + Ok(Self { + path: path.as_ref().to_path_buf(), + child, + pidfd, + }) + } + + pub fn pidfd(&self) -> &PidFd { + &self.pidfd + } + + pub fn handle_quit(mut self) -> anyhow::Result> { + if let Some(status) = self.child.try_wait().ok().flatten() { + if !status.success() { + warn!("{} service exited abnormally ({}{}); restarting", self.path.to_string_lossy(), + status.code().map(|c| format!("code {}", c)).unwrap_or("".to_string()), + status.signal().map(|s| format!("signal {}", s)).unwrap_or("".to_string()) + ); + Self::start_internal(&self.path).map(Some) + } else { + trace!("{} service exited normally", self.path.to_string_lossy()); + Ok(None) + } + } else { + trace!("{} service didn't seem to actually quit", self.path.to_string_lossy()); + Ok(Some(self)) + } + } +} + +impl Drop for Subservice { + fn drop(&mut self) { + let _ = self.child.kill(); + let _ = self.child.try_wait(); + } +} + +pub struct Subservices { + dbus_service: Option, + systemd_service: Option, +} + +impl Subservices { + pub fn start_all>(helper_dir: P) -> anyhow::Result { + let dbus_service = Subservice::start(helper_dir.as_ref(), "bscreensaver-dbus-service")?; + let systemd_service = Subservice::start(helper_dir, "bscreensaver-systemd")?; + Ok(Self { + dbus_service: Some(dbus_service), + systemd_service: Some(systemd_service), + }) + } + + pub fn stop_all(&mut self) { + let _ = self.dbus_service.take(); + let _ = self.systemd_service.take(); + } + + pub fn dbus_service(&self) -> Option<&Subservice> { + self.dbus_service.as_ref() + } + + pub fn systemd_service(&self) -> Option<&Subservice> { + self.systemd_service.as_ref() + } + + pub fn handle_quit(&mut self) -> anyhow::Result<()> { + if let Some(dbus_service) = self.dbus_service.take() { + self.dbus_service = dbus_service.handle_quit()?; + } + if let Some(systemd_service) = self.systemd_service.take() { + self.systemd_service = systemd_service.handle_quit()?; + } + Ok(()) + } +} diff --git a/xcb-xembed/src/embedder.rs b/xcb-xembed/src/embedder.rs index 7fc2d21..1114cca 100644 --- a/xcb-xembed/src/embedder.rs +++ b/xcb-xembed/src/embedder.rs @@ -95,8 +95,12 @@ impl<'a> Embedder<'a> { flags: info.flags, }) } + + pub fn end(mut self) -> Result<(), Error> { + self.end_internal() + } - pub fn end(self) -> Result<(), Error> { + fn end_internal(&mut self) -> Result<(), Error> { if self.client == x::WINDOW_NONE { return Err(Error::ClientDestroyed); } @@ -290,6 +294,12 @@ impl<'a> Embedder<'a> { } } +impl<'a> Drop for Embedder<'a> { + fn drop(&mut self) { + let _ = self.end_internal(); + } +} + fn fetch_xembed_info(conn: &xcb::Connection, client: x::Window) -> Result { let xembed_info_atom = intern_atom(conn, XEMBED_INFO_ATOM_NAME)?; let cookie = conn.send_request(&x::GetProperty {