Initial import. Most things seem working.
This includes an abortive attempt to do a gtk4 dialog (which I don't think is possible, as gtk4 doesn't allow embedding toplevels anymore), and an iced dialog, which I just never started writing.
This commit is contained in:
commit
2e86445c3d
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
target/
|
||||||
|
.tags
|
1938
Cargo.lock
generated
Normal file
1938
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"async-xcb",
|
||||||
|
"auth",
|
||||||
|
"command",
|
||||||
|
"locker",
|
||||||
|
"dbus-service",
|
||||||
|
"dialog-gtk3",
|
||||||
|
# "dialog-gtk4",
|
||||||
|
# "dialog-iced",
|
||||||
|
"util",
|
||||||
|
"systemd",
|
||||||
|
"xcb-xembed",
|
||||||
|
]
|
47
Makefile
Normal file
47
Makefile
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
.PHONY: all build build-dev install clean uninstall run
|
||||||
|
|
||||||
|
PREFIX ?= /usr/local
|
||||||
|
BINDIR ?= $(PREFIX)/bin
|
||||||
|
LIBEXECDIR ?= $(PREFIX)/libexec
|
||||||
|
|
||||||
|
HELPER_DIR = $(LIBEXECDIR)/bscreensaver
|
||||||
|
HELPERS = \
|
||||||
|
bscreensaver-dbus-service \
|
||||||
|
bscreensaver-systemd \
|
||||||
|
bscreensaver-dialog-gtk3 \
|
||||||
|
$(NULL)
|
||||||
|
|
||||||
|
INSTALL ?= install
|
||||||
|
|
||||||
|
DEV_LOG_LEVEL = debug
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
build:
|
||||||
|
HELPER_DIR=$(HELPER_DIR) cargo build --release
|
||||||
|
|
||||||
|
build-dev:
|
||||||
|
HELPER_DIR=target/debug cargo build
|
||||||
|
|
||||||
|
install: build
|
||||||
|
$(INSTALL) -m 0755 -d $(BINDIR) $(HELPER_DIR)
|
||||||
|
$(INSTALL) -m 0755 target/release/bscreensaver $(BINDIR)
|
||||||
|
$(INSTALL) -m 0755 $(addprefix target/release/,$(HELPERS)) $(HELPER_DIR)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
cargo clean
|
||||||
|
|
||||||
|
uninstall:
|
||||||
|
rm -f $(BINDIR)/bscreensaver $(addprefix $(HELPER_DIR)/,$(HELPERS)) || true
|
||||||
|
rmdir -p $(BINDIR) $(HELPER_DIR) || true
|
||||||
|
rmdir -p $(PREFIX) || true
|
||||||
|
|
||||||
|
run: build-dev
|
||||||
|
BSCREENSAVER_LOCAL_DEV=1 \
|
||||||
|
RUST_BACKTRACE=1 \
|
||||||
|
BSCREENSAVER_LOG=$(DEV_LOG_LEVEL) \
|
||||||
|
BSCREENSAVER_DBUS_SERVICE_LOG=$(DEV_LOG_LEVEL) \
|
||||||
|
BSCREENSAVER_SYSTEMD_LOG=$(DEV_LOG_LEVEL) \
|
||||||
|
BSCREENSAVER_DIALOG_GTK3_LOG=$(DEV_LOG_LEVEL) \
|
||||||
|
HELPER_DIR=target/debug \
|
||||||
|
cargo run --bin bscreensaver
|
13
async-xcb/Cargo.toml
Normal file
13
async-xcb/Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "async-xcb"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-io = "1.6"
|
||||||
|
async-std = { version = "1.11", features = ["attributes"] }
|
||||||
|
futures = "0.3"
|
||||||
|
futures-lite = "1.12"
|
||||||
|
nix = "0.23"
|
||||||
|
# git source needed until extension event error resolution fix is released
|
||||||
|
xcb = { git = "https://github.com/rust-x-bindings/rust-xcb", rev = "d09b5f91bc07d56673f1bc0d6c7ecd72b5ff7b3e" }
|
49
async-xcb/src/lib.rs
Normal file
49
async-xcb/src/lib.rs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
use async_io::{Async, ReadableOwned};
|
||||||
|
use futures::prelude::*;
|
||||||
|
use futures_lite::ready;
|
||||||
|
use nix::{fcntl::{fcntl, F_GETFL, F_SETFL, OFlag}, unistd::read};
|
||||||
|
use std::{io, os::unix::io::AsRawFd, pin::Pin, sync::Arc, task::{Context, Poll}};
|
||||||
|
|
||||||
|
pub struct AsyncConnection {
|
||||||
|
conn: Arc<Async<xcb::Connection>>,
|
||||||
|
readable: Option<ReadableOwned<xcb::Connection>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncConnection {
|
||||||
|
pub fn new(conn: xcb::Connection) -> io::Result<Self> {
|
||||||
|
let flags = fcntl(conn.as_raw_fd(), F_GETFL)?;
|
||||||
|
fcntl(conn.as_raw_fd(), F_SETFL(OFlag::from_bits_truncate(flags) | OFlag::O_NONBLOCK))?;
|
||||||
|
Ok(Self {
|
||||||
|
conn: Arc::new(Async::new(conn)?),
|
||||||
|
readable: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncRead for AsyncConnection {
|
||||||
|
fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll<std::io::Result<usize>> {
|
||||||
|
loop {
|
||||||
|
match read(self.conn.as_raw_fd(), buf) {
|
||||||
|
Err(nix::Error::EAGAIN) => (),
|
||||||
|
Err(err) => {
|
||||||
|
self.readable = None;
|
||||||
|
return Poll::Ready(Err(err.into()));
|
||||||
|
},
|
||||||
|
Ok(count) => {
|
||||||
|
self.readable = None;
|
||||||
|
return Poll::Ready(Ok(count));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.readable.is_none() {
|
||||||
|
self.readable = Some(Arc::clone(&self.conn).readable_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(f) = &mut self.readable {
|
||||||
|
let res = ready!(Pin::new(f).poll(cx));
|
||||||
|
self.readable = None;
|
||||||
|
res?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
auth/Cargo.toml
Normal file
8
auth/Cargo.toml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "bscreensaver-auth"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1"
|
||||||
|
pam = "0.7"
|
16
auth/src/main.rs
Normal file
16
auth/src/main.rs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
use std::io;
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
let stdin = io::stdin();
|
||||||
|
|
||||||
|
let mut username = String::new();
|
||||||
|
stdin.read_line(&mut username)?;
|
||||||
|
let mut password = String::new();
|
||||||
|
stdin.read_line(&mut password)?;
|
||||||
|
|
||||||
|
let mut authenticator = pam::Authenticator::with_password("xscreensaver")?;
|
||||||
|
authenticator.get_handler().set_credentials(username.trim(), password.trim());
|
||||||
|
authenticator.authenticate()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
9
command/Cargo.toml
Normal file
9
command/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "bscreensaver-command"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bscreensaver-util = { path = "../util" }
|
||||||
|
clap = "3"
|
||||||
|
xcb = { git = "https://github.com/rust-x-bindings/rust-xcb", rev = "d09b5f91bc07d56673f1bc0d6c7ecd72b5ff7b3e" }
|
184
command/src/lib.rs
Normal file
184
command/src/lib.rs
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
use std::{error::Error as StdError, fmt};
|
||||||
|
use xcb::{x, Xid};
|
||||||
|
|
||||||
|
use bscreensaver_util::{create_atom, BSCREENSAVER_WM_CLASS};
|
||||||
|
|
||||||
|
const COMMAND_WINDOW_ID_ATOM_NAME: &[u8] = b"BSCREENSAVER_COMMAND_WINDOW_ID";
|
||||||
|
const COMMAND_WINDOW_WM_NAME: &[u8] = b"bscreensaver command window";
|
||||||
|
|
||||||
|
const BSCREENSAVER_BLANK_ATOM_NAME: &[u8] = b"BSCREENSAVER_BLANK";
|
||||||
|
const BSCREENSAVER_LOCK_ATOM_NAME: &[u8] = b"BSCREENSAVER_LOCK";
|
||||||
|
const BSCREENSAVER_DEACTIVATE_ATOM_NAME: &[u8] = b"BSCREENSAVER_DEACTIVATE";
|
||||||
|
const BSCREENSAVER_RESTART_ATOM_NAME: &[u8] = b"BSCREENSAVER_RESTART";
|
||||||
|
const BSCREENSAVER_EXIT_ATOM_NAME: &[u8] = b"BSCREENSAVER_EXIT";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum BCommand {
|
||||||
|
Blank,
|
||||||
|
Lock,
|
||||||
|
Deactivate,
|
||||||
|
Restart,
|
||||||
|
Exit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BCommand {
|
||||||
|
pub fn atom_name(&self) -> &'static [u8] {
|
||||||
|
match self {
|
||||||
|
Self::Blank => BSCREENSAVER_BLANK_ATOM_NAME,
|
||||||
|
Self::Lock => BSCREENSAVER_LOCK_ATOM_NAME,
|
||||||
|
Self::Deactivate => BSCREENSAVER_DEACTIVATE_ATOM_NAME,
|
||||||
|
Self::Restart => BSCREENSAVER_RESTART_ATOM_NAME,
|
||||||
|
Self::Exit => BSCREENSAVER_EXIT_ATOM_NAME,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
X(xcb::Error),
|
||||||
|
NotRunning,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::X(err) => write!(f, "{}", err),
|
||||||
|
Self::NotRunning => write!(f, "bscreensaver is not running"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StdError for Error {
|
||||||
|
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||||
|
match self {
|
||||||
|
Self::X(err) => Some(err),
|
||||||
|
Self::NotRunning => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<xcb::Error> for Error {
|
||||||
|
fn from(error: xcb::Error) -> Self {
|
||||||
|
Self::X(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<xcb::ProtocolError> for Error {
|
||||||
|
fn from(error: xcb::ProtocolError) -> Self {
|
||||||
|
Self::X(xcb::Error::Protocol(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<xcb::ConnError> for Error {
|
||||||
|
fn from(error: xcb::ConnError) -> Self {
|
||||||
|
Self::X(xcb::Error::Connection(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_command_window(conn: &xcb::Connection, screen: &x::Screen) -> Result<x::Window, Error> {
|
||||||
|
let mut cookies = Vec::new();
|
||||||
|
|
||||||
|
let msg_win: x::Window = conn.generate_id();
|
||||||
|
cookies.push(conn.send_request_checked(&x::CreateWindow {
|
||||||
|
depth: x::COPY_FROM_PARENT as u8,
|
||||||
|
wid: msg_win,
|
||||||
|
parent: screen.root(),
|
||||||
|
x: -50,
|
||||||
|
y: -50,
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
border_width: 0,
|
||||||
|
class: x::WindowClass::InputOnly,
|
||||||
|
visual: x::COPY_FROM_PARENT,
|
||||||
|
value_list: &[
|
||||||
|
x::Cw::OverrideRedirect(true),
|
||||||
|
x::Cw::EventMask(x::EventMask::STRUCTURE_NOTIFY | x::EventMask::PROPERTY_CHANGE),
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
cookies.push(conn.send_request_checked(&x::ChangeProperty {
|
||||||
|
mode: x::PropMode::Replace,
|
||||||
|
window: msg_win,
|
||||||
|
property: x::ATOM_WM_NAME,
|
||||||
|
r#type: x::ATOM_STRING,
|
||||||
|
data: COMMAND_WINDOW_WM_NAME,
|
||||||
|
}));
|
||||||
|
cookies.push(conn.send_request_checked(&x::ChangeProperty {
|
||||||
|
mode: x::PropMode::Replace,
|
||||||
|
window: msg_win,
|
||||||
|
property: x::ATOM_WM_CLASS,
|
||||||
|
r#type: x::ATOM_STRING,
|
||||||
|
data: BSCREENSAVER_WM_CLASS,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let msg_win_atom = create_atom(&conn, COMMAND_WINDOW_ID_ATOM_NAME)?;
|
||||||
|
cookies.push(conn.send_request_checked(&x::ChangeProperty {
|
||||||
|
mode: x::PropMode::Replace,
|
||||||
|
window: screen.root(),
|
||||||
|
property: msg_win_atom,
|
||||||
|
r#type: x::ATOM_WINDOW,
|
||||||
|
data: &[msg_win.resource_id()],
|
||||||
|
}));
|
||||||
|
|
||||||
|
for cookie in cookies {
|
||||||
|
conn.check_request(cookie)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(msg_win)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bscreensaver_command(command: BCommand) -> Result<(), Error> {
|
||||||
|
let (conn, _) = xcb::Connection::connect(None)?;
|
||||||
|
let setup = conn.get_setup();
|
||||||
|
|
||||||
|
let msg_window_id_atom = create_atom(&conn, COMMAND_WINDOW_ID_ATOM_NAME)?;
|
||||||
|
let msg_window_id = 'outer: loop {
|
||||||
|
for screen in setup.roots() {
|
||||||
|
let cookie = conn.send_request(&x::GetProperty {
|
||||||
|
delete: false,
|
||||||
|
window: screen.root(),
|
||||||
|
property: msg_window_id_atom,
|
||||||
|
r#type: x::ATOM_WINDOW,
|
||||||
|
long_offset: 0,
|
||||||
|
long_length: std::mem::size_of::<u32>() as u32,
|
||||||
|
});
|
||||||
|
let reply = conn.wait_for_reply(cookie)?;
|
||||||
|
let windows = reply.value::<x::Window>();
|
||||||
|
if windows.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let window = windows[0];
|
||||||
|
|
||||||
|
let cookie = conn.send_request(&x::GetProperty {
|
||||||
|
delete: false,
|
||||||
|
window,
|
||||||
|
property: x::ATOM_WM_NAME,
|
||||||
|
r#type: x::ATOM_STRING,
|
||||||
|
long_offset: 0,
|
||||||
|
long_length: COMMAND_WINDOW_WM_NAME.len() as u32 + 1,
|
||||||
|
});
|
||||||
|
let reply = conn.wait_for_reply(cookie)?;
|
||||||
|
if reply.value::<u8>() == COMMAND_WINDOW_WM_NAME {
|
||||||
|
break 'outer Ok(window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break Err(Error::NotRunning);
|
||||||
|
}?;
|
||||||
|
let command_atom = create_atom(&conn, command.atom_name())?;
|
||||||
|
|
||||||
|
let res = conn.send_and_check_request(&x::SendEvent {
|
||||||
|
propagate: false,
|
||||||
|
destination: x::SendEventDest::Window(msg_window_id),
|
||||||
|
event_mask: x::EventMask::STRUCTURE_NOTIFY,
|
||||||
|
event: &x::ClientMessageEvent::new(
|
||||||
|
msg_window_id,
|
||||||
|
command_atom,
|
||||||
|
x::ClientMessageData::Data32([0; 5])
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Err(xcb::ProtocolError::X(x::Error::Window(x::WindowError { .. }), _)) => Err(Error::NotRunning),
|
||||||
|
Err(err) => Err(Error::X(xcb::Error::Protocol(err))),
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
72
command/src/main.rs
Normal file
72
command/src/main.rs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
use clap::{Arg, Command};
|
||||||
|
use std::{env, io, process::exit};
|
||||||
|
|
||||||
|
use bscreensaver_command::{BCommand, Error, bscreensaver_command};
|
||||||
|
|
||||||
|
fn main() -> io::Result<()> {
|
||||||
|
let mut command = Command::new("bscreensaver-command")
|
||||||
|
.author(env!("CARGO_PKG_AUTHORS"))
|
||||||
|
.version(env!("CARGO_PKG_VERSION"))
|
||||||
|
.about("Send commands to the running bscreensaver instance")
|
||||||
|
.arg(
|
||||||
|
Arg::new("blank")
|
||||||
|
.long("blank")
|
||||||
|
.short('b')
|
||||||
|
.help("Blanks the screen right now")
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("lock")
|
||||||
|
.long("lock")
|
||||||
|
.short('l')
|
||||||
|
.help("Lock the screen right now")
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("deactivate")
|
||||||
|
.long("deactivate")
|
||||||
|
.short('d')
|
||||||
|
.help("Deactivates the screen lock, presenting the unlock dialog if needed. This can be used to 'reset' things so the screensaver thinks there has been user input")
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("restart")
|
||||||
|
.long("restart")
|
||||||
|
.short('r')
|
||||||
|
.help("Restarts the bscreensaver daemon")
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("exit")
|
||||||
|
.long("exit")
|
||||||
|
.short('x')
|
||||||
|
.help("Causes the bscreensaver daemon to exit now, even if the screen is locked")
|
||||||
|
);
|
||||||
|
let args = command.get_matches_mut();
|
||||||
|
|
||||||
|
let command =
|
||||||
|
if args.is_present("blank") {
|
||||||
|
BCommand::Blank
|
||||||
|
} else if args.is_present("lock") {
|
||||||
|
BCommand::Lock
|
||||||
|
} else if args.is_present("deactivate") {
|
||||||
|
BCommand::Deactivate
|
||||||
|
} else if args.is_present("restart") {
|
||||||
|
BCommand::Restart
|
||||||
|
} else if args.is_present("exit") {
|
||||||
|
BCommand::Exit
|
||||||
|
} else {
|
||||||
|
command.print_help()?;
|
||||||
|
exit(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
match bscreensaver_command(command) {
|
||||||
|
Err(Error::NotRunning) => {
|
||||||
|
eprintln!("bscreensaver is not running");
|
||||||
|
exit(1);
|
||||||
|
},
|
||||||
|
Err(Error::X(err)) => {
|
||||||
|
eprintln!("Failed to communicate with X server: {}", err);
|
||||||
|
exit(1);
|
||||||
|
},
|
||||||
|
Ok(_) => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
16
dbus-service/Cargo.toml
Normal file
16
dbus-service/Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "bscreensaver-dbus-service"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1"
|
||||||
|
async-std = { version = "1.11", features = ["attributes"] }
|
||||||
|
async-xcb = { path = "../async-xcb" }
|
||||||
|
bscreensaver-command = { path = "../command" }
|
||||||
|
bscreensaver-util = { path = "../util" }
|
||||||
|
futures = "0.3"
|
||||||
|
log = "0.4"
|
||||||
|
# git source needed until extension event error resolution fix is released
|
||||||
|
xcb = { git = "https://github.com/rust-x-bindings/rust-xcb", rev = "d09b5f91bc07d56673f1bc0d6c7ecd72b5ff7b3e" }
|
||||||
|
zbus = "2"
|
235
dbus-service/src/main.rs
Normal file
235
dbus-service/src/main.rs
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
#![feature(option_result_contains)]
|
||||||
|
#![feature(is_some_with)]
|
||||||
|
|
||||||
|
use async_std::{fs::File, prelude::*, sync::{Arc, Mutex}, task};
|
||||||
|
use bscreensaver_util::init_logging;
|
||||||
|
use futures::{future::FutureExt, pin_mut, select};
|
||||||
|
use log::{debug, error, info, trace, warn};
|
||||||
|
use std::{io, process::exit, time::{Duration, Instant}};
|
||||||
|
use zbus::{dbus_interface, fdo::{self, DBusProxy, RequestNameFlags}, names::{BusName, UniqueName, WellKnownName}, ConnectionBuilder, MessageHeader};
|
||||||
|
|
||||||
|
use bscreensaver_command::{bscreensaver_command, BCommand};
|
||||||
|
|
||||||
|
const OUR_DBUS_NAME: &str = "org.freedesktop.ScreenSaver";
|
||||||
|
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(45);
|
||||||
|
|
||||||
|
struct Inhibitor {
|
||||||
|
cookie: u32,
|
||||||
|
app_name: String,
|
||||||
|
peer: Option<UniqueName<'static>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct State {
|
||||||
|
inhibitors: Vec<Inhibitor>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ScreenSaver {
|
||||||
|
state: Arc<Mutex<State>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[dbus_interface(name = "org.freedesktop.ScreenSaver")]
|
||||||
|
impl ScreenSaver {
|
||||||
|
async fn inhibit(
|
||||||
|
&mut self,
|
||||||
|
#[zbus(header)]
|
||||||
|
hdr: MessageHeader<'_>,
|
||||||
|
app_name: &str,
|
||||||
|
reason: &str
|
||||||
|
) -> fdo::Result<u32> {
|
||||||
|
debug!("Handling inhibit for app {}: {}", app_name, reason);
|
||||||
|
if app_name.trim().is_empty() {
|
||||||
|
return Err(fdo::Error::InvalidArgs("Application name is blank".to_string()));
|
||||||
|
} else if reason.trim().is_empty() {
|
||||||
|
return Err(fdo::Error::InvalidArgs("Reason is blank".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Firefox tries to inhibit when only audio is playing, so ignore that
|
||||||
|
if reason.contains("audio") && !reason.contains("video") {
|
||||||
|
info!("Ignoring audio-only inhibit from app {}", app_name);
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let peer = hdr.sender()?;
|
||||||
|
let cookie = rand_u32().await
|
||||||
|
.map_err(|err| fdo::Error::IOError(err.to_string()))?;
|
||||||
|
self.state.lock().await.inhibitors.push(Inhibitor {
|
||||||
|
cookie,
|
||||||
|
app_name: app_name.to_string(),
|
||||||
|
peer: peer.map(|s| s.to_owned()),
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn un_inhibit(&mut self, cookie: u32) -> fdo::Result<()> {
|
||||||
|
let mut state = self.state.lock().await;
|
||||||
|
|
||||||
|
let before = state.inhibitors.len();
|
||||||
|
state.inhibitors.retain(|inhibitor| {
|
||||||
|
if inhibitor.cookie == cookie {
|
||||||
|
info!("Uninhibit received from {} for cookie {}", inhibitor.app_name, cookie);
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if before == state.inhibitors.len() {
|
||||||
|
info!("No inhibitor found with cookie {}", cookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::main]
|
||||||
|
async fn main() {
|
||||||
|
init_logging("BSCREENSAVER_DBUS_SERVICE_LOG");
|
||||||
|
|
||||||
|
let state = Arc::new(Mutex::new(State {
|
||||||
|
inhibitors: Vec::new(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let xcb_handle = task::spawn(xcb_task()).fuse();
|
||||||
|
let dbus_handle = task::spawn(dbus_task(Arc::clone(&state))).fuse();
|
||||||
|
let heartbeat_handle = task::spawn(heartbeat_task(Arc::clone(&state))).fuse();
|
||||||
|
|
||||||
|
pin_mut!(xcb_handle, dbus_handle, heartbeat_handle);
|
||||||
|
|
||||||
|
let res = loop {
|
||||||
|
select! {
|
||||||
|
_ = xcb_handle => {
|
||||||
|
info!("Lost connection to X server; quitting");
|
||||||
|
break Ok(());
|
||||||
|
},
|
||||||
|
res = dbus_handle => {
|
||||||
|
match res {
|
||||||
|
Err(err) => error!("Lost connection to the system bus: {}", err),
|
||||||
|
Ok(_) => error!("DBus task exited normally; this should not happen!"),
|
||||||
|
}
|
||||||
|
break Err(());
|
||||||
|
},
|
||||||
|
res = heartbeat_handle => {
|
||||||
|
match res {
|
||||||
|
Err(err) => error!("Heartbeat task terminated with error: {}", err),
|
||||||
|
Ok(_) => error!("Heartbeat task exited normally; this should not happen!"),
|
||||||
|
}
|
||||||
|
break Err(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(_) = res {
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn xcb_task() -> anyhow::Result<()> {
|
||||||
|
let (xcb_conn, _) = task::block_on(async { xcb::Connection::connect(None) })?;
|
||||||
|
let mut xcb_conn = async_xcb::AsyncConnection::new(xcb_conn)?;
|
||||||
|
|
||||||
|
// We need to drain the XCB connection periodically. Even though we have not
|
||||||
|
// asked for any events, we'll still get stuff like MappingNotify if the keyboard
|
||||||
|
// settings change.
|
||||||
|
loop {
|
||||||
|
let mut buf = [0u8; 512];
|
||||||
|
xcb_conn.read(&mut buf).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn dbus_task(state: Arc<Mutex<State>>) -> anyhow::Result<()> {
|
||||||
|
let org_fdo_screensaver = ScreenSaver { state: Arc::clone(&state) };
|
||||||
|
let screensaver = ScreenSaver { state: Arc::clone(&state) };
|
||||||
|
|
||||||
|
let dbus_conn = ConnectionBuilder::session()?
|
||||||
|
.serve_at("/org/freedesktop/ScreenSaver", org_fdo_screensaver)?
|
||||||
|
.serve_at("/ScreenSaver", screensaver)?
|
||||||
|
.build()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let our_unique_name = dbus_conn.unique_name().unwrap();
|
||||||
|
|
||||||
|
let dbus_proxy = DBusProxy::new(&dbus_conn).await?;
|
||||||
|
dbus_proxy.request_name(
|
||||||
|
WellKnownName::from_static_str(OUR_DBUS_NAME)?,
|
||||||
|
RequestNameFlags::AllowReplacement | RequestNameFlags::ReplaceExisting | RequestNameFlags::DoNotQueue
|
||||||
|
).await?;
|
||||||
|
let mut name_owner_changed_stream = dbus_proxy.receive_name_owner_changed().await?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if let Some(name_owner_changed) = name_owner_changed_stream.next().await {
|
||||||
|
let args = name_owner_changed.args()?;
|
||||||
|
match args.name() {
|
||||||
|
BusName::WellKnown(name) if name == OUR_DBUS_NAME => {
|
||||||
|
if args.new_owner().is_none() || args.new_owner().is_some_and(|no| no != our_unique_name) {
|
||||||
|
info!("Lost bus name {}; quitting", OUR_DBUS_NAME);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
BusName::Unique(name) => {
|
||||||
|
if args.new_owner().is_none() {
|
||||||
|
state.lock().await.inhibitors.retain(|inhibitor| {
|
||||||
|
if inhibitor.peer.contains(name) {
|
||||||
|
info!("Canceling inhibit from {}, as the client has disappeared", inhibitor.app_name);
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn heartbeat_task(state_mtx: Arc<Mutex<State>>) -> anyhow::Result<()> {
|
||||||
|
let mut last_heartbeat: Option<Instant> = None;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let state = state_mtx.lock().await;
|
||||||
|
let next_heartbeat =
|
||||||
|
if state.inhibitors.is_empty() {
|
||||||
|
HEARTBEAT_INTERVAL
|
||||||
|
} else {
|
||||||
|
if let Some(lh) = last_heartbeat {
|
||||||
|
let since_last = Instant::now().duration_since(lh);
|
||||||
|
if since_last < HEARTBEAT_INTERVAL {
|
||||||
|
HEARTBEAT_INTERVAL - since_last
|
||||||
|
} else {
|
||||||
|
Duration::ZERO
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Duration::ZERO
|
||||||
|
}
|
||||||
|
};
|
||||||
|
drop(state);
|
||||||
|
|
||||||
|
task::sleep(next_heartbeat).await;
|
||||||
|
debug!("Heartbeat timeout expired");
|
||||||
|
|
||||||
|
let state = state_mtx.lock().await;
|
||||||
|
if !state.inhibitors.is_empty() && (last_heartbeat.is_none() || last_heartbeat.as_ref().filter(|lh| lh.elapsed() < HEARTBEAT_INTERVAL).is_none()) {
|
||||||
|
trace!("About to deactivate; active inhibitors:");
|
||||||
|
for inhibitor in &state.inhibitors {
|
||||||
|
trace!(" {}: {}", inhibitor.cookie, inhibitor.app_name);
|
||||||
|
}
|
||||||
|
drop(state);
|
||||||
|
task::block_on(async {
|
||||||
|
if let Err(err) = bscreensaver_command(BCommand::Deactivate) {
|
||||||
|
warn!("Failed to deactivate screen lock: {}", err);
|
||||||
|
} else {
|
||||||
|
debug!("Successfully issued deactivate heartbeat");
|
||||||
|
last_heartbeat = Some(Instant::now());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn rand_u32() -> io::Result<u32> {
|
||||||
|
let mut f = File::open("/dev/urandom").await?;
|
||||||
|
let mut buf = [0u8; 4];
|
||||||
|
f.read_exact(&mut buf).await?;
|
||||||
|
Ok(((buf[0] as u32) << 24) | ((buf[1] as u32) << 16) | ((buf[2] as u32) << 8) | (buf[3] as u32))
|
||||||
|
}
|
18
dialog-gtk3/Cargo.toml
Normal file
18
dialog-gtk3/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "bscreensaver-dialog-gtk3"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1"
|
||||||
|
bscreensaver-util = { path = "../util" }
|
||||||
|
chrono = "0.4"
|
||||||
|
gethostname = "0.2"
|
||||||
|
glib = { version = "0.15", features = ["v2_68"] }
|
||||||
|
gtk = { version = "0.15", features = ["v3_24"] }
|
||||||
|
gtk-sys = "0.15"
|
||||||
|
gdk-sys = "0.15"
|
||||||
|
gdkx11 = "0.15"
|
||||||
|
log = "0.4"
|
||||||
|
pam = "0.7"
|
||||||
|
#x11 = "2.19"
|
261
dialog-gtk3/src/main.rs
Normal file
261
dialog-gtk3/src/main.rs
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
use chrono::prelude::*;
|
||||||
|
use gdkx11::X11Window;
|
||||||
|
use gethostname::gethostname;
|
||||||
|
use glib::GString;
|
||||||
|
use gtk::{prelude::*, Button, Entry, Label, Plug, Window};
|
||||||
|
use log::{debug, error};
|
||||||
|
use std::{io::{self, Write}, process::exit, thread};
|
||||||
|
|
||||||
|
use bscreensaver_util::init_logging;
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
init_logging("BSCREENSAVER_DIALOG_GTK3_LOG");
|
||||||
|
|
||||||
|
let standalone = std::env::var("BSCREENSAVER_DIALOG_STANDALONE").is_ok();
|
||||||
|
|
||||||
|
unsafe { glib::log_writer_default_set_use_stderr(true) };
|
||||||
|
gtk::init()?;
|
||||||
|
|
||||||
|
let top_sg = gtk::SizeGroup::builder()
|
||||||
|
.mode(gtk::SizeGroupMode::Horizontal)
|
||||||
|
.build();
|
||||||
|
let label_sg = gtk::SizeGroup::builder()
|
||||||
|
.mode(gtk::SizeGroupMode::Horizontal)
|
||||||
|
.build();
|
||||||
|
let entry_sg = gtk::SizeGroup::builder()
|
||||||
|
.mode(gtk::SizeGroupMode::Horizontal)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let header = gtk::HeaderBar::builder()
|
||||||
|
.title("Unlock Screen")
|
||||||
|
.show_close_button(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let top_vbox = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Vertical)
|
||||||
|
.spacing(0)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let dialog =
|
||||||
|
if standalone {
|
||||||
|
let win = Window::builder()
|
||||||
|
.type_(gtk::WindowType::Toplevel)
|
||||||
|
.title("Unlock Screen")
|
||||||
|
.modal(true)
|
||||||
|
.build();
|
||||||
|
win.set_titlebar(Some(&header));
|
||||||
|
win.upcast::<gtk::Window>()
|
||||||
|
} else {
|
||||||
|
let plug = Plug::builder()
|
||||||
|
.type_(gtk::WindowType::Toplevel)
|
||||||
|
.title("Unlock Screen")
|
||||||
|
.modal(true)
|
||||||
|
.build();
|
||||||
|
plug.connect_embedded(|_| debug!("DIALOG EMBEDDED"));
|
||||||
|
plug.connect_embedded_notify(|_| debug!("DIALOG EMBEDDED (notify)"));
|
||||||
|
plug.connect_realize(|plug| {
|
||||||
|
let plug_window = match plug.window().unwrap().downcast::<X11Window>() {
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to find XID of unlock dialog window: {}", err);
|
||||||
|
exit(2);
|
||||||
|
},
|
||||||
|
Ok(w) => w,
|
||||||
|
};
|
||||||
|
let xid = plug_window.xid() as u32;
|
||||||
|
let xid_buf: [u8; 4] = [
|
||||||
|
((xid >> 24) & 0xff) as u8,
|
||||||
|
((xid >> 16) & 0xff) as u8,
|
||||||
|
((xid >> 8) & 0xff) as u8,
|
||||||
|
(xid & 0xff) as u8,
|
||||||
|
];
|
||||||
|
let out = io::stdout();
|
||||||
|
let mut out_locked = out.lock();
|
||||||
|
if let Err(err) = out_locked.write_all(&xid_buf).and_then(|_| out_locked.flush()) {
|
||||||
|
error!("Failed to write XID to stdout: {}", err);
|
||||||
|
exit(2);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Walking the header's widget tree, finding the close button, and connecting
|
||||||
|
// to the 'clicked' signal strangely doesn't work either, so let's just
|
||||||
|
// disable it for now.
|
||||||
|
header.set_show_close_button(false);
|
||||||
|
top_vbox.pack_start(&header, true, false, 0);
|
||||||
|
|
||||||
|
plug.upcast::<gtk::Window>()
|
||||||
|
};
|
||||||
|
|
||||||
|
// I don't know why, but this doesn't work when we're a GktPlug, despite
|
||||||
|
// an examination of the gtk source suggesting that the header should send
|
||||||
|
// a delete-event to the toplevel (which should be the plug) when the close
|
||||||
|
// button is clicked. For some reason, though, we never get the delete-event.
|
||||||
|
dialog.connect_delete_event(|_, _| exit(1));
|
||||||
|
dialog.connect_realize(|_| debug!("DIALOG REALIZED"));
|
||||||
|
dialog.connect_map(|_| debug!("DIALOG MAPPED"));
|
||||||
|
dialog.connect_unmap(|_| debug!("DIALOG UNMAPPED"));
|
||||||
|
dialog.connect_unrealize(|_| debug!("DIALOG_UNREALIZED"));
|
||||||
|
dialog.add(&top_vbox);
|
||||||
|
|
||||||
|
let top_hbox = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Horizontal)
|
||||||
|
.border_width(48)
|
||||||
|
.spacing(8)
|
||||||
|
.build();
|
||||||
|
top_vbox.pack_start(&top_hbox, true, true, 0);
|
||||||
|
|
||||||
|
let vbox = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Vertical)
|
||||||
|
.spacing(4)
|
||||||
|
.build();
|
||||||
|
top_sg.add_widget(&vbox);
|
||||||
|
top_hbox.pack_start(&vbox, true, true, 0);
|
||||||
|
|
||||||
|
let attrs = gtk::pango::AttrList::new();
|
||||||
|
attrs.insert(gtk::pango::AttrFloat::new_scale(gtk::pango::SCALE_XX_LARGE));
|
||||||
|
let mut bold_desc = gtk::pango::FontDescription::new();
|
||||||
|
bold_desc.set_weight(gtk::pango::Weight::Bold);
|
||||||
|
attrs.insert(gtk::pango::AttrFontDesc::new(&bold_desc));
|
||||||
|
let label = gtk::Label::builder()
|
||||||
|
.label(gethostname().to_str().unwrap_or("(unknown hostname)"))
|
||||||
|
.xalign(0.5)
|
||||||
|
.yalign(0.5)
|
||||||
|
.attributes(&attrs)
|
||||||
|
.build();
|
||||||
|
vbox.pack_start(&label, false, false, 0);
|
||||||
|
|
||||||
|
let attrs = gtk::pango::AttrList::new();
|
||||||
|
attrs.insert(gtk::pango::AttrFloat::new_scale(gtk::pango::SCALE_LARGE));
|
||||||
|
let label = gtk::Label::builder()
|
||||||
|
.xalign(0.5)
|
||||||
|
.yalign(0.5)
|
||||||
|
.attributes(&attrs)
|
||||||
|
.build();
|
||||||
|
set_time_label(&label);
|
||||||
|
vbox.pack_start(&label, false, false, 0);
|
||||||
|
glib::timeout_add_seconds_local(1, move || {
|
||||||
|
set_time_label(&label);
|
||||||
|
glib::source::Continue(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
let sep = gtk::Separator::builder()
|
||||||
|
.orientation(gtk::Orientation::Vertical)
|
||||||
|
.build();
|
||||||
|
top_hbox.pack_start(&sep, true, false, 0);
|
||||||
|
|
||||||
|
let vbox = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Vertical)
|
||||||
|
.spacing(4)
|
||||||
|
.build();
|
||||||
|
top_sg.add_widget(&vbox);
|
||||||
|
top_hbox.pack_start(&vbox, true, true, 0);
|
||||||
|
|
||||||
|
let hbox = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Horizontal)
|
||||||
|
.spacing(8)
|
||||||
|
.build();
|
||||||
|
vbox.pack_start(&hbox, true, true, 2);
|
||||||
|
|
||||||
|
let label = Label::builder()
|
||||||
|
.label("Username:")
|
||||||
|
.xalign(0.0)
|
||||||
|
.build();
|
||||||
|
label_sg.add_widget(&label);
|
||||||
|
hbox.pack_start(&label, false, true, 8);
|
||||||
|
|
||||||
|
let username = bscreensaver_util::get_username()?;
|
||||||
|
let username_box = Entry::builder()
|
||||||
|
.text(&username)
|
||||||
|
.sensitive(false)
|
||||||
|
.build();
|
||||||
|
entry_sg.add_widget(&username_box);
|
||||||
|
hbox.pack_start(&username_box, true, true, 8);
|
||||||
|
|
||||||
|
let hbox = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Horizontal)
|
||||||
|
.spacing(8)
|
||||||
|
.build();
|
||||||
|
vbox.pack_start(&hbox, true, true, 0);
|
||||||
|
|
||||||
|
let label = Label::builder()
|
||||||
|
.label("Password:")
|
||||||
|
.xalign(0.0)
|
||||||
|
.build();
|
||||||
|
label_sg.add_widget(&label);
|
||||||
|
hbox.pack_start(&label, false, true, 8);
|
||||||
|
|
||||||
|
let password_box = Entry::builder()
|
||||||
|
.visibility(false)
|
||||||
|
.input_purpose(gtk::InputPurpose::Password)
|
||||||
|
.activates_default(true)
|
||||||
|
.width_chars(25)
|
||||||
|
.build();
|
||||||
|
entry_sg.add_widget(&password_box);
|
||||||
|
hbox.pack_start(&password_box, true, true, 8);
|
||||||
|
password_box.connect_key_press_event(|_, ev| {
|
||||||
|
if ev.keyval().name() == Some(GString::from("Escape")) {
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
gtk::Inhibit(false)
|
||||||
|
});
|
||||||
|
password_box.grab_focus();
|
||||||
|
|
||||||
|
let hbox = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Horizontal)
|
||||||
|
.spacing(8)
|
||||||
|
.build();
|
||||||
|
vbox.pack_start(&hbox, true, true, 2);
|
||||||
|
|
||||||
|
let button = Button::builder()
|
||||||
|
.label("Unlock")
|
||||||
|
.build();
|
||||||
|
button.connect_clicked(move |button| {
|
||||||
|
button.set_sensitive(false);
|
||||||
|
password_box.set_sensitive(false);
|
||||||
|
|
||||||
|
let username = username.clone();
|
||||||
|
let password = password_box.text().to_string();
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
if authenticate(&username, &password) {
|
||||||
|
exit(0);
|
||||||
|
} else {
|
||||||
|
exit(-1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
hbox.pack_end(&button, false, true, 8);
|
||||||
|
button.set_can_default(true);
|
||||||
|
button.set_has_default(true);
|
||||||
|
|
||||||
|
dialog.show_all();
|
||||||
|
|
||||||
|
gtk::main();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_time_label(label: >k::Label) {
|
||||||
|
let now = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
||||||
|
label.set_label(&now);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn authenticate(username: &String, password: &String) -> bool {
|
||||||
|
let mut authenticator = match pam::Authenticator::with_password("xscreensaver") {
|
||||||
|
Err(err) => {
|
||||||
|
error!("[PAM] {}", err);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
Ok(authenticator) => authenticator,
|
||||||
|
};
|
||||||
|
authenticator.get_handler().set_credentials(username, password);
|
||||||
|
if let Err(err) = authenticator.authenticate() {
|
||||||
|
error!("[PAM] {}", err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if let Err(err) = authenticator.open_session() {
|
||||||
|
error!("[PAM] {}", err);
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
9
dialog-gtk4/Cargo.toml
Normal file
9
dialog-gtk4/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "bscreensaver-dialog-gtk4"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gtk = { version = "0.4", package = "gtk4", features = ["v4_6"]}
|
||||||
|
gdk-x11 = { version = "0.4", package = "gdk4-x11", features = ["v4_4", "xlib"]}
|
||||||
|
x11 = "2.19"
|
43
dialog-gtk4/src/main.rs
Normal file
43
dialog-gtk4/src/main.rs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
use gdk_x11::{X11Surface, X11Display};
|
||||||
|
use gtk::{prelude::*, Application, ApplicationWindow, Label};
|
||||||
|
use std::process::exit;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let app = Application::builder()
|
||||||
|
.application_id("org.spurint.bscreensaver.dialog-gtk4")
|
||||||
|
.build();
|
||||||
|
app.connect_activate(build_ui);
|
||||||
|
|
||||||
|
app.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_ui(app: &Application) {
|
||||||
|
let titlebar = Label::builder()
|
||||||
|
.label("Unlock Screen")
|
||||||
|
.halign(gtk::Align::Center)
|
||||||
|
.single_line_mode(true)
|
||||||
|
.build();
|
||||||
|
titlebar.show();
|
||||||
|
|
||||||
|
let window = ApplicationWindow::builder()
|
||||||
|
.application(app)
|
||||||
|
.titlebar(&titlebar)
|
||||||
|
.modal(true)
|
||||||
|
.decorated(false)
|
||||||
|
.build();
|
||||||
|
window.realize();
|
||||||
|
|
||||||
|
let surface = unsafe { window.surface().unsafe_cast::<X11Surface>() };
|
||||||
|
let xid = surface.xid();
|
||||||
|
if xid == 0 {
|
||||||
|
eprintln!("Lock dialog has no XID");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
println!("{}", surface.xid());
|
||||||
|
|
||||||
|
let mut buf = String::new();
|
||||||
|
let stdin = std::io::stdin();
|
||||||
|
stdin.read_line(&mut buf).unwrap();
|
||||||
|
|
||||||
|
window.present();
|
||||||
|
}
|
8
dialog-iced/Cargo.toml
Normal file
8
dialog-iced/Cargo.toml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "bscreensaver-dialog-iced"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bscreensaver-util = { path = "../util" }
|
||||||
|
xcb = { version = "1.1", features = ["randr"] }
|
0
dialog-iced/src/main.rs
Normal file
0
dialog-iced/src/main.rs
Normal file
27
locker/Cargo.toml
Normal file
27
locker/Cargo.toml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
[package]
|
||||||
|
name = "bscreensaver"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = [
|
||||||
|
"Brian Tarricone <brian@tarricone.org>",
|
||||||
|
]
|
||||||
|
edition = "2021"
|
||||||
|
description = "Secure, simple X11 screen locker"
|
||||||
|
license = "GPL-3.0"
|
||||||
|
repository = "https://github.com/kelnos/bscreensaver"
|
||||||
|
readme = "README.md"
|
||||||
|
keywords = ["gui", "screensaver", "screen-locker"]
|
||||||
|
categories = ["gui"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1"
|
||||||
|
clap = "3"
|
||||||
|
bscreensaver-command = { path = "../command" }
|
||||||
|
bscreensaver-util = { path = "../util" }
|
||||||
|
humantime = "2"
|
||||||
|
log = "0.4"
|
||||||
|
nix = "0.23"
|
||||||
|
toml = "0.5"
|
||||||
|
# git source needed until extension event error resolution fix is released
|
||||||
|
xcb = { git = "https://github.com/rust-x-bindings/rust-xcb", rev = "d09b5f91bc07d56673f1bc0d6c7ecd72b5ff7b3e", features = ["randr", "xfixes", "xinput"] }
|
||||||
|
xcb-xembed = { path = "../xcb-xembed" }
|
||||||
|
xdg = "2"
|
992
locker/src/main.rs
Normal file
992
locker/src/main.rs
Normal file
@ -0,0 +1,992 @@
|
|||||||
|
#![feature(linux_pidfd)]
|
||||||
|
#![feature(option_result_contains)]
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
|
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, File},
|
||||||
|
io::{self, Read},
|
||||||
|
os::{
|
||||||
|
linux::process::{ChildExt, CommandExt, PidFd},
|
||||||
|
unix::io::AsRawFd,
|
||||||
|
unix::process::ExitStatusExt,
|
||||||
|
},
|
||||||
|
process::{exit, Child, Command, Stdio},
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
use xcb::{randr, x, xinput, Xid};
|
||||||
|
use xcb_xembed::embedder::Embedder;
|
||||||
|
|
||||||
|
use bscreensaver_command::{BCommand, create_command_window};
|
||||||
|
use bscreensaver_util::*;
|
||||||
|
|
||||||
|
const BLANKED_ARG: &str = "blanked";
|
||||||
|
const LOCKED_ARG: &str = "locked";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
enum DialogBackend {
|
||||||
|
Gtk3,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DialogBackend {
|
||||||
|
pub fn binary_name(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::Gtk3 => "bscreensaver-dialog-gtk3",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for DialogBackend {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
"gtk3" => Ok(Self::Gtk3),
|
||||||
|
other => Err(anyhow!("'{}' is not a valid dialog backend (valid: 'gtk3')", other)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Configuration {
|
||||||
|
lock_timeout: Duration,
|
||||||
|
blank_before_locking: Duration,
|
||||||
|
dialog_backend: DialogBackend,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Configuration {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
lock_timeout: Duration::from_secs(60 * 10),
|
||||||
|
blank_before_locking: Duration::ZERO,
|
||||||
|
dialog_backend: DialogBackend::Gtk3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 = parse_config()?;
|
||||||
|
|
||||||
|
let args = ClapCommand::new("Blanks and locks the screen after a period of time")
|
||||||
|
.author(env!("CARGO_PKG_AUTHORS"))
|
||||||
|
.version(env!("CARGO_PKG_VERSION"))
|
||||||
|
.arg(
|
||||||
|
Arg::new("blanked")
|
||||||
|
.long(BLANKED_ARG)
|
||||||
|
.help("Starts up in the blanked state")
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("locked")
|
||||||
|
.long(LOCKED_ARG)
|
||||||
|
.help("Starts up in the blanked and locked state")
|
||||||
|
)
|
||||||
|
.get_matches();
|
||||||
|
|
||||||
|
let mut signal_fd = init_signals()?;
|
||||||
|
|
||||||
|
let (conn, screen_num) = xcb::Connection::connect_with_extensions(
|
||||||
|
None,
|
||||||
|
&[xcb::Extension::RandR, xcb::Extension::XFixes, xcb::Extension::Input],
|
||||||
|
&[]
|
||||||
|
)?;
|
||||||
|
let setup = conn.get_setup();
|
||||||
|
let screen = setup.roots().nth(screen_num as usize).unwrap();
|
||||||
|
|
||||||
|
init_xinput(&conn)?;
|
||||||
|
init_randr(&conn)?;
|
||||||
|
create_command_window(&conn, &screen)?;
|
||||||
|
|
||||||
|
let command_atoms = CommandAtoms {
|
||||||
|
blank: create_atom(&conn, BCommand::Blank.atom_name())?,
|
||||||
|
lock: create_atom(&conn, BCommand::Lock.atom_name())?,
|
||||||
|
deactivate: create_atom(&conn, BCommand::Deactivate.atom_name())?,
|
||||||
|
restart: create_atom(&conn, BCommand::Restart.atom_name())?,
|
||||||
|
exit: create_atom(&conn, BCommand::Exit.atom_name())?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut state = State {
|
||||||
|
config,
|
||||||
|
monitors: create_blanker_windows(&conn)?,
|
||||||
|
dbus_service: None,
|
||||||
|
systemd_service: None,
|
||||||
|
last_user_activity: Instant::now(),
|
||||||
|
blanker_state: BlankerState::Idle,
|
||||||
|
unlock_dialog: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
start_dbus_service(&mut state)?;
|
||||||
|
start_systemd_service(&mut state)?;
|
||||||
|
|
||||||
|
if args.is_present(LOCKED_ARG) {
|
||||||
|
match lock_screen(&conn, &mut state) {
|
||||||
|
Err(err) => error!("POSSIBLY FAILED TO LOCK SCREEN ON STARTUP: {}", err),
|
||||||
|
Ok(_) => debug!("Got --{} arg; screen locked on startup", LOCKED_ARG),
|
||||||
|
}
|
||||||
|
} else if args.is_present(BLANKED_ARG) {
|
||||||
|
match lock_screen(&conn, &mut state) {
|
||||||
|
Err(err) => warn!("Possibly failed to blank screen on startup: {}", err),
|
||||||
|
Ok(_) => debug!("Got --{} arg; screen locked on startup", BLANKED_ARG),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = conn.send_and_check_request(&x::SetScreenSaver {
|
||||||
|
timeout: 0,
|
||||||
|
interval: 0,
|
||||||
|
prefer_blanking: x::Blanking::NotPreferred,
|
||||||
|
allow_exposures: x::Exposures::NotAllowed,
|
||||||
|
});
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if let Err(err) = handle_xcb_events(&conn, &mut state, &command_atoms) {
|
||||||
|
if conn.has_error().is_err() {
|
||||||
|
error!("Lost connection to X server; attempting to restart");
|
||||||
|
restart_daemon(&mut state)?;
|
||||||
|
}
|
||||||
|
warn!("Error handling event: {}", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pfds = Vec::new();
|
||||||
|
pfds.push(PollFd::new(signal_fd.as_raw_fd(), PollFlags::POLLIN));
|
||||||
|
pfds.push(PollFd::new(conn.as_raw_fd(), PollFlags::POLLIN));
|
||||||
|
let dbus_service_fd = maybe_add_fd!(&mut pfds, state.dbus_service.as_ref().map(|ds| ds.1.as_raw_fd()));
|
||||||
|
let systemd_service_fd = maybe_add_fd!(&mut pfds, state.systemd_service.as_ref().map(|ds| ds.1.as_raw_fd()));
|
||||||
|
let dialog_fd = maybe_add_fd!(&mut pfds, state.unlock_dialog.as_ref().map(|ud| ud.child_pidfd.as_raw_fd()));
|
||||||
|
|
||||||
|
let since_last_activity = Instant::now().duration_since(state.last_user_activity);
|
||||||
|
let poll_timeout = match state.blanker_state {
|
||||||
|
BlankerState::Idle if since_last_activity > state.config.lock_timeout - state.config.blank_before_locking => Some(Duration::ZERO),
|
||||||
|
BlankerState::Idle => Some(state.config.lock_timeout - state.config.blank_before_locking - since_last_activity),
|
||||||
|
BlankerState::Blanked if since_last_activity > state.config.lock_timeout => Some(Duration::ZERO),
|
||||||
|
BlankerState::Blanked => Some(state.config.lock_timeout - since_last_activity),
|
||||||
|
BlankerState::Locked => None,
|
||||||
|
};
|
||||||
|
let poll_timeout = poll_timeout.map(|pt| if pt.as_millis() > i32::MAX as u128 {
|
||||||
|
i32::MAX
|
||||||
|
} else {
|
||||||
|
pt.as_millis() as i32
|
||||||
|
}).unwrap_or(-1);
|
||||||
|
|
||||||
|
trace!("about to poll (timeout={})", poll_timeout);
|
||||||
|
let nready = poll(pfds.as_mut_slice(), poll_timeout)?; // FIXME: maybe shouldn't quit here on errors if screen is locked
|
||||||
|
trace!("polled; {} FD ready", nready);
|
||||||
|
if nready > 0 {
|
||||||
|
for pfd in pfds {
|
||||||
|
if pfd.revents().filter(|pf| pf.contains(PollFlags::POLLIN)).is_some() {
|
||||||
|
let result = match pfd.as_raw_fd() {
|
||||||
|
fd if fd == signal_fd.as_raw_fd() => handle_signals(&mut state, &mut signal_fd),
|
||||||
|
fd if fd == conn.as_raw_fd() => handle_xcb_events(&conn, &mut state, &command_atoms),
|
||||||
|
fd if dbus_service_fd.contains(&fd) => handle_subservice_quit(state.dbus_service.take(), "DBus", || start_dbus_service(&mut state)),
|
||||||
|
fd if systemd_service_fd.contains(&fd) => handle_subservice_quit(state.systemd_service.take(), "systemd", || start_systemd_service(&mut state)),
|
||||||
|
fd if dialog_fd.contains(&fd) => handle_unlock_dialog_quit(&conn, &mut state),
|
||||||
|
_ => Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = result {
|
||||||
|
if conn.has_error().is_err() {
|
||||||
|
error!("Lost connection to X server; atempting to restart");
|
||||||
|
restart_daemon(&mut state)?;
|
||||||
|
}
|
||||||
|
warn!("Error handling event: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let since_last_activity = Instant::now().duration_since(state.last_user_activity);
|
||||||
|
|
||||||
|
if state.blanker_state < BlankerState::Blanked && since_last_activity > state.config.lock_timeout - state.config.blank_before_locking {
|
||||||
|
if let Err(err) = blank_screen(&conn, &mut state) {
|
||||||
|
error!("POSSIBLY FAILED TO BLANK SCREEN: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.blanker_state < BlankerState::Locked && since_last_activity > state.config.lock_timeout {
|
||||||
|
if let Err(err) = lock_screen(&conn, &mut state) {
|
||||||
|
error!("POSSIBLY FAILED TO LOCK SCREEN: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_config() -> anyhow::Result<Configuration> {
|
||||||
|
use humantime::parse_duration;
|
||||||
|
use toml::Value;
|
||||||
|
|
||||||
|
match xdg::BaseDirectories::new()?.find_config_file("bscreensaver.toml") {
|
||||||
|
None => Ok(Configuration::default()),
|
||||||
|
Some(config_path) => {
|
||||||
|
let mut f = File::open(config_path)?;
|
||||||
|
let mut config = String::new();
|
||||||
|
f.read_to_string(&mut config)?;
|
||||||
|
drop(f);
|
||||||
|
|
||||||
|
let config_toml = config.parse::<Value>()?;
|
||||||
|
let mut config = Configuration::default();
|
||||||
|
|
||||||
|
config.lock_timeout = match config_toml.get("lock-timeout") {
|
||||||
|
None => config.lock_timeout,
|
||||||
|
Some(val) => parse_duration(val.as_str().ok_or(anyhow!("'lock-timeout' must be a duration string like '10m' or '90s'"))?)?,
|
||||||
|
};
|
||||||
|
config.blank_before_locking = match config_toml.get("blank-before-locking") {
|
||||||
|
None => config.blank_before_locking,
|
||||||
|
Some(val) => parse_duration(val.as_str().ok_or(anyhow!("'blank-before-locking' must be a duration string like '10m' or '90s'"))?)?,
|
||||||
|
};
|
||||||
|
config.dialog_backend = match config_toml.get("dialog-backend") {
|
||||||
|
None => config.dialog_backend,
|
||||||
|
Some(val) => DialogBackend::try_from(val.as_str().ok_or(anyhow!("'dialog-backend' must be a string"))?)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
if config.blank_before_locking >= config.lock_timeout {
|
||||||
|
Err(anyhow!("'blank-before-locking' cannot be greater than 'lock-timeout'"))
|
||||||
|
} else {
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 child = Command::new(format!("{}/{}", env!("HELPER_DIR"), binary_name))
|
||||||
|
.create_pidfd(true)
|
||||||
|
.spawn()?;
|
||||||
|
let pidfd = child.take_pidfd()?;
|
||||||
|
Ok((child, pidfd))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_dbus_service(state: &mut State) -> anyhow::Result<()> {
|
||||||
|
state.dbus_service = Some(start_subservice("bscreensaver-dbus-service")?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_systemd_service(state: &mut State) -> anyhow::Result<()> {
|
||||||
|
state.systemd_service = Some(start_subservice("bscreensaver-systemd")?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_subservice_quit<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 child = Command::new(format!("{}/{}", env!("HELPER_DIR"), state.config.dialog_backend.binary_name()))
|
||||||
|
.create_pidfd(true)
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
let mut child_out = child.stdout.take().unwrap();
|
||||||
|
let child_pidfd = child.take_pidfd()?;
|
||||||
|
|
||||||
|
let mut xid_buf: [u8; 4] = [0; 4];
|
||||||
|
child_out.read_exact(&mut xid_buf)?;
|
||||||
|
let client_window = {
|
||||||
|
let wid: u32 = ((xid_buf[0] as u32) << 24) | ((xid_buf[1] as u32) << 16) | ((xid_buf[2] as u32) << 8) | (xid_buf[3] as u32);
|
||||||
|
unsafe {
|
||||||
|
use xcb::XidNew;
|
||||||
|
x::Window::new(wid)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
debug!("Dialog process created plug window 0x{:x}", client_window.resource_id());
|
||||||
|
|
||||||
|
let cookie = conn.send_request(&x::GetWindowAttributes {
|
||||||
|
window: client_window,
|
||||||
|
});
|
||||||
|
let reply = conn.wait_for_reply(cookie)?;
|
||||||
|
if !reply.your_event_mask().contains(x::EventMask::STRUCTURE_NOTIFY) {
|
||||||
|
conn.send_and_check_request(&x::ChangeWindowAttributes {
|
||||||
|
window: client_window,
|
||||||
|
value_list: &[
|
||||||
|
x::Cw::EventMask(reply.your_event_mask() | x::EventMask::STRUCTURE_NOTIFY),
|
||||||
|
],
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let unlock_window = pointer_monitor.unlock_window;
|
||||||
|
let embedder = Embedder::start(conn, unlock_window, client_window)?;
|
||||||
|
|
||||||
|
Ok(UnlockDialog {
|
||||||
|
monitor: *pointer_monitor,
|
||||||
|
embedder,
|
||||||
|
event_to_forward: trigger_event,
|
||||||
|
child,
|
||||||
|
child_pidfd,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blank_screen(conn: &xcb::Connection, state: &mut State) -> anyhow::Result<()> {
|
||||||
|
if state.blanker_state >= BlankerState::Blanked {
|
||||||
|
return Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Blanking");
|
||||||
|
|
||||||
|
for monitor in &state.monitors {
|
||||||
|
let mut cookies = Vec::new();
|
||||||
|
|
||||||
|
cookies.push(conn.send_request_checked(&x::ConfigureWindow {
|
||||||
|
window: monitor.blanker_window,
|
||||||
|
value_list: &[
|
||||||
|
x::ConfigWindow::StackMode(x::StackMode::Above),
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
cookies.push(conn.send_request_checked(&x::MapWindow {
|
||||||
|
window: monitor.blanker_window,
|
||||||
|
}));
|
||||||
|
|
||||||
|
for cookie in cookies {
|
||||||
|
conn.check_request(cookie)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.blanker_state = BlankerState::Blanked;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unblank_screen(conn: &xcb::Connection, state: &mut State) -> anyhow::Result<()> {
|
||||||
|
if state.blanker_state != BlankerState::Blanked {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Unblanking");
|
||||||
|
|
||||||
|
let mut cookies = Vec::new();
|
||||||
|
for monitor in &state.monitors {
|
||||||
|
cookies.push(conn.send_request_checked(&x::UnmapWindow {
|
||||||
|
window: monitor.blanker_window,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
for cookie in cookies {
|
||||||
|
conn.check_request(cookie)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.blanker_state = BlankerState::Idle;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lock_screen(conn: &xcb::Connection, state: &mut State) -> anyhow::Result<()> {
|
||||||
|
blank_screen(conn, state)?;
|
||||||
|
if state.blanker_state >= BlankerState::Locked {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Locking");
|
||||||
|
|
||||||
|
for monitor in &state.monitors {
|
||||||
|
let mut cookies = Vec::new();
|
||||||
|
|
||||||
|
cookies.push(conn.send_request_checked(&x::ConfigureWindow {
|
||||||
|
window: monitor.unlock_window,
|
||||||
|
value_list: &[
|
||||||
|
x::ConfigWindow::StackMode(x::StackMode::Above),
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
cookies.push(conn.send_request_checked(&x::MapWindow {
|
||||||
|
window: monitor.unlock_window,
|
||||||
|
}));
|
||||||
|
|
||||||
|
for cookie in cookies {
|
||||||
|
conn.check_request(cookie)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cookie = conn.send_request(&x::GrabKeyboard {
|
||||||
|
owner_events: true,
|
||||||
|
grab_window: monitor.unlock_window,
|
||||||
|
time: x::CURRENT_TIME,
|
||||||
|
pointer_mode: x::GrabMode::Async,
|
||||||
|
keyboard_mode: x::GrabMode::Async,
|
||||||
|
});
|
||||||
|
let reply = conn.wait_for_reply(cookie)?;
|
||||||
|
if reply.status() != x::GrabStatus::Success {
|
||||||
|
// FIXME: try to grab later?
|
||||||
|
warn!("Failed to grab keyboard on window {:?}: {:?}", monitor.blanker_window, reply.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
let cookie = conn.send_request(&x::GrabPointer {
|
||||||
|
owner_events: true,
|
||||||
|
grab_window: monitor.unlock_window,
|
||||||
|
event_mask: x::EventMask::BUTTON_PRESS | x::EventMask::BUTTON_RELEASE | x::EventMask::POINTER_MOTION | x::EventMask::POINTER_MOTION_HINT,
|
||||||
|
pointer_mode: x::GrabMode::Async,
|
||||||
|
keyboard_mode: x::GrabMode::Async,
|
||||||
|
confine_to: monitor.blanker_window,
|
||||||
|
cursor: x::CURSOR_NONE,
|
||||||
|
time: x::CURRENT_TIME,
|
||||||
|
});
|
||||||
|
let reply = conn.wait_for_reply(cookie)?;
|
||||||
|
if reply.status() != x::GrabStatus::Success {
|
||||||
|
// FIXME: try to grab later?
|
||||||
|
warn!("Failed to grab pointer on window {:?}: {:?}", monitor.blanker_window, reply.status());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.blanker_state = BlankerState::Locked;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unlock_screen(conn: &xcb::Connection, state: &mut State) -> anyhow::Result<()> {
|
||||||
|
if state.blanker_state != BlankerState::Locked {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Unlocking");
|
||||||
|
|
||||||
|
let mut cookies = Vec::new();
|
||||||
|
for monitor in &state.monitors {
|
||||||
|
cookies.push(conn.send_request_checked(&x::UngrabKeyboard {
|
||||||
|
time: x::CURRENT_TIME,
|
||||||
|
}));
|
||||||
|
cookies.push(conn.send_request_checked(&x::UngrabPointer {
|
||||||
|
time: x::CURRENT_TIME,
|
||||||
|
}));
|
||||||
|
cookies.push(conn.send_request_checked(&x::UnmapWindow {
|
||||||
|
window: monitor.unlock_window,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
for cookie in cookies {
|
||||||
|
conn.check_request(cookie)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.blanker_state = BlankerState::Blanked;
|
||||||
|
unblank_screen(conn, state)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restart_daemon(state: &mut State) -> anyhow::Result<()> {
|
||||||
|
info!("Restarting");
|
||||||
|
|
||||||
|
let exe = read_link("/proc/self/exe")?;
|
||||||
|
let exe = CString::new(exe.to_str().ok_or(io::Error::new(io::ErrorKind::InvalidData, "Path cannot be converted to str".to_string()))?)?;
|
||||||
|
|
||||||
|
let argv = match state.blanker_state {
|
||||||
|
BlankerState::Idle => vec![],
|
||||||
|
BlankerState::Blanked => vec![CString::new("--".to_string() + BLANKED_ARG)?],
|
||||||
|
BlankerState::Locked => vec![CString::new("--".to_string() + LOCKED_ARG)?],
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = kill_child_processes(state) {
|
||||||
|
warn!("Failed to kill child processes: {}", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
match unsafe { fork() } {
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to fork: {}", err);
|
||||||
|
Err(err)?;
|
||||||
|
},
|
||||||
|
Ok(ForkResult::Parent { .. }) => exit(0),
|
||||||
|
Ok(ForkResult::Child) => {
|
||||||
|
if let Err(err) = setsid() {
|
||||||
|
warn!("Failed to start new session: {}", err);
|
||||||
|
}
|
||||||
|
execv(exe.as_c_str(), &argv)?;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exit_daemon(state: &mut State) -> anyhow::Result<()> {
|
||||||
|
info!("Quitting");
|
||||||
|
if let Err(err) = kill_child_processes(state) {
|
||||||
|
warn!("Failed to kill child processes: {}", err);
|
||||||
|
}
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kill_child_processes(state: &mut State) -> anyhow::Result<()> {
|
||||||
|
if let Some(mut unlock_dialog) = state.unlock_dialog.take() {
|
||||||
|
unlock_dialog.embedder.end().ok();
|
||||||
|
unlock_dialog.child.kill().ok();
|
||||||
|
unlock_dialog.child.try_wait().ok();
|
||||||
|
}
|
||||||
|
if let Some(mut dbus_service) = state.dbus_service.take() {
|
||||||
|
dbus_service.0.kill().ok();
|
||||||
|
dbus_service.0.try_wait().ok();
|
||||||
|
}
|
||||||
|
if let Some(mut systemd_service) = state.systemd_service.take() {
|
||||||
|
systemd_service.0.kill().ok();
|
||||||
|
systemd_service.0.try_wait().ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
1
rust-toolchain
Normal file
1
rust-toolchain
Normal file
@ -0,0 +1 @@
|
|||||||
|
nightly
|
18
systemd/Cargo.toml
Normal file
18
systemd/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "bscreensaver-systemd"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1"
|
||||||
|
async-std = { version = "1.11", features = ["attributes"] }
|
||||||
|
async-xcb = { path = "../async-xcb" }
|
||||||
|
bscreensaver-command = { path = "../command" }
|
||||||
|
bscreensaver-util = { path = "../util" }
|
||||||
|
futures = "0.3"
|
||||||
|
log = "0.4"
|
||||||
|
nix = "0.23"
|
||||||
|
# git source needed until extension event error resolution fix is released
|
||||||
|
xcb = { git = "https://github.com/rust-x-bindings/rust-xcb", rev = "d09b5f91bc07d56673f1bc0d6c7ecd72b5ff7b3e" }
|
||||||
|
zbus = "2.2"
|
||||||
|
logind-zbus = "3"
|
99
systemd/src/main.rs
Normal file
99
systemd/src/main.rs
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
use async_std::task;
|
||||||
|
use futures::{future::FutureExt, pin_mut, select, AsyncReadExt, StreamExt};
|
||||||
|
use log::{debug, error, info, warn};
|
||||||
|
use logind_zbus::manager::{InhibitType, ManagerProxy};
|
||||||
|
use std::{os::unix::io::AsRawFd, process::exit};
|
||||||
|
use zbus::Connection;
|
||||||
|
|
||||||
|
use bscreensaver_command::{bscreensaver_command, BCommand};
|
||||||
|
use bscreensaver_util::init_logging;
|
||||||
|
|
||||||
|
#[async_std::main]
|
||||||
|
async fn main() {
|
||||||
|
init_logging("BSCREENSAVER_SYSTEMD_LOG");
|
||||||
|
|
||||||
|
let xcb_handle = task::spawn(xcb_task()).fuse();
|
||||||
|
let dbus_handle = task::spawn(dbus_task()).fuse();
|
||||||
|
|
||||||
|
pin_mut!(xcb_handle, dbus_handle);
|
||||||
|
|
||||||
|
let res = loop {
|
||||||
|
select! {
|
||||||
|
_ = xcb_handle => {
|
||||||
|
info!("Lost connection to X server; quitting");
|
||||||
|
break Ok(());
|
||||||
|
},
|
||||||
|
res = dbus_handle => {
|
||||||
|
match res {
|
||||||
|
Err(err) => error!("Lost connection to the session bus: {}", err),
|
||||||
|
Ok(_) => error!("DBus task exited normally; this should not happen!"),
|
||||||
|
}
|
||||||
|
break Err(());
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(_) = res {
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn xcb_task() -> anyhow::Result<()> {
|
||||||
|
let (xcb_conn, _) = task::block_on(async { xcb::Connection::connect(None) })?;
|
||||||
|
let mut xcb_conn = async_xcb::AsyncConnection::new(xcb_conn)?;
|
||||||
|
|
||||||
|
// We need to drain the XCB connection periodically. Even though we have not
|
||||||
|
// asked for any events, we'll still get stuff like MappingNotify if the keyboard
|
||||||
|
// settings change.
|
||||||
|
loop {
|
||||||
|
let mut buf = [0u8; 512];
|
||||||
|
xcb_conn.read(&mut buf).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn dbus_task() -> anyhow::Result<()> {
|
||||||
|
|
||||||
|
let system_bus = Connection::system().await?;
|
||||||
|
let manager_proxy = ManagerProxy::new(&system_bus).await?;
|
||||||
|
let mut prepare_for_sleep_stream = manager_proxy.receive_prepare_for_sleep().await?;
|
||||||
|
let mut inhibit_fd = Some(register_sleep_lock(&manager_proxy).await?);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if let Some(prepare_for_sleep) = prepare_for_sleep_stream.next().await {
|
||||||
|
if *prepare_for_sleep.args()?.start() {
|
||||||
|
debug!("Preparing for sleep");
|
||||||
|
if let Err(err) = do_bscreensaver_command(BCommand::Lock).await {
|
||||||
|
warn!("Failed to lock screen: {}", err);
|
||||||
|
}
|
||||||
|
if let Some(fd) = inhibit_fd.take() {
|
||||||
|
if let Err(err) = nix::unistd::close(fd.as_raw_fd()) {
|
||||||
|
warn!("Failed to close sleep inhibit lock: {}", err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("No sleep lock present");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug!("Resuming from sleep");
|
||||||
|
if let Err(err) = do_bscreensaver_command(BCommand::Deactivate).await {
|
||||||
|
warn!("Failed to deactivate screen lock: {}", err);
|
||||||
|
}
|
||||||
|
inhibit_fd = Some(register_sleep_lock(&manager_proxy).await?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_bscreensaver_command(command: BCommand) -> anyhow::Result<()> {
|
||||||
|
task::block_on(async {
|
||||||
|
bscreensaver_command(command)
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn register_sleep_lock<'a>(manager_proxy: &ManagerProxy<'a>) -> anyhow::Result<zbus::zvariant::OwnedFd> {
|
||||||
|
debug!("Registering sleep lock");
|
||||||
|
// ManagerProxy uses RawFd for the return value, which rust's type system thinks is an i32,
|
||||||
|
// which means the generated proxy uses the wrong dbus type signature. So instead, use a raw
|
||||||
|
// Proxy instance and do it all ourselves.
|
||||||
|
Ok((*manager_proxy).call("Inhibit", &(InhibitType::Sleep, "bscreensaver", "blank before sleep", "delay")).await?)
|
||||||
|
}
|
13
util/Cargo.toml
Normal file
13
util/Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "bscreensaver-util"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = "3"
|
||||||
|
env_logger = "0.9"
|
||||||
|
lazy_static = "1"
|
||||||
|
libc = "0.2"
|
||||||
|
xcb = { git = "https://github.com/rust-x-bindings/rust-xcb", rev = "d09b5f91bc07d56673f1bc0d6c7ecd72b5ff7b3e", features = ["randr", "screensaver", "xfixes"] }
|
50
util/src/lib.rs
Normal file
50
util/src/lib.rs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
use std::{ffi::CStr, io};
|
||||||
|
use xcb::x;
|
||||||
|
|
||||||
|
pub const BSCREENSAVER_WM_CLASS: &[u8] = b"bscreensaver\0Bscreensaver\0";
|
||||||
|
|
||||||
|
pub fn init_logging(env_name: &str) {
|
||||||
|
env_logger::builder()
|
||||||
|
.format_timestamp_millis()
|
||||||
|
.parse_env(env_name)
|
||||||
|
.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_atom(conn: &xcb::Connection, name: &[u8]) -> xcb::Result<x::Atom> {
|
||||||
|
let cookie = conn.send_request(&x::InternAtom {
|
||||||
|
only_if_exists: false,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
Ok(conn.wait_for_reply(cookie)?.atom())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn destroy_gc(conn: &xcb::Connection, gc: x::Gcontext) -> xcb::Result<()> {
|
||||||
|
conn.send_and_check_request(&x::FreeGc {
|
||||||
|
gc,
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn destroy_window(conn: &xcb::Connection, window: x::Window) -> xcb::Result<()> {
|
||||||
|
conn.send_and_check_request(&x::DestroyWindow {
|
||||||
|
window,
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_username() -> io::Result<String> {
|
||||||
|
// SAFETY: libc must be sane
|
||||||
|
let uid = unsafe { libc::getuid() };
|
||||||
|
// SAFETY: libc must be sane
|
||||||
|
let pwd = unsafe { libc::getpwuid(uid) };
|
||||||
|
// SAFETY: null-check occurs on same line
|
||||||
|
if pwd.is_null() || unsafe { *pwd }.pw_name.is_null() {
|
||||||
|
Err(io::Error::new(io::ErrorKind::NotFound, "Username not found".to_string()))
|
||||||
|
} else {
|
||||||
|
// SAFETY: libc must be sane; null checks performed above
|
||||||
|
let cstr = unsafe { CStr::from_ptr((*pwd).pw_name) };
|
||||||
|
cstr.to_str()
|
||||||
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid UTF-8 data in username".to_string()))
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
}
|
||||||
|
}
|
18
xcb-xembed/Cargo.toml
Normal file
18
xcb-xembed/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "xcb-xembed"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = [
|
||||||
|
"Brian Tarricone <brian@tarricone.org>",
|
||||||
|
]
|
||||||
|
edition = "2021"
|
||||||
|
description = "XCB-based implementation of the X11 XEMBED protocol"
|
||||||
|
license = "LGPL-3.0"
|
||||||
|
repository = "https://github.com/kelnos/xcb-xembed"
|
||||||
|
readme = "README.md"
|
||||||
|
keywords = ["gui", "x11", "xcb", "xembed"]
|
||||||
|
categories = ["gui"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bitflags = "1"
|
||||||
|
log = "0.4"
|
||||||
|
xcb = { git = "https://github.com/rust-x-bindings/rust-xcb", rev = "d09b5f91bc07d56673f1bc0d6c7ecd72b5ff7b3e", features = ["randr", "screensaver", "xfixes"] }
|
341
xcb-xembed/src/embedder.rs
Normal file
341
xcb-xembed/src/embedder.rs
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
use log::{debug, info, trace};
|
||||||
|
use xcb::{x, xfixes, Xid};
|
||||||
|
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
pub struct Embedder<'a> {
|
||||||
|
conn: &'a xcb::Connection,
|
||||||
|
embedder: x::Window,
|
||||||
|
client: x::Window,
|
||||||
|
flags: XEmbedFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct XEmbedInfo {
|
||||||
|
version: u32,
|
||||||
|
flags: XEmbedFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Embedder<'a> {
|
||||||
|
pub fn start(conn: &'a xcb::Connection, embedder: x::Window, client: x::Window) -> Result<Embedder<'a>, Error> {
|
||||||
|
debug!("Reparenting {} to be a child of {}", client.resource_id(), embedder.resource_id());
|
||||||
|
conn.send_and_check_request(&x::ReparentWindow {
|
||||||
|
window: client,
|
||||||
|
parent: embedder,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let event_mask = x::EventMask::PROPERTY_CHANGE | x::EventMask::STRUCTURE_NOTIFY;
|
||||||
|
let cookie = conn.send_request(&x::GetWindowAttributes {
|
||||||
|
window: client,
|
||||||
|
});
|
||||||
|
let reply = conn.wait_for_reply(cookie)?;
|
||||||
|
if reply.your_event_mask().intersection(event_mask) != event_mask {
|
||||||
|
conn.send_and_check_request(&x::ChangeWindowAttributes {
|
||||||
|
window: client,
|
||||||
|
value_list: &[
|
||||||
|
x::Cw::EventMask(reply.your_event_mask() | event_mask),
|
||||||
|
],
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let info = fetch_xembed_info(conn, client)?;
|
||||||
|
let supported_version = std::cmp::min(info.version, XEMBED_VERSION);
|
||||||
|
|
||||||
|
if let Err(err) = conn.send_and_check_request(&xfixes::ChangeSaveSet {
|
||||||
|
mode: xfixes::SaveSetMode::Insert,
|
||||||
|
target: xfixes::SaveSetTarget::Root,
|
||||||
|
map: xfixes::SaveSetMapping::Unmap,
|
||||||
|
window: client,
|
||||||
|
}) {
|
||||||
|
info!("Failed to send XFIXES ChangeSaveSet request: {}", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("sending EMBEDDED_NOTIFY to client with embedder wid {} and version {}", embedder.resource_id(), supported_version);
|
||||||
|
send_xembed_message(conn, client, x::CURRENT_TIME, XEmbedMessage::EmbeddedNotify, None, Some(embedder.resource_id()), Some(supported_version))?;
|
||||||
|
|
||||||
|
let cookie = conn.send_request(&x::GetInputFocus {});
|
||||||
|
let reply = conn.wait_for_reply(cookie)?;
|
||||||
|
let focus_window = reply.focus();
|
||||||
|
|
||||||
|
let mut cur_window = embedder;
|
||||||
|
let activated_message = if focus_window == embedder {
|
||||||
|
XEmbedMessage::WindowActivate
|
||||||
|
} else {
|
||||||
|
loop {
|
||||||
|
let cookie = conn.send_request(&x::QueryTree {
|
||||||
|
window: cur_window,
|
||||||
|
});
|
||||||
|
let reply = conn.wait_for_reply(cookie)?;
|
||||||
|
if reply.root() == reply.parent() {
|
||||||
|
break XEmbedMessage::WindowDeactivate;
|
||||||
|
} else if reply.parent() == focus_window {
|
||||||
|
break XEmbedMessage::WindowActivate;
|
||||||
|
} else {
|
||||||
|
cur_window = reply.parent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
send_xembed_message(conn, client, x::CURRENT_TIME, activated_message, None, None, None)?;
|
||||||
|
|
||||||
|
let (focus_message, focus_detail) = if focus_window == embedder {
|
||||||
|
(XEmbedMessage::FocusIn, Some(XEmbedFocus::Current))
|
||||||
|
} else {
|
||||||
|
(XEmbedMessage::FocusOut, None)
|
||||||
|
};
|
||||||
|
send_xembed_message(conn, client, x::CURRENT_TIME, focus_message, focus_detail.map(|fd| fd as u32), None, None)?;
|
||||||
|
|
||||||
|
// XXX: how do we know if there's something modal active?
|
||||||
|
send_xembed_message(conn, client, x::CURRENT_TIME, XEmbedMessage::ModalityOff, None, None, None)?;
|
||||||
|
|
||||||
|
Ok(Embedder {
|
||||||
|
conn,
|
||||||
|
embedder,
|
||||||
|
client,
|
||||||
|
flags: info.flags,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end(self) -> Result<(), Error> {
|
||||||
|
if self.client == x::WINDOW_NONE {
|
||||||
|
return Err(Error::ClientDestroyed);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("Ending XEMBED");
|
||||||
|
let cookie = self.conn.send_request(&x::QueryTree {
|
||||||
|
window: self.client,
|
||||||
|
});
|
||||||
|
let reply = self.conn.wait_for_reply(cookie)?;
|
||||||
|
self.conn.send_and_check_request(&x::UnmapWindow {
|
||||||
|
window: self.client,
|
||||||
|
})?;
|
||||||
|
self.conn.send_and_check_request(&x::ReparentWindow {
|
||||||
|
window: self.client,
|
||||||
|
parent: reply.root(),
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn event(&mut self, event: &xcb::Event) -> Result<bool, Error> {
|
||||||
|
if self.client == x::WINDOW_NONE {
|
||||||
|
return Err(Error::ClientDestroyed);
|
||||||
|
}
|
||||||
|
|
||||||
|
match event {
|
||||||
|
xcb::Event::X(x::Event::PropertyNotify(ev)) if ev.window() == self.client && ev.atom() == intern_atom(self.conn, XEMBED_INFO_ATOM_NAME)? => {
|
||||||
|
let info = fetch_xembed_info(self.conn, self.client)?;
|
||||||
|
if (self.flags & XEmbedFlags::MAPPED) != (info.flags & XEmbedFlags::MAPPED) {
|
||||||
|
if info.flags.contains(XEmbedFlags::MAPPED) {
|
||||||
|
debug!("Mapping client window");
|
||||||
|
self.conn.send_and_check_request(&x::MapWindow {
|
||||||
|
window: self.client,
|
||||||
|
})?;
|
||||||
|
} else {
|
||||||
|
debug!("Unmapping client window");
|
||||||
|
self.conn.send_and_check_request(&x::UnmapWindow {
|
||||||
|
window: self.client,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
self.flags = info.flags;
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
},
|
||||||
|
xcb::Event::X(x::Event::ConfigureNotify(ev)) if ev.window() == self.client => {
|
||||||
|
let mut cookies = Vec::new();
|
||||||
|
if ev.x() != 0 || ev.y() != 0 {
|
||||||
|
cookies.push(self.conn.send_request_checked(&x::ConfigureWindow {
|
||||||
|
window: self.client,
|
||||||
|
value_list: &[
|
||||||
|
x::ConfigWindow::X(0),
|
||||||
|
x::ConfigWindow::Y(0),
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
cookies.push(self.conn.send_request_checked(&x::ConfigureWindow {
|
||||||
|
window: self.embedder,
|
||||||
|
value_list: &[
|
||||||
|
x::ConfigWindow::Width(ev.width() as u32),
|
||||||
|
x::ConfigWindow::Height(ev.height() as u32),
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
for cookie in cookies {
|
||||||
|
self.conn.check_request(cookie)?;
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
},
|
||||||
|
xcb::Event::X(x::Event::ClientMessage(ev)) if ev.window() == self.embedder && ev.r#type() == intern_atom(self.conn, XEMBED_MESSAGE_ATOM_NAME)? => {
|
||||||
|
match ev.data() {
|
||||||
|
x::ClientMessageData::Data32(data) if data[1] == XEmbedMessage::RequestFocus as u32 => {
|
||||||
|
debug!("Client requests focus");
|
||||||
|
self.conn.send_and_check_request(&x::SetInputFocus {
|
||||||
|
revert_to: x::InputFocus::Parent,
|
||||||
|
focus: self.client,
|
||||||
|
time: x::CURRENT_TIME,
|
||||||
|
})?;
|
||||||
|
send_xembed_message(self.conn, self.client, x::CURRENT_TIME, XEmbedMessage::FocusIn, Some(XEmbedFocus::Current as u32), None, None)?;
|
||||||
|
Ok(true)
|
||||||
|
},
|
||||||
|
// TODO: XEMBED_FOCUS_NEXT
|
||||||
|
// TODO: XEMBED_FOCUS_PREV
|
||||||
|
// TODO: XEMBED_REGISTER_ACCELERATOR
|
||||||
|
// TODO: XEMBED_UNREGISTER_ACCELERATOR
|
||||||
|
_ => Ok(false),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
xcb::Event::X(x::Event::KeyPress(ev)) if ev.event() == self.embedder && self.flags.contains(XEmbedFlags::MAPPED) => {
|
||||||
|
trace!("Forwarding key press to client ({:?} + {})", ev.state(), ev.detail());
|
||||||
|
self.conn.send_and_check_request(&x::SendEvent {
|
||||||
|
propagate: false,
|
||||||
|
destination: x::SendEventDest::Window(self.client),
|
||||||
|
event_mask: x::EventMask::NO_EVENT,
|
||||||
|
event: &x::KeyPressEvent::new(ev.detail(), ev.time(), ev.root(), self.client, ev.child(), ev.root_x(), ev.root_y(), ev.event_x(), ev.event_y(), ev.state(), ev.same_screen()),
|
||||||
|
})?;
|
||||||
|
Ok(true)
|
||||||
|
},
|
||||||
|
/*
|
||||||
|
xcb::Event::X(x::Event::KeyRelease(ev)) if ev.event() == self.embedder && self.flags.contains(XEmbedFlags::MAPPED) => {
|
||||||
|
trace!("Forwarding key release to client ({:?} + {})", ev.state(), ev.detail());
|
||||||
|
self.conn.send_and_check_request(&x::SendEvent {
|
||||||
|
propagate: false,
|
||||||
|
destination: x::SendEventDest::Window(self.client),
|
||||||
|
event_mask: x::EventMask::NO_EVENT,
|
||||||
|
event: &x::KeyReleaseEvent::new(ev.detail(), ev.time(), ev.root(), self.client, ev.child(), ev.root_x(), ev.root_y(), ev.event_x(), ev.event_y(), ev.state(), ev.same_screen()),
|
||||||
|
})?;
|
||||||
|
Ok(true)
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
xcb::Event::X(x::Event::MotionNotify(ev)) if ev.event() == self.embedder && self.flags.contains(XEmbedFlags::MAPPED) => {
|
||||||
|
trace!("Forwarding pointer motion to client ({}, {})", ev.event_x(), ev.event_y());
|
||||||
|
self.conn.send_and_check_request(&x::SendEvent {
|
||||||
|
propagate: false,
|
||||||
|
destination: x::SendEventDest::Window(self.client),
|
||||||
|
event_mask: x::EventMask::NO_EVENT,
|
||||||
|
event: &x::MotionNotifyEvent::new(ev.detail(), ev.time(), ev.root(), self.client, ev.child(), ev.root_x(), ev.root_y(), ev.event_x(), ev.event_y(), ev.state(), ev.same_screen()),
|
||||||
|
})?;
|
||||||
|
Ok(true)
|
||||||
|
},
|
||||||
|
xcb::Event::X(x::Event::ButtonPress(ev)) if ev.event() == self.embedder && self.flags.contains(XEmbedFlags::MAPPED) => {
|
||||||
|
trace!("Forwarding button press to client ({:?} + {}: {}, {})", ev.state(), ev.detail(), ev.event_x(), ev.event_y());
|
||||||
|
self.conn.send_and_check_request(&x::SendEvent {
|
||||||
|
propagate: false,
|
||||||
|
destination: x::SendEventDest::Window(self.client),
|
||||||
|
event_mask: x::EventMask::NO_EVENT,
|
||||||
|
event: &x::ButtonPressEvent::new(ev.detail(), ev.time(), ev.root(), self.client, ev.child(), ev.root_x(), ev.root_y(), ev.event_x(), ev.event_y(), ev.state(), ev.same_screen()),
|
||||||
|
})?;
|
||||||
|
Ok(true)
|
||||||
|
},
|
||||||
|
xcb::Event::X(x::Event::ButtonRelease(ev)) if ev.event() == self.embedder && self.flags.contains(XEmbedFlags::MAPPED) => {
|
||||||
|
trace!("Forwarding button release to client ({:?} + {}: {}, {})", ev.state(), ev.detail(), ev.event_x(), ev.event_y());
|
||||||
|
self.conn.send_and_check_request(&x::SendEvent {
|
||||||
|
propagate: false,
|
||||||
|
destination: x::SendEventDest::Window(self.client),
|
||||||
|
event_mask: x::EventMask::NO_EVENT,
|
||||||
|
event: &x::ButtonReleaseEvent::new(ev.detail(), ev.time(), ev.root(), self.client, ev.child(), ev.root_x(), ev.root_y(), ev.event_x(), ev.event_y(), ev.state(), ev.same_screen()),
|
||||||
|
})?;
|
||||||
|
Ok(true)
|
||||||
|
},
|
||||||
|
xcb::Event::X(x::Event::UnmapNotify(ev)) if ev.window() == self.client => {
|
||||||
|
debug!("Client was unmapped");
|
||||||
|
self.flags -= XEmbedFlags::MAPPED;
|
||||||
|
Ok(true)
|
||||||
|
},
|
||||||
|
xcb::Event::X(x::Event::DestroyNotify(ev)) if ev.window() == self.client => {
|
||||||
|
debug!("Client was destroyed");
|
||||||
|
self.flags -= XEmbedFlags::MAPPED;
|
||||||
|
self.client = x::WINDOW_NONE;
|
||||||
|
Ok(true)
|
||||||
|
},
|
||||||
|
event => {
|
||||||
|
trace!("Not handling event {:#?}", event);
|
||||||
|
Ok(false)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn activate_client(&self) -> Result<(), Error> {
|
||||||
|
if self.client != x::WINDOW_NONE {
|
||||||
|
send_xembed_message(&self.conn, self.client, x::CURRENT_TIME, XEmbedMessage::WindowActivate, None, None, None)?;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::ClientDestroyed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_client(&self, mode: super::XEmbedFocus) -> Result<(), Error> {
|
||||||
|
if self.client != x::WINDOW_NONE {
|
||||||
|
self.conn.send_and_check_request(&x::SetInputFocus {
|
||||||
|
revert_to: x::InputFocus::Parent,
|
||||||
|
focus: self.client,
|
||||||
|
time: x::CURRENT_TIME,
|
||||||
|
})?;
|
||||||
|
send_xembed_message(&self.conn, self.client, x::CURRENT_TIME, XEmbedMessage::FocusIn, Some(mode as u32), None, None)?;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::ClientDestroyed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn embedder_window(&self) -> x::Window {
|
||||||
|
self.embedder
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn client_window(&self) -> x::Window {
|
||||||
|
self.client
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_client_mapped(&self) -> bool {
|
||||||
|
self.flags.contains(XEmbedFlags::MAPPED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
delete: false,
|
||||||
|
window: client,
|
||||||
|
property: xembed_info_atom,
|
||||||
|
r#type: xembed_info_atom,
|
||||||
|
long_offset: 0,
|
||||||
|
long_length: 8,
|
||||||
|
});
|
||||||
|
let reply = conn.wait_for_reply(cookie)?;
|
||||||
|
if reply.value::<u32>().len() < 2 {
|
||||||
|
Err(Error::ProtocolError("Invalid format of _XEMBED_INFO property".to_string()))
|
||||||
|
} else {
|
||||||
|
debug!("_XEMBED_INFO -> ({}, 0x{:x})", reply.value::<u32>()[0], reply.value::<u32>()[1]);
|
||||||
|
Ok(XEmbedInfo {
|
||||||
|
version: reply.value()[0],
|
||||||
|
flags: XEmbedFlags::from_bits_truncate(reply.value()[1]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn intern_atom(conn: &xcb::Connection, name: &str) -> xcb::Result<x::Atom> {
|
||||||
|
let cookie = conn.send_request(&x::InternAtom {
|
||||||
|
only_if_exists: false,
|
||||||
|
name: name.as_bytes(),
|
||||||
|
});
|
||||||
|
conn.wait_for_reply(cookie).map(|reply| reply.atom())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_xembed_message(conn: &xcb::Connection, window: x::Window, time: u32, message: XEmbedMessage, detail: Option<u32>, data1: Option<u32>, data2: Option<u32>) -> xcb::Result<()> {
|
||||||
|
conn.send_and_check_request(&x::SendEvent {
|
||||||
|
propagate: false,
|
||||||
|
destination: x::SendEventDest::Window(window),
|
||||||
|
event_mask: x::EventMask::NO_EVENT,
|
||||||
|
event: &x::ClientMessageEvent::new(
|
||||||
|
window,
|
||||||
|
intern_atom(conn, XEMBED_MESSAGE_ATOM_NAME)?,
|
||||||
|
x::ClientMessageData::Data32([
|
||||||
|
time,
|
||||||
|
message as u32,
|
||||||
|
detail.unwrap_or(0),
|
||||||
|
data1.unwrap_or(0),
|
||||||
|
data2.unwrap_or(0),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
96
xcb-xembed/src/lib.rs
Normal file
96
xcb-xembed/src/lib.rs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
use bitflags::bitflags;
|
||||||
|
use std::{
|
||||||
|
error::Error as StdError,
|
||||||
|
fmt,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod embedder;
|
||||||
|
|
||||||
|
pub(crate) const XEMBED_VERSION: u32 = 0;
|
||||||
|
pub(crate) const XEMBED_INFO_ATOM_NAME: &str = "_XEMBED_INFO";
|
||||||
|
pub(crate) const XEMBED_MESSAGE_ATOM_NAME: &str = "_XEMBED";
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) enum XEmbedMessage {
|
||||||
|
EmbeddedNotify = 0,
|
||||||
|
WindowActivate = 1,
|
||||||
|
WindowDeactivate = 2,
|
||||||
|
RequestFocus = 3,
|
||||||
|
FocusIn = 4,
|
||||||
|
FocusOut = 5,
|
||||||
|
FocusNext = 6,
|
||||||
|
FocusPrev = 7,
|
||||||
|
ModalityOn = 10,
|
||||||
|
ModalityOff = 11,
|
||||||
|
RegisterAccelerator = 12,
|
||||||
|
UnregisterAccelerator = 13,
|
||||||
|
ActivateAccelerator = 14,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum XEmbedFocus {
|
||||||
|
Current = 0,
|
||||||
|
First = 1,
|
||||||
|
Last = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
bitflags! {
|
||||||
|
struct XEmbedFlags: u32 {
|
||||||
|
const MAPPED = (1 << 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct XEmbedModifier: u32 {
|
||||||
|
const SHIFT = (1 << 0);
|
||||||
|
const CONTROL = (1 << 1);
|
||||||
|
const ALT = (1 << 2);
|
||||||
|
const SUPER = (1 << 3);
|
||||||
|
const HYPER = (1 << 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct XEmbedAcceleratorFlags: u32 {
|
||||||
|
const OVERLOADED = (1 << 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
ProtocolError(String),
|
||||||
|
Xcb(xcb::Error),
|
||||||
|
ClientDestroyed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::ProtocolError(reason) => write!(f, "XEMBED: Protocol error: {}", reason),
|
||||||
|
Self::Xcb(err) => write!(f, "XEMBED: {}", err),
|
||||||
|
Self::ClientDestroyed => write!(f, "XEMBED: client destroyed"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StdError for Error {
|
||||||
|
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||||
|
match self {
|
||||||
|
Self::Xcb(err) => Some(err),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<xcb::Error> for Error {
|
||||||
|
fn from(error: xcb::Error) -> Self {
|
||||||
|
Self::Xcb(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<xcb::ProtocolError> for Error {
|
||||||
|
fn from(error: xcb::ProtocolError) -> Self {
|
||||||
|
Self::Xcb(xcb::Error::Protocol(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<xcb::ConnError> for Error {
|
||||||
|
fn from(error: xcb::ConnError) -> Self {
|
||||||
|
Self::Xcb(xcb::Error::Connection(error))
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user