333 lines
13 KiB
Rust
333 lines
13 KiB
Rust
use clap::Parser;
|
|
use log::{debug, error, info, trace, warn};
|
|
use nix::{
|
|
poll::{poll, PollFd, PollFlags, PollTimeout}, sys::{
|
|
signal::{sigprocmask, SigSet, SigmaskHow, Signal},
|
|
signalfd::{SfdFlags, SignalFd},
|
|
}, unistd::{execv, fork, setsid, ForkResult}
|
|
};
|
|
use std::{
|
|
env, ffi::CString, fs::read_link, io, os::{fd::AsFd, unix::io::AsRawFd}, path::PathBuf, process::exit, rc::Rc, sync::Mutex, time::{Duration, Instant}
|
|
};
|
|
use xcb::{randr, x, xfixes, xinput};
|
|
|
|
use bscreensaver::{screensaver::{BlankerState, CommandHandlers, Screensaver}, subservice::Subservices, xkb_ext};
|
|
use bscreensaver_util::{*, settings::Configuration};
|
|
|
|
const BLANKED_ARG: &str = "blanked";
|
|
const LOCKED_ARG: &str = "locked";
|
|
|
|
macro_rules! maybe_add_fd {
|
|
($pfds:expr, $fd:expr) => {
|
|
if let Some(fd) = $fd {
|
|
if fd >= 0 {
|
|
$pfds.push(PollFd::new(borrow_raw_fd(fd), PollFlags::POLLIN));
|
|
Some(fd)
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
};
|
|
}
|
|
|
|
#[derive(Parser, Debug)]
|
|
#[command(name = "bscreensaver", version, author, about = "Blanks and locks the screen after a period of time", long_about = None)]
|
|
struct Args {
|
|
/// Starts up in already blanked
|
|
#[arg(long = BLANKED_ARG)]
|
|
blanked: bool,
|
|
|
|
/// Stats up already blanked and locked
|
|
#[arg(long = LOCKED_ARG)]
|
|
locked: bool,
|
|
}
|
|
|
|
fn main() -> anyhow::Result<()> {
|
|
init_logging("BSCREENSAVER_LOG");
|
|
|
|
let config = Configuration::load()?;
|
|
let args = Args::parse();
|
|
|
|
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, xcb::Extension::Xkb],
|
|
&[]
|
|
)?;
|
|
let setup = conn.get_setup();
|
|
let screen = setup.roots().nth(screen_num as usize).unwrap();
|
|
|
|
init_xfixes(&conn)?;
|
|
init_xinput(&conn)?;
|
|
init_randr(&conn)?;
|
|
xkb_ext::x11_setup(&conn, xkb::x11::MIN_MAJOR_XKB_VERSION, xkb::x11::MIN_MINOR_XKB_VERSION)
|
|
.map_err(|_| anyhow::anyhow!("Failed to initialize XKB extension"))?;
|
|
|
|
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, trigger| {
|
|
restart_daemon(screensaver, &mut subservices1.lock().unwrap(), trigger)
|
|
},
|
|
exit_handler: &move |screensaver, trigger| {
|
|
exit_daemon(screensaver, &mut subservices2.lock().unwrap(), trigger)
|
|
},
|
|
}
|
|
};
|
|
let mut screensaver = Screensaver::new(&config, &helper_dir, &command_handlers, &conn, screen)?;
|
|
|
|
if args.locked {
|
|
match screensaver.lock_screen() {
|
|
Err(err) => error!("POSSIBLY FAILED TO LOCK SCREEN ON STARTUP: {}", err),
|
|
Ok(_) => debug!("Got --{} arg; screen locked on startup", LOCKED_ARG),
|
|
}
|
|
} else if args.blanked {
|
|
match screensaver.blank_screen() {
|
|
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) = screensaver.handle_xcb_events() {
|
|
if conn.has_error().is_err() {
|
|
error!("Lost connection to X server; attempting to restart");
|
|
(command_handlers.restart_handler)(&mut screensaver, None)?;
|
|
}
|
|
warn!("Error handling event: {}", err);
|
|
}
|
|
|
|
let conn_fd = conn.as_raw_fd();
|
|
if conn_fd < 0 {
|
|
error!("Lost connection to X server; attempting to restart");
|
|
(command_handlers.restart_handler)(&mut screensaver, None)?;
|
|
}
|
|
let conn_fd = borrow_raw_fd(conn_fd);
|
|
|
|
|
|
let mut pfds = Vec::new();
|
|
pfds.push(PollFd::new(borrow_raw_fd(signal_fd.as_raw_fd()), PollFlags::POLLIN));
|
|
pfds.push(PollFd::new(conn_fd.as_fd(), PollFlags::POLLIN));
|
|
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(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| PollTimeout::try_from(pt).unwrap_or(PollTimeout::MAX))
|
|
.unwrap_or(PollTimeout::NONE);
|
|
|
|
trace!("about to poll (timeout={})", poll_timeout.as_millis().map(|pt| pt.to_string()).unwrap_or("(?)".into()));
|
|
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_fd().as_raw_fd() {
|
|
fd if fd == signal_fd.as_raw_fd() => handle_signals(&mut screensaver, &mut subservices.lock().unwrap(), &mut signal_fd),
|
|
fd if fd == conn_fd.as_raw_fd() => screensaver.handle_xcb_events(),
|
|
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(),
|
|
_ => Ok(()),
|
|
};
|
|
|
|
if let Err(err) = result {
|
|
if conn.has_error().is_err() {
|
|
error!("Lost connection to X server; atempting to restart");
|
|
(command_handlers.restart_handler)(&mut screensaver, None)?;
|
|
}
|
|
warn!("Error handling event: {}", err);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let since_last_activity = Instant::now().duration_since(screensaver.last_user_activity());
|
|
|
|
if screensaver.blanker_state() < BlankerState::Blanked && since_last_activity > config.lock_timeout - config.blank_before_locking {
|
|
if let Err(err) = screensaver.blank_screen() {
|
|
error!("POSSIBLY FAILED TO BLANK SCREEN: {}", err);
|
|
}
|
|
}
|
|
|
|
if screensaver.blanker_state() < BlankerState::Locked && since_last_activity > config.lock_timeout {
|
|
if let Err(err) = screensaver.lock_screen() {
|
|
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 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(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 restart_daemon(screensaver: &mut Screensaver, subservices: &mut Subservices, trigger: Option<&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 screensaver.blanker_state() {
|
|
BlankerState::Idle => vec![],
|
|
BlankerState::Blanked => vec![CString::new("--".to_string() + BLANKED_ARG)?],
|
|
BlankerState::Locked => vec![CString::new("--".to_string() + LOCKED_ARG)?],
|
|
};
|
|
|
|
kill_child_processes(screensaver, subservices);
|
|
|
|
match unsafe { fork() } {
|
|
Err(err) => {
|
|
error!("Failed to fork: {}", err);
|
|
if let Some(ev) = trigger {
|
|
bscreensaver::send_command_reply(screensaver.xcb_connection(), ev, false);
|
|
}
|
|
Err(err)?;
|
|
},
|
|
Ok(ForkResult::Parent { .. }) => {
|
|
if let Some(ev) = trigger {
|
|
bscreensaver::send_command_reply(screensaver.xcb_connection(), ev, true);
|
|
}
|
|
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(screensaver: &mut Screensaver, subservices: &mut Subservices, trigger: Option<&x::ClientMessageEvent>) -> anyhow::Result<()> {
|
|
info!("Quitting");
|
|
kill_child_processes(screensaver, subservices);
|
|
if let Some(ev) = trigger {
|
|
bscreensaver::send_command_reply(screensaver.xcb_connection(), ev, true);
|
|
}
|
|
exit(0);
|
|
}
|
|
|
|
fn kill_child_processes(screensaver: &mut Screensaver, subservices: &mut Subservices) {
|
|
screensaver.stop_unlock_dialog();
|
|
subservices.stop_all();
|
|
}
|