Files
SellPL 8ae043658c
CI / ci (pull_request) Has been cancelled
CI / android (pull_request) Has been cancelled
feat(linux): add enforcement actions and the daemon event loop
The daemon now has the two pieces that turn a decision into an outcome: what to
do to a blocked process, and the loop that connects focus events to the engine.

**The enforcement ladder is escalating and non-destructive by default.**
`SIGSTOP` freezes an app; it does not kill it. That is a deliberate choice, and
the reason is section 1.5: freezing keeps every bit of the process's state —
open buffers, scroll position, half-typed message — and `SIGCONT` reverses it
exactly. Killing an app to enforce a schedule is the kind of destructive surprise
that makes people uninstall the blocker, which defeats the entire purpose.

Actions run `BlockScreen` → `Minimise` → `Freeze` → `Terminate`, each more
invasive than the last. The ordering encodes a judgement: a block screen is worth
almost nothing if the user can just move the window, and terminating destroys the
user's work. Start with the least destructive thing that could work.

**The event loop has no tick.** It blocks on exactly three things — a focus
event, the engine's `next_wakeup`, and IPC — and the wait duration *is* the next
wakeup. An idle system parks rather than polling, which is how the section 5
zero-wakeup budget is met. Two threads, both blocked, both costing nothing.

**The monotonic clock moved into the platform layer**, behind a trait. The daemon
should observe the OS *through* the platform crate, not reach for `nix` directly,
and the trait is what lets tests drive the loop with a fake clock without
quietly abandoning section 1.5.3's guarantee that locks never touch the wall
clock.

**Five enforcement tests, all against real processes.** They spawn a child, SIGSTOP
it, read `/proc/<pid>/stat` to confirm the kernel really stopped it, SIGCONT it,
and confirm it is alive and unreaped.

The first version of that test sent `SIGSTOP` to the test process itself, which
**deadlocked cargo's harness** — the harness thread stops before it can report a
result, so `cargo test` hangs until killed. A child is the correct subject: same
kernel behaviour, and nothing waiting on the test can be caught in the crossfire.

**What is not wired yet, stated plainly.** The loop runs on the X11 backend
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. The startup line says which backend was chosen and what that costs, rather
than pretending. On a Wayland session this means XWayland clients only.

Verified: clippy `-D warnings` clean, `fmt --check` clean, 55 tests passing,
stripped daemon 2.237 MB against the 5 MB budget.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-01 22:20:23 +03:00

39 lines
1.5 KiB
TOML

[package]
name = "blocker-platform-linux"
version = "0.1.0"
edition = "2021"
description = "Linux platform layer: foreground-window detection, enforcement, and system integration for Blocker."
license = "UNLICENSED"
[lints]
workspace = true
[lib]
name = "blocker_platform_linux"
path = "src/lib.rs"
[dependencies]
blocker-core = { path = "../../core" }
# CLAUDE.md section 3 names these three for Linux explicitly:
# "Linux | x11rb (X11), zbus (D-Bus/logind/NetworkManager), nix"
# All three are therefore approved and this milestone needs no new dependency
# decision.
#
# zbus is BLOCKING, on async-io rather than tokio, deliberately.
#
# `blocking-api` is what exposes `zbus::blocking`. Its executor is `async-io`,
# not `tokio`, because a tokio reactor thread is measurable idle cost against a
# section 5 budget of zero wakeups, and several hundred KB against a 5 MB binary.
# Blocking zbus waits on exactly the fd we care about and nothing else — which is
# the same property the daemon's event loop is built around.
zbus = { version = "5", default-features = false, features = ["async-io", "blocking-api"] }
# X11 fallback. Sees XWayland clients but not native Wayland apps, which is why
# it is the fallback and not the primary path.
x11rb = "0.13"
# SIGSTOP/SIGCONT for the non-destructive freeze, and process lookup.
# `time` for CLOCK_MONOTONIC, which section 1.5.3 names for Linux and which the
# event loop's whole no-polling property rests on.
nix = { version = "0.29", features = ["signal", "process", "time"] }