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:
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))
|
||||
}
|
Reference in New Issue
Block a user