"""Application blocking by watching `/proc`. No root needed: these are your own processes. The scan reads `/proc/*/comm` and the cmdline, matches against the configured names, and either terminates the process (SIGTERM, then SIGKILL if it ignores us) or just reports it, depending on the setting. Matching is deliberately forgiving — `steam` should catch `steamwebhelper`, and `telegram` should catch `Telegram` — but it never matches ourselves, PID 1, or anything outside the current user's processes. """ from __future__ import annotations import logging import os import signal import time from dataclasses import dataclass from pathlib import Path log = logging.getLogger(__name__) _SELF_MARKERS = ("kadence", "python3 -m kadence_desktop") @dataclass(slots=True) class RunningApp: pid: int name: str cmdline: str matched: str def _read(path: Path) -> str: try: return path.read_text("utf-8", errors="replace") except OSError: return "" def scan(patterns: list[str]) -> list[RunningApp]: """Every live process of this user matching one of `patterns`.""" wanted = [pattern.strip().lower() for pattern in patterns if pattern.strip()] if not wanted: return [] uid = os.getuid() self_pid = os.getpid() found: list[RunningApp] = [] for entry in Path("/proc").iterdir(): if not entry.name.isdigit(): continue pid = int(entry.name) if pid in (1, self_pid): continue try: if entry.stat().st_uid != uid: continue except OSError: continue comm = _read(entry / "comm").strip() if not comm: continue cmdline = _read(entry / "cmdline").replace("\x00", " ").strip() haystack = f"{comm} {cmdline}".lower() if any(marker in haystack for marker in _SELF_MARKERS): continue match = next((pattern for pattern in wanted if pattern in haystack), None) if match: found.append(RunningApp(pid=pid, name=comm, cmdline=cmdline[:200], matched=match)) return found def terminate(app: RunningApp, *, grace_seconds: float = 2.0) -> bool: """Ask the process to quit, then insist. True when it is gone.""" try: os.kill(app.pid, signal.SIGTERM) except ProcessLookupError: return True except PermissionError: log.warning("not allowed to stop %s (pid %s)", app.name, app.pid) return False deadline = time.monotonic() + grace_seconds while time.monotonic() < deadline: try: os.kill(app.pid, 0) except ProcessLookupError: return True time.sleep(0.1) try: os.kill(app.pid, signal.SIGKILL) except ProcessLookupError: return True except PermissionError: return False return True