Files
HMS MediaEngine Agent 362e089be0 AUFGERAUMT: Root auf 10 sichtbare Elemente reduziert
Der Nutzer hat recht: Der Ordner war voller Entwicklungs-Muell.
Jetzt ist sauber getrennt:

ROOT (was der Nutzer sieht und braucht):
- run.py                     = das Programm
- hms_app/                   = der Anwendungscode
- HMS MediaEngine.app        = macOS Doppelklick-Starter
- HMS-Start.vbs              = Windows Doppelklick-Starter
- HMS-Install.vbs             = Windows Erst-Installation
- HMS-Mac-Install.command     = macOS Homebrew-Installation
- HMS-Portable-Install.command = macOS Portable-Installation (16GB-Fix)
- installer_gui.py           = grafischer Installer
- launcher.pyw + launcher_core.py = interne Start-Logik
- LIESMICH.txt               = 10-Zeilen-Kurzanleitung
- .gitignore

_entwicklung/ (alles andere, NICHT benoetigt):
- packages/ apps/ native/ plugins/ tools/ schemas/ tests/ docs/
  build/ fixture_profiles/
- PLAN.md STATUS.md ERRORS.md TEST_REPORT.md CHANGELOG.md README.md
- pyproject.toml uv.lock setup_*.sh/ps1 make_mac_app.py

Diese Trennung gilt ab sofort fuer alle Commits. Der Nutzer kann
_entwicklung/ loeschen wenn er Platz braucht - die App laeuft ohne.

Verifiziert: App startet nach Aufraeumen unveraendert (Health 200).
2026-09-11 23:44:06 +02:00

165 lines
5.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Cluster-Nachrichtenhülle (PLAN.md §6.5).
Jede Cluster-Nachricht enthält mindestens: cluster_id, node_id, command_id,
Sequenz, Projekt-Revision, Absenderzeit, optionale execute_at-Showzeit und
Trace-ID. Zustandsändernde Commands sind idempotent und werden mit
accepted/armed/executed/failed bestätigt.
"""
from __future__ import annotations
import time
import uuid
from enum import StrEnum
class CommandStatus(StrEnum):
"""Bestätigungsstufen zustandsändernder Commands (§6.5)."""
ACCEPTED = "accepted"
ARMED = "armed"
EXECUTED = "executed"
FAILED = "failed"
class ClusterMessage:
"""Versionierte Cluster-Nachricht mit Pflichtfeldern (§6.5).
- sequence: je Absender monoton; Lücken signalisieren Paketverlust
- project_revision: Zustandsrevision, auf die sich der Command bezieht
- sender_time_ns: monotone Absenderzeit (nicht Wanduhr)
- execute_at_show_time_ns: optional; 100300 ms Vorlauf für Sync-Starts
- trace_id: Korrelations-ID über Log-Grenzen (§28.1)
"""
__slots__ = (
"cluster_id",
"node_id",
"command_id",
"sequence",
"project_revision",
"sender_time_ns",
"execute_at_show_time_ns",
"trace_id",
"status",
"payload",
)
def __init__(
self,
cluster_id: str,
node_id: str,
command_id: str,
sequence: int,
project_revision: int,
sender_time_ns: int | None = None,
execute_at_show_time_ns: int | None = None,
trace_id: str | None = None,
status: CommandStatus | None = None,
payload: dict | None = None,
) -> None:
for field_name, value in (
("cluster_id", cluster_id),
("node_id", node_id),
("command_id", command_id),
):
try:
uuid.UUID(value)
except (ValueError, AttributeError) as exc:
raise ValueError(f"{field_name} must be a UUID") from exc
if sequence < 0:
raise ValueError("sequence must be >= 0")
if project_revision < 0:
raise ValueError("project_revision must be >= 0")
self.cluster_id = cluster_id
self.node_id = node_id
self.command_id = command_id
self.sequence = sequence
self.project_revision = project_revision
self.sender_time_ns = (
sender_time_ns if sender_time_ns is not None else time.monotonic_ns()
)
self.execute_at_show_time_ns = execute_at_show_time_ns
self.trace_id = trace_id or str(uuid.uuid4())
self.status = status
self.payload = payload or {}
def to_dict(self) -> dict:
return {
"cluster_id": self.cluster_id,
"node_id": self.node_id,
"command_id": self.command_id,
"sequence": self.sequence,
"project_revision": self.project_revision,
"sender_time_ns": self.sender_time_ns,
"execute_at_show_time_ns": self.execute_at_show_time_ns,
"trace_id": self.trace_id,
"status": self.status.value if self.status else None,
"payload": self.payload,
}
@classmethod
def from_dict(cls, data: dict) -> ClusterMessage:
status_raw = data.get("status")
status = CommandStatus(status_raw) if status_raw else None
return cls(
cluster_id=data["cluster_id"],
node_id=data["node_id"],
command_id=data["command_id"],
sequence=int(data["sequence"]),
project_revision=int(data["project_revision"]),
sender_time_ns=data.get("sender_time_ns"),
execute_at_show_time_ns=data.get("execute_at_show_time_ns"),
trace_id=data.get("trace_id"),
status=status,
payload=data.get("payload", {}),
)
class CommandTracker:
"""Idempotenz je (node_id, command_id) mit Statusübergängen (§6.5).
- register: neuer Command → True; Duplikat → False (gleiches Ack)
- advance: nur vorwärts accepted → armed → executed/failed
- veraltete Einträge werden nach Kapazität begrenzt (kein unbeschränkter
Cache, §33)
"""
_ORDER = {
CommandStatus.ACCEPTED: 1,
CommandStatus.ARMED: 2,
CommandStatus.EXECUTED: 3,
CommandStatus.FAILED: 3,
}
def __init__(self, capacity: int = 4096) -> None:
if capacity <= 0:
raise ValueError("capacity must be positive")
self._capacity = capacity
self._states: dict[str, CommandStatus] = {}
def register(self, node_id: str, command_id: str) -> bool:
"""True, wenn der Command neu ist; False bei Duplikat."""
key = f"{node_id}:{command_id}"
if key in self._states:
return False
self._states[key] = CommandStatus.ACCEPTED
if len(self._states) > self._capacity:
oldest = next(iter(self._states))
del self._states[oldest]
return True
def advance(self, node_id: str, command_id: str, new_status: CommandStatus) -> bool:
"""Nur vorwärts; False bei unbekanntem Command oder Rückschritt."""
key = f"{node_id}:{command_id}"
current = self._states.get(key)
if current is None:
return False
if self._ORDER[new_status] <= self._ORDER[current]:
return False
self._states[key] = new_status
return True
def status(self, node_id: str, command_id: str) -> CommandStatus | None:
return self._states.get(f"{node_id}:{command_id}")