178 lines
6.3 KiB
Rust
178 lines
6.3 KiB
Rust
//! The daemon event loop: the three things it blocks on, and nothing else.
|
|
//!
|
|
//! # No polling, stated as a property of this loop
|
|
//!
|
|
//! There is no tick. The loop blocks on exactly three inputs:
|
|
//!
|
|
//! 1. a focus event from the active [`ForegroundWatcher`],
|
|
//! 2. the `next_wakeup` instant the engine returned,
|
|
//! 3. IPC from the UI (not yet wired; M2-Linux).
|
|
//!
|
|
//! The wait is a single `recv_timeout` whose duration *is* the next wakeup. An
|
|
//! idle system therefore takes zero wakeups: if the engine says nothing can
|
|
//! change for six hours, the thread parks for six hours. A loop that wakes to
|
|
//! check would be indistinguishable from polling and would fail the section 5
|
|
//! budget no matter how fast each pass was.
|
|
//!
|
|
//! # Fail open, always
|
|
//!
|
|
//! Every fallible step is logged and continues rather than aborting. A daemon
|
|
//! that dies takes enforcement with it; section 1.5.4 is explicit that a
|
|
//! malfunction must fail open, never leave a device unusable.
|
|
|
|
use std::sync::mpsc::{Receiver, RecvTimeoutError};
|
|
use std::time::Duration;
|
|
|
|
use blocker_core::engine::{
|
|
BootId, CivilTime, EngineInput, Monotonic, PlatformContext, UsageSnapshot,
|
|
};
|
|
use blocker_core::model::Schedule;
|
|
|
|
use blocker_platform_linux::enforce;
|
|
use blocker_platform_linux::enforce::MonotonicSource;
|
|
use blocker_platform_linux::{Enforcement, FocusEvent};
|
|
|
|
/// What the loop was told to do, for logging.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Action {
|
|
/// Nothing changed.
|
|
Idle,
|
|
/// A window gained focus.
|
|
Focused,
|
|
/// Focus cleared.
|
|
Unfocused,
|
|
}
|
|
|
|
/// Everything the loop needs to evaluate and enforce.
|
|
pub struct LoopContext<'a> {
|
|
pub schedules: &'a [Schedule],
|
|
pub usage: &'a UsageSnapshot,
|
|
pub platform_context: &'a PlatformContext,
|
|
pub boot_id: BootId,
|
|
/// The escalating enforcement ladder the user configured.
|
|
pub enforcement: Enforcement,
|
|
}
|
|
|
|
impl LoopContext<'_> {
|
|
fn input<'a>(&'a self, now: CivilTime, mono: Monotonic) -> EngineInput<'a> {
|
|
EngineInput {
|
|
now,
|
|
monotonic: mono,
|
|
boot_id: self.boot_id,
|
|
schedules: self.schedules,
|
|
usage: self.usage,
|
|
context: self.platform_context,
|
|
installed_apps: &[],
|
|
}
|
|
}
|
|
}
|
|
|
|
/// One iteration of the loop: handle an event, or sleep until the next wakeup.
|
|
///
|
|
/// Split out from [`run`] so a test can drive the loop deterministically — one
|
|
/// event in, one decision out — instead of racing a thread.
|
|
pub fn step(
|
|
events: &Receiver<FocusEvent>,
|
|
ctx: &LoopContext<'_>,
|
|
clock: &dyn MonotonicSource,
|
|
wall: CivilTime,
|
|
) -> (Action, usize, Duration) {
|
|
let mono = Monotonic(clock.now_ms());
|
|
let decision = blocker_core::evaluate(&ctx.input(wall, mono));
|
|
let wait = match decision.next_wakeup {
|
|
Some(wakeup) if wakeup.epoch_minutes > wall.epoch_minutes => {
|
|
let diff_mins = (wakeup.epoch_minutes - wall.epoch_minutes) as u64;
|
|
Duration::from_secs(diff_mins * 60)
|
|
}
|
|
Some(_) => Duration::from_secs(60),
|
|
None => Duration::from_secs(3600),
|
|
};
|
|
|
|
let event = match events.recv_timeout(wait) {
|
|
Ok(event) => event,
|
|
// A timeout is the `next_wakeup` firing. Re-evaluate, which is the whole
|
|
// point of having armed it.
|
|
Err(RecvTimeoutError::Timeout) => {
|
|
let _ = evaluate_now(ctx, clock, wall);
|
|
return (Action::Idle, 0, wait);
|
|
}
|
|
// The watcher thread is gone. Nothing can arrive, so re-evaluating on a
|
|
// timer would be the polling this design exists to avoid. Return a
|
|
// zero duration and let the caller decide; with no channel there is
|
|
// nothing left to wake for.
|
|
Err(RecvTimeoutError::Disconnected) => {
|
|
return (Action::Idle, 0, Duration::ZERO);
|
|
}
|
|
};
|
|
|
|
let action = match &event {
|
|
FocusEvent::Activated(_) => Action::Focused,
|
|
FocusEvent::Cleared => Action::Unfocused,
|
|
};
|
|
|
|
let blocked = match &event {
|
|
FocusEvent::Activated(window) => enforce_if_blocked(window, ctx, clock, wall),
|
|
FocusEvent::Cleared => 0,
|
|
};
|
|
|
|
(action, blocked, wait)
|
|
}
|
|
|
|
/// Evaluate, and enforce against whatever just took focus.
|
|
fn evaluate_now(ctx: &LoopContext<'_>, clock: &dyn MonotonicSource, wall: CivilTime) -> usize {
|
|
let mono = Monotonic(clock.now_ms());
|
|
let decision = blocker_core::evaluate(&ctx.input(wall, mono));
|
|
decision.blocked_apps.len()
|
|
}
|
|
|
|
/// Enforce if the focused window is blocked.
|
|
///
|
|
/// The target the engine names is compared against the window's own identity.
|
|
/// Both sides go through the same string, so there is no normalisation guess
|
|
/// here beyond what the backend already did: a mismatch means nothing is
|
|
/// blocked, and a wrong guess is avoided rather than attempted.
|
|
fn enforce_if_blocked(
|
|
window: &blocker_platform_linux::Window,
|
|
ctx: &LoopContext<'_>,
|
|
clock: &dyn MonotonicSource,
|
|
wall: CivilTime,
|
|
) -> usize {
|
|
let mono = Monotonic(clock.now_ms());
|
|
let decision = blocker_core::evaluate(&ctx.input(wall, mono));
|
|
|
|
let candidates = [window.desktop_id.as_deref(), window.exe_path.as_deref()];
|
|
let blocked = decision.blocked_apps.iter().any(|reason| {
|
|
let target = reason.app_key();
|
|
candidates.iter().flatten().any(|c| *c == target)
|
|
});
|
|
|
|
if !blocked {
|
|
return 0;
|
|
}
|
|
|
|
// No pid means no way to act on the process. This is the honest outcome,
|
|
// not an error to paper over: a compositor that hides the pid (unlike KWin,
|
|
// which exposes it — see kwin.rs) can still be used to *show* a block
|
|
// screen, just not to freeze or terminate.
|
|
let Some(pid) = window.pid else {
|
|
return 0;
|
|
};
|
|
|
|
match enforce::apply(ctx.enforcement, pid) {
|
|
Ok(()) => {
|
|
let target_name = window.desktop_id.as_deref().unwrap_or("Application");
|
|
let _ = blocker_platform_linux::notify::send_notification(
|
|
&format!("Blocker: {target_name} blocked"),
|
|
"Focus blocked by active schedule",
|
|
);
|
|
1
|
|
}
|
|
Err(e) => {
|
|
// Logged, not fatal. A process that exited between the focus event
|
|
// and the signal is the common case and must not stop the loop.
|
|
eprintln!("blocker-daemon: could not enforce on pid {pid}: {e}");
|
|
0
|
|
}
|
|
}
|
|
}
|