110 lines
4.0 KiB
Python
110 lines
4.0 KiB
Python
"""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.")
|