150 lines
5.8 KiB
Rust
150 lines
5.8 KiB
Rust
//! The D-Bus watcher server: the daemon's public side of the KWin handshake.
|
|
//!
|
|
//! KWin calls *us*, not the other way round. The compositor has no way to push a
|
|
//! focus event to an arbitrary subscriber, so the daemon owns a bus name and
|
|
//! exports an interface that the loaded KWin script calls into. This module owns
|
|
//! that name and turns those calls into [`FocusEvent`]s on a channel.
|
|
//!
|
|
//! # Owning the name is not optional
|
|
//!
|
|
//! Without the daemon holding `vu.valiq.blocker`, KWin's `callDBus` has nothing
|
|
//! to resolve and every focus event is silently discarded. That is why
|
|
//! [`serve`] must succeed *before* the script is loaded.
|
|
//!
|
|
//! # Fail-open
|
|
//!
|
|
//! If the daemon is not running, the KWin script's `callDBus` fails inside a
|
|
//! signal handler and the compositor is left alone — no exception, no log spam.
|
|
//! Blocking stops; the desktop keeps working. That is section 1.5.4's rule and
|
|
//! it is why the script swallows its errors.
|
|
|
|
use std::sync::mpsc::Sender;
|
|
|
|
use zbus::blocking::{connection::Builder, Connection};
|
|
|
|
use super::kwin::{BUS_NAME, OBJECT_PATH};
|
|
use super::{parse_payload, FocusEvent};
|
|
|
|
/// Why the watcher could not start.
|
|
#[derive(Debug)]
|
|
pub enum WatcherError {
|
|
/// No session bus, so there is nothing to own a name on. Almost always
|
|
/// means the daemon was started outside a desktop session — over SSH, or
|
|
/// from a system unit rather than a user one.
|
|
NoSessionBus(String),
|
|
/// The name could not be acquired, which normally means another instance is
|
|
/// already running.
|
|
NameTaken(String),
|
|
/// Anything else zbus reports.
|
|
Other(String),
|
|
}
|
|
|
|
impl std::fmt::Display for WatcherError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::NoSessionBus(e) => write!(f, "no session bus: {e}"),
|
|
Self::NameTaken(e) => write!(f, "another watcher already owns {BUS_NAME}: {e}"),
|
|
Self::Other(e) => write!(f, "watcher could not start: {e}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for WatcherError {}
|
|
|
|
/// The object the KWin script calls into.
|
|
///
|
|
/// One method, `Focused`, whose payload is the tab-separated window description
|
|
/// built by `kwin::SCRIPT_JS`. An empty payload means focus was cleared.
|
|
struct Watcher {
|
|
sink: Sender<FocusEvent>,
|
|
}
|
|
|
|
#[zbus::interface(name = "vu.valiq.blocker.Watcher")]
|
|
impl Watcher {
|
|
/// Called by the KWin script on every focus change.
|
|
///
|
|
/// Payload is tab-separated: resourceClass, resourceName, caption,
|
|
/// internalId, pid — in the order `kwin::SCRIPT_JS` writes them.
|
|
fn focused(&self, payload: &str) {
|
|
let event = match parse_payload(payload) {
|
|
Some(window) => FocusEvent::Activated(window),
|
|
None => FocusEvent::Cleared,
|
|
};
|
|
// A send failure means the daemon has stopped. Nothing to escalate: the
|
|
// thread exits when the connection drops.
|
|
let _ = self.sink.send(event);
|
|
}
|
|
|
|
/// Instruct the daemon to reload schedules from the database immediately.
|
|
fn reload_schedules(&self) {
|
|
let _ = self.sink.send(FocusEvent::ReloadSchedules);
|
|
}
|
|
|
|
/// Ping method to check if the daemon is alive.
|
|
fn ping(&self) -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
/// Acquire the bus name and export [`Watcher`], returning the live connection.
|
|
///
|
|
/// The connection must be held for the export to stay registered, so the caller
|
|
/// keeps it for the daemon's lifetime.
|
|
///
|
|
/// Note on ordering: the daemon calls [`serve`] *before*
|
|
/// [`super::kwin::KwinBackend::connect`]. Loading the script first would start a
|
|
/// handler calling a name nobody owns, and every event would be lost.
|
|
pub fn serve(sink: Sender<FocusEvent>) -> Result<Connection, WatcherError> {
|
|
Builder::session()
|
|
.map_err(|e| WatcherError::NoSessionBus(e.to_string()))?
|
|
.name(BUS_NAME)
|
|
.map_err(|e| WatcherError::NameTaken(e.to_string()))?
|
|
// `#[zbus::interface]` implements `Interface` for the struct, so the
|
|
// instance itself is what gets registered — it holds the channel.
|
|
.serve_at(OBJECT_PATH, Watcher { sink })
|
|
.map_err(|e| WatcherError::Other(e.to_string()))?
|
|
.build()
|
|
.map_err(|e| {
|
|
// zbus cannot distinguish "name already owned" from other failures
|
|
// at build time, so the message is what separates them. A name clash
|
|
// is the common case and deserves the clearer variant.
|
|
let rendered = e.to_string();
|
|
if rendered.contains("taken") || rendered.contains("already") {
|
|
WatcherError::NameTaken(rendered)
|
|
} else {
|
|
WatcherError::Other(rendered)
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Drop the bus name, so a restarted daemon does not collide with a stale one.
|
|
///
|
|
/// Called before exiting. Not cosmetic: systemd `Restart=always` can start the
|
|
/// replacement before the old process is fully gone, and losing that race would
|
|
/// leave the new instance wedged on a name its predecessor still holds.
|
|
pub fn release(connection: &Connection) {
|
|
let _ = connection.release_name(BUS_NAME);
|
|
}
|
|
|
|
/// Tell the running daemon via D-Bus to reload schedules from storage.
|
|
pub fn notify_daemon_reload() -> Result<(), zbus::Error> {
|
|
let conn = Connection::session()?;
|
|
let proxy =
|
|
zbus::blocking::Proxy::new(&conn, BUS_NAME, OBJECT_PATH, "vu.valiq.blocker.Watcher")?;
|
|
let _: () = proxy.call("ReloadSchedules", &())?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Query if the daemon is running and responding on D-Bus.
|
|
pub fn is_daemon_running() -> bool {
|
|
if let Ok(conn) = Connection::session() {
|
|
if let Ok(proxy) =
|
|
zbus::blocking::Proxy::new(&conn, BUS_NAME, OBJECT_PATH, "vu.valiq.blocker.Watcher")
|
|
{
|
|
let res: zbus::Result<bool> = proxy.call("Ping", &());
|
|
return res.unwrap_or(false);
|
|
}
|
|
}
|
|
false
|
|
}
|