Files
kadence/desktop/kadence_desktop/blocking/processes.py
T
2026-10-18 18:02:05 +03:00

103 lines
2.8 KiB
Python

"""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