Brian J. Tarricone 23fef4d9e3 Support stable rust
This removes use of Option.contains(), and provides a fallback pidfd
implementation for stable.
2022-05-14 00:17:50 -07:00

943 lines
36 KiB
Rust

#![cfg_attr(feature = "use-nightly", feature(linux_pidfd))]
#![cfg_attr(feature = "use-nightly", feature(option_result_contains))]
#[cfg(not(feature = "use-nightly"))]
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, xinput, Xid};
use xcb_xembed::embedder::Embedder;
use bscreensaver_command::{BCommand, create_command_window};
use bscreensaver_util::{*, settings::Configuration};
#[cfg(feature = "use-nightly")]
use std::os::linux::process::{ChildExt, CommandExt, PidFd};
#[cfg(not(feature = "use-nightly"))]
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_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_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<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 mut command = Command::new(format!("{}/{}", env!("HELPER_DIR"), binary_name));
#[cfg(feature = "use-nightly")]
command.create_pidfd(true);
let mut child = command.spawn()?;
#[cfg(feature = "use-nightly")]
let pidfd = child.take_pidfd()?;
#[cfg(not(feature = "use-nightly"))]
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)) => 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<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)?;
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 command = Command::new(format!("{}/{}", env!("HELPER_DIR"), state.config.dialog_backend.binary_name()));
#[cfg(feature = "use-nightly")]
command.create_pidfd(true);
let mut child = command
.stdout(Stdio::piped())
.spawn()?;
let mut child_out = child.stdout.take().unwrap();
#[cfg(feature = "use-nightly")]
let child_pidfd = child.take_pidfd()?;
#[cfg(not(feature = "use-nightly"))]
let child_pidfd = child.create_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(())
}
fn opt_contains<T: PartialEq>(o: &Option<T>, v: &T) -> bool {
#[cfg(feature = "use-nightly")]
let res = o.contains(v);
#[cfg(not(feature = "use-nightly"))]
let res = o.as_ref().filter(|ov| ov == &v).is_some();
res
}