smlflg's picture
Initial public upload from Projekte/agent-process-dashboard
06357e7 verified
Raw History Blame Contribute Delete
4.49 kB
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
import os
import signal
from .models import DashboardSnapshot
from .procfs import sample_process
from .snapshot import build_snapshot
SignalName = str
SnapshotBuilder = Callable[[], DashboardSnapshot]
SignalSender = Callable[[int, int], None]
SUPPORTED_SIGNALS: dict[SignalName, int] = {
"TERM": signal.SIGTERM,
"KILL": signal.SIGKILL,
}
@dataclass(frozen=True)
class ProcessActionRequest:
pid: int
process_key: str
signal_name: SignalName = "TERM"
@dataclass(frozen=True)
class ProcessActionResult:
ok: bool
status: str
message: str
pid: int
process_key: str
signal_name: SignalName
def send_signal_to_candidate(
request: ProcessActionRequest,
*,
snapshot_builder: SnapshotBuilder | None = None,
signal_sender: SignalSender = os.kill,
current_pid: int | None = None,
) -> ProcessActionResult:
"""Send a signal only if the PID still matches a current dry-run candidate."""
current_pid = os.getpid() if current_pid is None else current_pid
snapshot_builder = snapshot_builder or build_snapshot
signal_number = SUPPORTED_SIGNALS.get(request.signal_name)
if signal_number is None:
return _result(
request,
ok=False,
status="invalid_signal",
message=f"Signal {request.signal_name} wird nicht unterstützt.",
)
if request.pid == current_pid:
return _result(
request,
ok=False,
status="self_process",
message="Der Dashboard-Prozess selbst wird nicht beendet.",
)
snapshot = snapshot_builder()
live = next((sample for sample in snapshot.processes if sample.pid == request.pid), None)
if live is None:
return _result(
request,
ok=False,
status="not_found",
message=(
f"PID {request.pid} läuft nicht mehr oder ist für den aktuellen User "
"nicht sichtbar."
),
)
if live.process_key != request.process_key:
return _result(
request,
ok=False,
status="pid_reused",
message=(
f"PID {request.pid} gehört inzwischen zu einem anderen Prozess. "
"Es wurde kein Signal gesendet."
),
)
candidate = next(
(
item
for item in snapshot.dry_run.candidates
if item.pid == request.pid and item.process_key == request.process_key
),
None,
)
if candidate is None:
return _result(
request,
ok=False,
status="not_candidate",
message=(
f"PID {request.pid} ist kein aktueller Dry-run-Kandidat. "
"Es wurde kein Signal gesendet."
),
)
try:
signal_sender(request.pid, signal_number)
except ProcessLookupError:
return _result(
request,
ok=False,
status="not_found",
message=f"PID {request.pid} ist vor dem Signal verschwunden.",
)
except PermissionError:
return _result(
request,
ok=False,
status="permission_denied",
message=f"Keine Berechtigung, Signal {request.signal_name} an PID {request.pid} zu senden.",
)
except OSError as exc:
return _result(
request,
ok=False,
status="signal_failed",
message=f"Signal {request.signal_name} an PID {request.pid} ist fehlgeschlagen: {exc}",
)
return _result(
request,
ok=True,
status="sent",
message=f"Signal {request.signal_name} wurde an PID {request.pid} gesendet.",
)
def same_process_is_running(
pid: int,
process_key: str,
*,
proc_root: str = "/proc",
) -> bool:
sample = sample_process(pid, proc_root=proc_root, current_uid_only=True)
return sample is not None and sample.process_key == process_key
def _result(
request: ProcessActionRequest,
*,
ok: bool,
status: str,
message: str,
) -> ProcessActionResult:
return ProcessActionResult(
ok=ok,
status=status,
message=message,
pid=request.pid,
process_key=request.process_key,
signal_name=request.signal_name,
)