Files
kadence/desktop/kadence_desktop/blocking/processes.py
T
timka123 77b26757bf refactor: enhance TimerPage layout and functionality; update theme and tokens for improved design consistency
- Refactored TimerPage component to improve layout and user experience.
- Updated theme configuration to utilize design tokens for better maintainability.
- Introduced new tokens for colors and typography to align with design principles.
- Simplified state management and improved timer functionality.
- Enhanced accessibility and responsiveness of UI elements.
2026-10-18 23:57:36 +03:00

121 lines
3.6 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` catches `steamwebhelper`, and
`telegram` catches `Telegram` — and an entry containing `*` or `?` is treated as
a glob (`steam*`, `*discord*`, `telegram-?`), matched against both the process
name and its command line. 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 fnmatch import fnmatch
from dataclasses import dataclass
from pathlib import Path
log = logging.getLogger(__name__)
_SELF_MARKERS = ("kadence", "python3 -m kadence_desktop")
def matches(pattern: str, name: str, cmdline: str) -> bool:
"""Whether one block-list entry matches a process.
A mask is anchored to the process *name* (so `steam*` does not match every
command that merely mentions steam) but also tried against the whole command
line, which is how `*discord*` catches an Electron app launched by path.
"""
pattern = pattern.strip().lower()
if not pattern:
return False
if any(char in pattern for char in "*?["):
return fnmatch(name, pattern) or fnmatch(cmdline, pattern)
return pattern in name or pattern in cmdline
@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()
name, command = comm.lower(), cmdline.lower()
if any(marker in f"{name} {command}" for marker in _SELF_MARKERS):
continue
match = next((pattern for pattern in wanted if matches(pattern, name, command)), 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