This repository has been archived on 2026-07-22. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
mission-control/jobengine.py
T
Hitonabi 5560c18816 v3 Phase C: Security-Härtung + UX-Feedback
Security:
- Passwort-Leak dicht: os-update/reboot fuettern das sudo-Passwort ueber stdin
  (jobengine: neues stdin_data + sanitisierte Log-Zeile log_cmd). Verifiziert:
  Secret taucht nicht mehr im Job-Log/ps auf.
- /api/status liefert "secured" -> Topbar-Security-Chip zeigt ehrlich, ob Token aktiv.
- WS-Token-Auth funktioniert (Query-Param, war bereits in auth.py).
- Bind bleibt 0.0.0.0 (kein Aussperren), kein Token erzwungen.

UX-Feedback:
- Schnellstart-Bug gefixt (Titel/Text brechen um, .qa-main column).
- Cookbook: Sortier-Dropdown (Downloads/Likes/Aktualisiert/Trend) wie HuggingFace.
- Server & Wartung: Buttons -> selbsterklaerende Aktionszeilen mit Beschreibung;
  Reboot nur mit rotem Icon (Vollrot-Bug behoben).
- Live-Konsole: neue Option "System (ganzer Server)" -> journalctl ueber alle Units.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 12:41:30 +02:00

71 lines
2.5 KiB
Python

"""
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