"""Turning the server's policy into what this machine actually does. Called on every poll with the policy the backend computed. It keeps the hosts file and the process list in step with it, reports every turned-away attempt back to the API (that is what makes the daily report fun), and — importantly — puts everything back the moment the policy says blocking is off, including when the app exits or crashes mid-interval. """ from __future__ import annotations import logging from typing import Callable from kadence_desktop.blocking import processes from kadence_desktop.blocking.hosts import HostsBlocker log = logging.getLogger(__name__) class BlockerManager: def __init__( self, *, helper_path: str, report: Callable[[str, str, str, str | None], None], notify: Callable[[str, str], None] | None = None, ) -> None: self._hosts = HostsBlocker(helper_path) self._report = report self._notify = notify self._active = False self._applied: list[str] = [] self._announced: set[int] = set() self.enforce_hosts = True self.enforce_apps = True self.last_error: str | None = None self.killed_today = 0 # ---- state ------------------------------------------------------------- @property def active(self) -> bool: return self._active @property def helper_installed(self) -> bool: return self._hosts.helper_installed @property def blocked_domains(self) -> list[str]: return list(self._applied) # ---- the one entry point ---------------------------------------------- def sync(self, policy: dict) -> None: """Bring the machine in line with `policy` (from /api/blocker/policy).""" should_block = bool(policy.get("active")) and bool(policy.get("enabled")) domains = [str(item) for item in policy.get("domains", [])] apps = [str(item) for item in policy.get("apps", [])] redirect_ip = str(policy.get("redirect_ip") or "127.0.0.1") if should_block: if self.enforce_hosts and domains != self._applied: if self._hosts.apply(domains, redirect_ip): self._applied = domains self.last_error = None log.info("blocking %s domain(s)", len(domains)) else: self.last_error = self._hosts.last_error if self.enforce_apps and apps: self._enforce_apps(apps, kill=bool(policy.get("kill_apps", True))) self._active = True else: self.release() def release(self) -> None: """Unblock everything. Called when the interval ends, and on shutdown.""" if self._applied or self._hosts_dirty(): if self._hosts.clear(): self._applied = [] self.last_error = None else: self.last_error = self._hosts.last_error self._active = False self._announced.clear() def _hosts_dirty(self) -> bool: from kadence_desktop.blocking.hosts import current_block return bool(current_block()) # ---- apps -------------------------------------------------------------- def _enforce_apps(self, patterns: list[str], *, kill: bool) -> None: for app in processes.scan(patterns): if app.pid in self._announced: continue self._announced.add(app.pid) if kill and processes.terminate(app): self.killed_today += 1 self._report("app", app.matched, "killed", f"{app.name} (pid {app.pid})") if self._notify: self._notify("Closed a distraction", f"{app.name} is blocked during focus.") else: self._report("app", app.matched, "warned", f"{app.name} (pid {app.pid})") if self._notify: self._notify("That app is blocked", f"{app.name} is on your block list.")