""" Mini Job-System: Hintergrund-Prozesse mit Live-Log. Bewusst KISS: ein In-Memory-Dict, ein Daemon-Thread je Job, Subprocess mit zeilenweisem Log-Capture. Keine Persistenz, kein Broker. Genutzt von allen Routern, die laenger laufende Shell-Befehle anstossen (Download, Update, ...). """ import os import shlex import subprocess import threading import time import uuid JOBS: dict[str, dict] = {} _LOG_CAP = 400 def _run_job(job_id: str, args: list[str], env: dict | None = None, stdin_data: str | None = None): job = JOBS[job_id] job["state"] = "running" try: proc = subprocess.Popen( args, stdin=subprocess.PIPE if stdin_data is not None else None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, env={**os.environ, **(env or {})}, ) if stdin_data is not None: # Secret (z.B. sudo-Passwort) ueber stdin fuettern — landet NICHT im Log/ps. try: proc.stdin.write(stdin_data) # type: ignore[union-attr] proc.stdin.close() # type: ignore[union-attr] except Exception: # noqa: BLE001 pass for line in proc.stdout: # type: ignore[union-attr] job["log"].append(line.rstrip("\n")) if len(job["log"]) > _LOG_CAP: del job["log"][0] proc.wait() job["returncode"] = proc.returncode job["state"] = "done" if proc.returncode == 0 else "failed" except Exception as exc: # noqa: BLE001 job["log"].append(f"[mission-control] Fehler: {exc}") job["state"] = "failed" job["returncode"] = -1 job["finished_at"] = time.time() def start_job(args: list[str], label: str, env: dict | None = None, stdin_data: str | None = None, log_cmd: str | None = None) -> str: job_id = uuid.uuid4().hex[:12] # log_cmd erlaubt eine sanitisierte Befehlszeile (kein Secret im Log), wenn stdin_data ein # Geheimnis (sudo-Passwort) traegt. Sonst die echten Argumente. first_line = log_cmd if log_cmd is not None else "$ " + " ".join(shlex.quote(a) for a in args) JOBS[job_id] = { "id": job_id, "label": label, "state": "queued", "log": [first_line], "returncode": None, "started_at": time.time(), "finished_at": None, } threading.Thread(target=_run_job, args=(job_id, args, env, stdin_data), daemon=True).start() return job_id