- 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.
121 lines
3.6 KiB
Python
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
|