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:
Brian Tarricone 2022-05-03 17:05:06 -07:00
commit 2e86445c3d
29 changed files with 4597 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
target/
.tags

1938
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

14
Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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: &gtk::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
View 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
View 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
View 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
View File

27
locker/Cargo.toml Normal file
View 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
View 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
View File

@ -0,0 +1 @@
nightly

18
systemd/Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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))
}
}