Major refactor of locker
Moves the meat of the screensaver into its own file, and separates out the subservice stuff.
This commit is contained in:
parent
611e3b8dd0
commit
63a176c26e
28
locker/src/lib.rs
Normal file
28
locker/src/lib.rs
Normal file
@ -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");
|
||||
}
|
||||
}
|
@ -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<xcb::Event>,
|
||||
child: Child,
|
||||
child_pidfd: PidFd,
|
||||
}
|
||||
|
||||
struct State<'a> {
|
||||
config: Configuration,
|
||||
monitors: Vec<Monitor>,
|
||||
dbus_service: Option<(Child, PidFd)>,
|
||||
systemd_service: Option<(Child, PidFd)>,
|
||||
last_user_activity: Instant,
|
||||
blanker_state: BlankerState,
|
||||
unlock_dialog: Option<UnlockDialog<'a>>,
|
||||
}
|
||||
|
||||
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<F>(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<xcb::Event>) -> anyhow::Result<UnlockDialog<'a>> {
|
||||
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 <enter> or <esc> 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<Option<xkb::Keysym>> {
|
||||
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();
|
||||
}
|
||||
|
492
locker/src/screensaver.rs
Normal file
492
locker/src/screensaver.rs
Normal file
@ -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<xcb::Event>,
|
||||
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<Monitor>,
|
||||
last_user_activity: Instant,
|
||||
unlock_dialog: Option<UnlockDialog<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Screensaver<'a> {
|
||||
pub fn new<P: AsRef<Path>>(config: &'a Configuration, helper_dir: P, command_handlers: &'a CommandHandlers<'a>, conn: &xcb::Connection, screen: &x::Screen) -> anyhow::Result<Self> {
|
||||
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<xcb::Event>) -> anyhow::Result<UnlockDialog<'a>> {
|
||||
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 <enter> or <esc> 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<Option<xkb::Keysym>> {
|
||||
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())
|
||||
}
|
100
locker/src/subservice.rs
Normal file
100
locker/src/subservice.rs
Normal file
@ -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<P: AsRef<Path>, S: AsRef<str>>(helper_dir: P, binary_name: S) -> anyhow::Result<Self> {
|
||||
let path = helper_dir.as_ref().join(binary_name.as_ref());
|
||||
Self::start_internal(path)
|
||||
}
|
||||
|
||||
fn start_internal<P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
|
||||
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<Option<Self>> {
|
||||
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<Subservice>,
|
||||
systemd_service: Option<Subservice>,
|
||||
}
|
||||
|
||||
impl Subservices {
|
||||
pub fn start_all<P: AsRef<Path>>(helper_dir: P) -> anyhow::Result<Subservices> {
|
||||
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(())
|
||||
}
|
||||
}
|
@ -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<XEmbedInfo, Error> {
|
||||
let xembed_info_atom = intern_atom(conn, XEMBED_INFO_ATOM_NAME)?;
|
||||
let cookie = conn.send_request(&x::GetProperty {
|
||||
|
Loading…
Reference in New Issue
Block a user