103 lines
2.8 KiB
Python
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
|