1001 lines
39 KiB
Rust
1001 lines
39 KiB
Rust
mod pidfd;
|
|
|
|
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::{
|
|
unix::io::AsRawFd,
|
|
unix::process::ExitStatusExt,
|
|
},
|
|
process::{exit, Child, Command, Stdio},
|
|
time::{Duration, Instant},
|
|
};
|
|
use xcb::{randr, x, xfixes, xinput, Xid, XidNew};
|
|
use xcb_xembed::embedder::Embedder;
|
|
|
|
use bscreensaver_command::{BCommand, create_command_window, bscreensaver_command_response};
|
|
use bscreensaver_util::{*, settings::Configuration};
|
|
use pidfd::{CreatePidFd, PidFd};
|
|
|
|
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<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 {
|
|
$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_xfixes(&conn)?;
|
|
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 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),
|
|
_ => 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<SignalFd> {
|
|
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_xfixes(conn: &xcb::Connection) -> xcb::Result<()> {
|
|
let cookie = conn.send_request(&xfixes::QueryVersion {
|
|
client_major_version: 4,
|
|
client_minor_version: 0,
|
|
});
|
|
let reply = conn.wait_for_reply(cookie)?;
|
|
if reply.major_version() < 4 {
|
|
warn!("XFIXES version 4 or better is not supported by the X server (max supported: {}.{})", reply.major_version(), reply.minor_version());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
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) -> anyhow::Result<()> {
|
|
let cookie = conn.send_request(&randr::QueryVersion {
|
|
major_version: 1,
|
|
minor_version: 2,
|
|
});
|
|
let reply = conn.wait_for_reply(cookie)?;
|
|
if reply.major_version() < 1 || (reply.major_version() == 1 && reply.minor_version() < 2) {
|
|
Err(anyhow::anyhow!("XRandR extension version 1.2 or greater is required, but the X server only supports {}.{}", reply.major_version(), reply.minor_version()))?;
|
|
}
|
|
|
|
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<Vec<Monitor>> {
|
|
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 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<()> {
|
|
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)) => {
|
|
let res = match ev.r#type() {
|
|
b if b == command_atoms.blank => Some(blank_screen(conn, state)),
|
|
l if l == command_atoms.lock => Some(lock_screen(conn, state)),
|
|
d if d == command_atoms.deactivate => {
|
|
state.last_user_activity = Instant::now();
|
|
match state.blanker_state {
|
|
BlankerState::Idle => Some(Ok(())),
|
|
BlankerState::Blanked => Some(unblank_screen(conn, state)),
|
|
BlankerState::Locked => {
|
|
if state.unlock_dialog.is_none() {
|
|
match start_unlock_dialog(conn, state, None) {
|
|
Ok(unlock_dialog) => {
|
|
state.unlock_dialog = Some(unlock_dialog);
|
|
Some(Ok(()))
|
|
},
|
|
Err(err) => Some(Err(err)),
|
|
}
|
|
} else {
|
|
Some(Ok(()))
|
|
}
|
|
},
|
|
}
|
|
},
|
|
r if r == command_atoms.restart => Some(restart_daemon(state)),
|
|
e if e == command_atoms.exit => Some(exit_daemon(state)),
|
|
_ => None,
|
|
};
|
|
|
|
if let Some(res) = res {
|
|
let is_success = if let Err(err) = res {
|
|
warn!("Failed to handle remote command {}: {}", ev.r#type().resource_id(), err);
|
|
false
|
|
} else {
|
|
true
|
|
};
|
|
|
|
let reply_window = match ev.data() {
|
|
x::ClientMessageData::Data32(data) => {
|
|
match unsafe { x::Window::new(data[0]) } {
|
|
x::WINDOW_NONE => None,
|
|
wid => Some(wid),
|
|
}
|
|
},
|
|
_ => None,
|
|
};
|
|
if let Some(reply_window) = reply_window {
|
|
if let Err(err) = bscreensaver_command_response(conn, reply_window, is_success) {
|
|
info!("Failed to send command response: {}", err);
|
|
}
|
|
} else {
|
|
debug!("Command sender did not include a reply window");
|
|
}
|
|
}
|
|
},
|
|
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);
|
|
}
|
|
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()
|
|
});
|
|
|
|
show_cursor(conn, state);
|
|
|
|
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 show_cursor(conn: &xcb::Connection, state: &State) {
|
|
for monitor in &state.monitors {
|
|
conn.send_request(&xfixes::ShowCursor {
|
|
window: monitor.blanker_window,
|
|
});
|
|
}
|
|
}
|
|
|
|
fn hide_cursor(conn: &xcb::Connection, state: &State) {
|
|
for monitor in &state.monitors {
|
|
if let Err(err) = conn.send_and_check_request(&xfixes::HideCursor {
|
|
window: monitor.blanker_window,
|
|
}) {
|
|
warn!("Failed to hide cursor: {}", err);
|
|
}
|
|
}
|
|
}
|
|
|
|
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)?;
|
|
}
|
|
}
|
|
|
|
hide_cursor(conn, state);
|
|
|
|
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");
|
|
|
|
show_cursor(conn, state);
|
|
|
|
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(())
|
|
}
|