#![feature(linux_pidfd)] #![feature(option_result_contains)] use clap::{Arg, Command as ClapCommand}; use log::{debug, error, info, trace, warn}; use nix::{ poll::{poll, PollFd, PollFlags}, unistd::{execv, fork, setsid, ForkResult}, sys::{ signal::{sigprocmask, SigSet, SigmaskHow, Signal}, signalfd::{SignalFd, SfdFlags}, }, }; use std::{ env, ffi::CString, fs::read_link, io::{self, Read}, os::{ linux::process::{ChildExt, CommandExt, PidFd}, unix::io::AsRawFd, unix::process::ExitStatusExt, }, process::{exit, Child, Command, Stdio}, time::{Duration, Instant}, }; use xcb::{randr, x, xinput, Xid}; use xcb_xembed::embedder::Embedder; use bscreensaver_command::{BCommand, create_command_window}; use bscreensaver_util::{*, settings::Configuration}; const BLANKED_ARG: &str = "blanked"; const LOCKED_ARG: &str = "locked"; #[derive(Clone, Copy)] struct Monitor { pub root: x::Window, pub black_gc: x::Gcontext, pub blanker_window: x::Window, pub unlock_window: x::Window, pub x: i16, pub y: i16, pub width: u16, pub height: u16, } #[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 { $pfds.push(PollFd::new(fd, PollFlags::POLLIN)); Some(fd) } else { None } }; } fn main() -> anyhow::Result<()> { init_logging("BSCREENSAVER_LOG"); let config = Configuration::load()?; let args = ClapCommand::new("Blanks and locks the screen after a period of time") .author(env!("CARGO_PKG_AUTHORS")) .version(env!("CARGO_PKG_VERSION")) .arg( Arg::new("blanked") .long(BLANKED_ARG) .help("Starts up in the blanked state") ) .arg( Arg::new("locked") .long(LOCKED_ARG) .help("Starts up in the blanked and locked state") ) .get_matches(); let mut signal_fd = init_signals()?; let (conn, screen_num) = xcb::Connection::connect_with_extensions( None, &[xcb::Extension::RandR, xcb::Extension::XFixes, xcb::Extension::Input], &[] )?; let setup = conn.get_setup(); let screen = setup.roots().nth(screen_num as usize).unwrap(); init_xinput(&conn)?; init_randr(&conn)?; 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 mut state = State { config, monitors: create_blanker_windows(&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)?; if args.is_present(LOCKED_ARG) { match lock_screen(&conn, &mut state) { 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) { Err(err) => warn!("Possibly failed to blank screen on startup: {}", err), Ok(_) => debug!("Got --{} arg; screen locked on startup", BLANKED_ARG), } } let _ = conn.send_and_check_request(&x::SetScreenSaver { timeout: 0, interval: 0, prefer_blanking: x::Blanking::NotPreferred, allow_exposures: x::Exposures::NotAllowed, }); loop { if let Err(err) = handle_xcb_events(&conn, &mut state, &command_atoms) { if conn.has_error().is_err() { error!("Lost connection to X server; attempting to restart"); restart_daemon(&mut state)?; } warn!("Error handling event: {}", err); } 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 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), BlankerState::Locked => None, }; let poll_timeout = poll_timeout.map(|pt| if pt.as_millis() > i32::MAX as u128 { i32::MAX } else { pt.as_millis() as i32 }).unwrap_or(-1); trace!("about to poll (timeout={})", poll_timeout); let nready = poll(pfds.as_mut_slice(), poll_timeout)?; // FIXME: maybe shouldn't quit here on errors if screen is locked trace!("polled; {} FD ready", nready); if nready > 0 { 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 dbus_service_fd.contains(&fd) => handle_subservice_quit(state.dbus_service.take(), "DBus", || start_dbus_service(&mut state)), fd if systemd_service_fd.contains(&fd) => handle_subservice_quit(state.systemd_service.take(), "systemd", || start_systemd_service(&mut state)), fd if dialog_fd.contains(&fd) => handle_unlock_dialog_quit(&conn, &mut state), _ => 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)?; } warn!("Error handling event: {}", err); } } } } let since_last_activity = Instant::now().duration_since(state.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) { 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) { error!("POSSIBLY FAILED TO LOCK SCREEN: {}", err); } } } } fn init_signals() -> anyhow::Result { let sigs = { let mut s = SigSet::empty(); s.add(Signal::SIGHUP); s.add(Signal::SIGINT); s.add(Signal::SIGQUIT); s.add(Signal::SIGTERM); s }; sigprocmask(SigmaskHow::SIG_BLOCK, Some(&sigs), None)?; let flags = SfdFlags::SFD_NONBLOCK | SfdFlags::SFD_CLOEXEC; let fd = SignalFd::with_flags(&sigs, flags)?; Ok(fd) } fn init_xinput(conn: &xcb::Connection) -> xcb::Result<()> { let cookie = conn.send_request(&xcb::xinput::XiQueryVersion { major_version: 2, minor_version: 2, }); let reply = conn.wait_for_reply(cookie)?; if reply.major_version() < 2 { error!("Version 2 or greater of the Xinput extension is required (got {}.{})", reply.major_version(), reply.minor_version()); exit(1); } let mut cookies = Vec::new(); for screen in conn.get_setup().roots() { cookies.push(conn.send_request_checked(&xinput::XiSelectEvents { window: screen.root(), masks: &[ xinput::EventMaskBuf::new( xinput::Device::AllMaster, &[ xinput::XiEventMask::RAW_KEY_PRESS | xinput::XiEventMask::RAW_KEY_RELEASE | xinput::XiEventMask::RAW_BUTTON_PRESS | xinput::XiEventMask::RAW_BUTTON_RELEASE | xinput::XiEventMask::RAW_TOUCH_BEGIN | xinput::XiEventMask::RAW_TOUCH_UPDATE | xinput::XiEventMask::RAW_TOUCH_END | xinput::XiEventMask::RAW_MOTION ] ) ], })); } for cookie in cookies { conn.check_request(cookie)?; } Ok(()) } fn init_randr(conn: &xcb::Connection) -> xcb::Result<()> { for screen in conn.get_setup().roots() { conn.send_and_check_request(&randr::SelectInput { window: screen.root(), enable: randr::NotifyMask::SCREEN_CHANGE | randr::NotifyMask::CRTC_CHANGE | randr::NotifyMask::OUTPUT_CHANGE, })?; } Ok(()) } fn create_blanker_windows(conn: &xcb::Connection) -> xcb::Result> { let mut cookies = Vec::new(); let mut monitors = Vec::new(); for screen in conn.get_setup().roots() { let cookie = conn.send_request(&randr::GetScreenResources { window: screen.root(), }); let reply = conn.wait_for_reply(cookie)?; let config_timestamp = reply.config_timestamp(); for output in reply.outputs() { let cookie = conn.send_request(&randr::GetOutputInfo { output: *output, config_timestamp, }); let reply = conn.wait_for_reply(cookie)?; if !reply.crtc().is_none() { let cookie = conn.send_request(&randr::GetCrtcInfo { crtc: reply.crtc(), config_timestamp, }); let reply = conn.wait_for_reply(cookie)?; let blanker_window: x::Window = conn.generate_id(); let unlock_window: x::Window = conn.generate_id(); debug!("creating blanker window 0x{:x}, {}x{}+{}+{}; unlock window 0x{:x}", blanker_window.resource_id(), reply.width(), reply.height(), reply.x(), reply.y(), unlock_window.resource_id()); cookies.push(conn.send_request_checked(&x::CreateWindow { depth: x::COPY_FROM_PARENT as u8, wid: blanker_window, parent: screen.root(), x: reply.x(), y: reply.y(), width: reply.width(), height: reply.height(), border_width: 0, class: x::WindowClass::InputOutput, visual: x::COPY_FROM_PARENT, value_list: &[ x::Cw::BackPixel(screen.black_pixel()), x::Cw::BorderPixel(screen.black_pixel()), x::Cw::OverrideRedirect(true), x::Cw::SaveUnder(true), x::Cw::EventMask(x::EventMask::KEY_PRESS | x::EventMask::KEY_RELEASE | x::EventMask::BUTTON_PRESS | x::EventMask::BUTTON_RELEASE | x::EventMask::POINTER_MOTION | x::EventMask::POINTER_MOTION_HINT | x::EventMask::EXPOSURE), ], })); cookies.push(conn.send_request_checked(&x::ChangeProperty { mode: x::PropMode::Replace, window: blanker_window, property: x::ATOM_WM_NAME, r#type: x::ATOM_STRING, data: b"bscreensaver blanker window", })); cookies.push(conn.send_request_checked(&x::ChangeProperty { mode: x::PropMode::Replace, window: blanker_window, property: x::ATOM_WM_CLASS, r#type: x::ATOM_STRING, data: BSCREENSAVER_WM_CLASS, })); cookies.push(conn.send_request_checked(&x::CreateWindow { depth: x::COPY_FROM_PARENT as u8, wid: unlock_window, parent: blanker_window, x: 0, y: 0, width: 1, height: 1, border_width: 0, class: x::WindowClass::InputOutput, visual: x::COPY_FROM_PARENT, value_list: &[ x::Cw::BackPixel(screen.black_pixel()), x::Cw::BorderPixel(screen.black_pixel()), x::Cw::OverrideRedirect(true), x::Cw::SaveUnder(true), x::Cw::EventMask(x::EventMask::KEY_PRESS | x::EventMask::KEY_RELEASE | x::EventMask::BUTTON_PRESS | x::EventMask::BUTTON_RELEASE | x::EventMask::POINTER_MOTION | x::EventMask::POINTER_MOTION_HINT | x::EventMask::STRUCTURE_NOTIFY | x::EventMask::EXPOSURE), ], })); cookies.push(conn.send_request_checked(&x::ChangeProperty { mode: x::PropMode::Replace, window: unlock_window, property: x::ATOM_WM_NAME, r#type: x::ATOM_STRING, data: b"bscreensaver unlock dialog socket window", })); cookies.push(conn.send_request_checked(&x::ChangeProperty { mode: x::PropMode::Replace, window: unlock_window, property: x::ATOM_WM_CLASS, r#type: x::ATOM_STRING, data: BSCREENSAVER_WM_CLASS, })); let black_gc: x::Gcontext = conn.generate_id(); cookies.push(conn.send_request_checked(&x::CreateGc { cid: black_gc, drawable: x::Drawable::Window(screen.root()), value_list: &[ x::Gc::Foreground(screen.black_pixel()), x::Gc::Background(screen.black_pixel()), ], })); monitors.push(Monitor { root: screen.root(), black_gc, blanker_window, unlock_window, x: reply.x(), y: reply.y(), width: reply.width(), height: reply.height(), }); } } } for cookie in cookies { conn.check_request(cookie)?; } Ok(monitors) } fn start_subservice(binary_name: &str) -> anyhow::Result<(Child, PidFd)> { let mut child = Command::new(format!("{}/{}", env!("HELPER_DIR"), binary_name)) .create_pidfd(true) .spawn()?; let pidfd = child.take_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<()> { match signal_fd.read_signal()? { None => (), Some(info) if info.ssi_signo == Signal::SIGHUP as u32 => restart_daemon(state)?, Some(info) if info.ssi_signo == Signal::SIGINT as u32 => exit_daemon(state)?, Some(info) if info.ssi_signo == Signal::SIGQUIT as u32 => exit_daemon(state)?, Some(info) if info.ssi_signo == Signal::SIGTERM as u32 => exit_daemon(state)?, 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 }; 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 = create_blanker_windows(&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)) => 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)?); }, } }, 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"); 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(_)) | ev @ xcb::Event::X(x::Event::KeyPress(_)) => match state.blanker_state { BlankerState::Idle => (), 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), }, 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)?; } }, } }, ev => trace!("Got other event: {:#?}", ev), } } } else { break; } } Ok(()) } 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); } }, Ok(Some(_)) => (), // 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)?; if reply.same_screen() { 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() }); let mut child = Command::new(format!("{}/{}", env!("HELPER_DIR"), state.config.dialog_backend.binary_name())) .create_pidfd(true) .stdout(Stdio::piped()) .spawn()?; let mut child_out = child.stdout.take().unwrap(); let child_pidfd = child.take_pidfd()?; 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 { use xcb::XidNew; 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 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 { let mut cookies = Vec::new(); cookies.push(conn.send_request_checked(&x::ConfigureWindow { window: monitor.blanker_window, value_list: &[ x::ConfigWindow::StackMode(x::StackMode::Above), ], })); cookies.push(conn.send_request_checked(&x::MapWindow { window: monitor.blanker_window, })); for cookie in cookies { conn.check_request(cookie)?; } } 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"); let mut cookies = Vec::new(); for monitor in &state.monitors { cookies.push(conn.send_request_checked(&x::UnmapWindow { window: monitor.blanker_window, })); } for cookie in cookies { conn.check_request(cookie)?; } 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 { let mut cookies = Vec::new(); cookies.push(conn.send_request_checked(&x::ConfigureWindow { window: monitor.unlock_window, value_list: &[ x::ConfigWindow::StackMode(x::StackMode::Above), ], })); cookies.push(conn.send_request_checked(&x::MapWindow { window: monitor.unlock_window, })); for cookie in cookies { conn.check_request(cookie)?; } let cookie = conn.send_request(&x::GrabKeyboard { owner_events: true, grab_window: monitor.unlock_window, time: x::CURRENT_TIME, pointer_mode: x::GrabMode::Async, keyboard_mode: x::GrabMode::Async, }); let reply = conn.wait_for_reply(cookie)?; if reply.status() != x::GrabStatus::Success { // FIXME: try to grab later? warn!("Failed to grab keyboard on window {:?}: {:?}", monitor.blanker_window, reply.status()); } let cookie = conn.send_request(&x::GrabPointer { owner_events: true, grab_window: monitor.unlock_window, event_mask: x::EventMask::BUTTON_PRESS | x::EventMask::BUTTON_RELEASE | x::EventMask::POINTER_MOTION | x::EventMask::POINTER_MOTION_HINT, pointer_mode: x::GrabMode::Async, keyboard_mode: x::GrabMode::Async, confine_to: monitor.blanker_window, cursor: x::CURSOR_NONE, time: x::CURRENT_TIME, }); let reply = conn.wait_for_reply(cookie)?; if reply.status() != x::GrabStatus::Success { // FIXME: try to grab later? warn!("Failed to grab pointer on window {:?}: {:?}", monitor.blanker_window, reply.status()); } } 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"); let mut cookies = Vec::new(); for monitor in &state.monitors { cookies.push(conn.send_request_checked(&x::UngrabKeyboard { time: x::CURRENT_TIME, })); cookies.push(conn.send_request_checked(&x::UngrabPointer { time: x::CURRENT_TIME, })); cookies.push(conn.send_request_checked(&x::UnmapWindow { window: monitor.unlock_window, })); } for cookie in cookies { conn.check_request(cookie)?; } state.blanker_state = BlankerState::Blanked; unblank_screen(conn, state)?; Ok(()) } fn restart_daemon(state: &mut State) -> 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 { 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); } match unsafe { fork() } { Err(err) => { error!("Failed to fork: {}", err); Err(err)?; }, Ok(ForkResult::Parent { .. }) => exit(0), Ok(ForkResult::Child) => { if let Err(err) = setsid() { warn!("Failed to start new session: {}", err); } execv(exe.as_c_str(), &argv)?; }, } Ok(()) } fn exit_daemon(state: &mut State) -> anyhow::Result<()> { info!("Quitting"); if let Err(err) = kill_child_processes(state) { warn!("Failed to kill child processes: {}", err); } 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(()) }