feat(linux): wire the KWin path end to end, and verify it live
The daemon now owns `vu.valiq.blocker`, loads the KWin script, and enters the event loop on the primary backend. Verified against the running session: ``` $ blocker-daemon blocker-daemon 0.1.0 (M0/M1) database: /tmp/bsmoke/blocker/blocker.db backend: kwin — native Wayland windows are visible owning the D-Bus name; entering the event loop enforcement: Freeze; entering the event loop ``` The process then parked until killed — which is the point. There is no tick; the loop blocks on a focus event and on the engine's `next_wakeup`. **Ordering is load-bearing and worth stating.** `callDBus` has nothing to resolve until the daemon holds the bus name, so the name must be acquired *before* the script is loaded. Loading first means a handler calling a name nobody owns, and every focus event silently dropped. **A real defect found by running it rather than reading it.** After the daemon was killed, `isScriptLoaded` still returned `true` — the script was left in KWin. `Drop` does not run on SIGTERM, so the cleanup path never executes. It is fail-open and harmless (the script's `callDBus` fails inside the compositor, which is exactly the section 1.5.4 behaviour), and `KwinBackend::connect` unloads any stale script before loading a fresh one, so a restart recovers. But "harmless" is not "correct", and the honest fix — a systemd `ExecStopPost` or a signal handler that unloads — is not in this PR. Also fixes the duplicated loop body: both backends now share one `run_loop`, so the X11 and KWin paths cannot drift into enforcing differently. Verified: clippy `-D warnings` clean, `fmt --check` clean, all tests passing, stripped daemon within the 5 MB budget. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,8 @@
|
||||
mod runner;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use blocker_platform_linux::kwin::BUS_NAME;
|
||||
use std::process::ExitCode;
|
||||
|
||||
/// `~/.local/share/blocker/blocker.db`, honouring `XDG_DATA_HOME`.
|
||||
@@ -105,13 +107,29 @@ fn main() -> ExitCode {
|
||||
|
||||
// The event loop, running for real.
|
||||
//
|
||||
// X11 is wired here rather than KWin because the KWin path needs the D-Bus
|
||||
// watcher server — the daemon owning `vu.valiq.blocker` and exporting the
|
||||
// interface `callDBus` targets — which is not built yet. Rather than stub it,
|
||||
// the loop runs on the backend that works today and says which one it picked.
|
||||
// The honest consequence is in the startup line below: on a Wayland session
|
||||
// this sees XWayland clients only.
|
||||
// The KWin path is preferred, and it requires owning the D-Bus name BEFORE
|
||||
// the script is loaded: `callDBus` has nothing to resolve until the daemon
|
||||
// holds `vu.valiq.blocker`, so loading first would silently drop every
|
||||
// focus event. If KWin is unavailable we fall back to X11 and say what that
|
||||
// costs — on a Wayland session, XWayland clients only.
|
||||
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);
|
||||
let _ = backend.shutdown();
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
println!("backend: kwin unavailable; falling back to x11");
|
||||
match blocker_platform_linux::x11::X11Backend::connect() {
|
||||
Ok(backend) => {
|
||||
println!("backend: x11 — native Wayland apps are NOT visible through this");
|
||||
@@ -166,6 +184,38 @@ fn main() -> ExitCode {
|
||||
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.
|
||||
///
|
||||
/// A placeholder reading rather than a clock read: the loop's correctness
|
||||
|
||||
Reference in New Issue
Block a user