8ae043658c
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>
39 lines
1.5 KiB
TOML
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"] } |