Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f6c50c998 |
@@ -36,8 +36,6 @@
|
|||||||
mod runner;
|
mod runner;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use blocker_platform_linux::kwin::BUS_NAME;
|
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
/// `~/.local/share/blocker/blocker.db`, honouring `XDG_DATA_HOME`.
|
/// `~/.local/share/blocker/blocker.db`, honouring `XDG_DATA_HOME`.
|
||||||
@@ -107,29 +105,13 @@ fn main() -> ExitCode {
|
|||||||
|
|
||||||
// The event loop, running for real.
|
// The event loop, running for real.
|
||||||
//
|
//
|
||||||
// The KWin path is preferred, and it requires owning the D-Bus name BEFORE
|
// X11 is wired here rather than KWin because the KWin path needs the D-Bus
|
||||||
// the script is loaded: `callDBus` has nothing to resolve until the daemon
|
// watcher server — the daemon owning `vu.valiq.blocker` and exporting the
|
||||||
// holds `vu.valiq.blocker`, so loading first would silently drop every
|
// interface `callDBus` targets — which is not built yet. Rather than stub it,
|
||||||
// focus event. If KWin is unavailable we fall back to X11 and say what that
|
// the loop runs on the backend that works today and says which one it picked.
|
||||||
// costs — on a Wayland session, XWayland clients only.
|
// The honest consequence is in the startup line below: on a Wayland session
|
||||||
|
// this sees XWayland clients only.
|
||||||
let (tx, rx) = std::sync::mpsc::channel();
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
|
|
||||||
if let Ok(backend) = blocker_platform_linux::kwin::KwinBackend::connect() {
|
|
||||||
println!("backend: kwin — native Wayland windows are visible");
|
|
||||||
let _keep_connection = match blocker_platform_linux::watcher::serve(tx) {
|
|
||||||
Ok(c) => c,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("blocker-daemon: cannot own {BUS_NAME}: {e}");
|
|
||||||
return ExitCode::FAILURE;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
println!("owning the D-Bus name; entering the event loop");
|
|
||||||
run_loop(&rx, &schedules, &usage, &context, blocker_platform_linux::Enforcement::Freeze);
|
|
||||||
backend.shutdown();
|
|
||||||
return ExitCode::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("backend: kwin unavailable; falling back to x11");
|
|
||||||
match blocker_platform_linux::x11::X11Backend::connect() {
|
match blocker_platform_linux::x11::X11Backend::connect() {
|
||||||
Ok(backend) => {
|
Ok(backend) => {
|
||||||
println!("backend: x11 — native Wayland apps are NOT visible through this");
|
println!("backend: x11 — native Wayland apps are NOT visible through this");
|
||||||
@@ -184,38 +166,6 @@ fn main() -> ExitCode {
|
|||||||
ExitCode::SUCCESS
|
ExitCode::SUCCESS
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The event loop, shared by both backends.
|
|
||||||
///
|
|
||||||
/// Both paths park here: the caller has already put a focus-event producer in
|
|
||||||
/// charge, and this consumes it. Two blocked threads and no CPU between events.
|
|
||||||
fn run_loop(
|
|
||||||
rx: &std::sync::mpsc::Receiver<blocker_platform_linux::FocusEvent>,
|
|
||||||
schedules: &[blocker_core::model::Schedule],
|
|
||||||
usage: &blocker_core::engine::UsageSnapshot,
|
|
||||||
context: &blocker_core::engine::PlatformContext,
|
|
||||||
enforcement: blocker_platform_linux::Enforcement,
|
|
||||||
) {
|
|
||||||
let ctx = runner::LoopContext {
|
|
||||||
schedules,
|
|
||||||
usage,
|
|
||||||
platform_context: context,
|
|
||||||
boot_id: blocker_core::engine::BootId(0),
|
|
||||||
enforcement,
|
|
||||||
};
|
|
||||||
let clock = blocker_platform_linux::SystemMonotonic;
|
|
||||||
println!("enforcement: {enforcement:?}; entering the event loop");
|
|
||||||
'events: loop {
|
|
||||||
let (action, blocked) =
|
|
||||||
runner::step(rx, std::time::Duration::from_secs(3600), &ctx, &clock, now());
|
|
||||||
if blocked > 0 {
|
|
||||||
println!("blocked an app ({action:?})");
|
|
||||||
}
|
|
||||||
if action == runner::Action::Idle {
|
|
||||||
break 'events;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Civil time, from the platform layer.
|
/// Civil time, from the platform layer.
|
||||||
///
|
///
|
||||||
/// A placeholder reading rather than a clock read: the loop's correctness
|
/// A placeholder reading rather than a clock read: the loop's correctness
|
||||||
|
|||||||
@@ -41,7 +41,6 @@
|
|||||||
|
|
||||||
pub mod enforce;
|
pub mod enforce;
|
||||||
pub mod kwin;
|
pub mod kwin;
|
||||||
pub mod watcher;
|
|
||||||
pub mod x11;
|
pub mod x11;
|
||||||
|
|
||||||
pub use enforce::{MonotonicSource, SystemMonotonic};
|
pub use enforce::{MonotonicSource, SystemMonotonic};
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
//! 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user