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

228 lines
8.6 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.
"""Plugin-Lifecycle-Manager (PLAN.md §14.5, §14.6, §26.3).
Lebenszyklus:
discovered → validated → installed → enabled → compiled → active
↘ quarantined / incompatible
Regeln:
- Übergänge nur entlang definierter Kanten; Sprünge sind Fehler
- Validierung umfasst Manifest-Schema, API-Kompatibilität,
Pfadsicherheit, Backend-/Shader-Prüfung (§14.5)
- ein fehlerhaftes Plugin wird quarantiniert, ohne ein Projekt unbrauchbar
zu machen (§3.5); der Effekt wird überbrückt (bypass_on_error)
- Show-Lock (§26.3): Installieren/Updaten ist gesperrt; Enable/Disable
und Parameter bleiben erlaubt (§17.7 Live-Modus)
- Doppelte Plugin-IDs sind Fehler, keine stillen Überschreibungen
"""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import StrEnum
from pathlib import Path
from hms_plugin_sdk.manifest import load_manifest
class LifecycleState(StrEnum):
"""Zustände gemäß §14.5."""
DISCOVERED = "discovered"
VALIDATED = "validated"
INSTALLED = "installed"
ENABLED = "enabled"
COMPILED = "compiled"
ACTIVE = "active"
QUARANTINED = "quarantined"
INCOMPATIBLE = "incompatible"
DISABLED = "disabled"
# Erlaubte Übergänge (§14.5-Lebenszyklusgraph)
_TRANSITIONS: dict[LifecycleState, frozenset[LifecycleState]] = {
LifecycleState.DISCOVERED: frozenset(
{LifecycleState.VALIDATED, LifecycleState.INCOMPATIBLE, LifecycleState.QUARANTINED}
),
LifecycleState.VALIDATED: frozenset(
{LifecycleState.INSTALLED, LifecycleState.INCOMPATIBLE, LifecycleState.QUARANTINED}
),
LifecycleState.INSTALLED: frozenset(
{LifecycleState.ENABLED, LifecycleState.DISABLED, LifecycleState.QUARANTINED}
),
LifecycleState.ENABLED: frozenset(
{LifecycleState.COMPILED, LifecycleState.DISABLED, LifecycleState.QUARANTINED}
),
LifecycleState.COMPILED: frozenset(
{LifecycleState.ACTIVE, LifecycleState.QUARANTINED, LifecycleState.DISABLED}
),
LifecycleState.ACTIVE: frozenset(
{LifecycleState.DISABLED, LifecycleState.QUARANTINED}
),
LifecycleState.DISABLED: frozenset(
{LifecycleState.ENABLED, LifecycleState.QUARANTINED}
),
LifecycleState.QUARANTINED: frozenset(), # manuelle Entfernung/Neuinstallation
LifecycleState.INCOMPATIBLE: frozenset(),
}
class InvalidTransitionError(Exception):
"""Unerlaubter Zustandsübergang im Lebenszyklus."""
@dataclass
class PluginRecord:
"""Ein Plugin im Lifecycle-Manager."""
plugin_id: str
version: str
state: LifecycleState = LifecycleState.DISCOVERED
package_hash: str | None = None
last_error: str | None = None
manifest: dict = field(default_factory=dict)
class PluginLifecycleManager:
"""Verwaltet den Lebenszyklus aller installierten Plugins (§14.5).
- discover(): Verzeichnis scannen, Manifest laden, Manifest-Validierung
- advance(): zustandsgeprüfter Übergang
- quarantine(): Fehlerfall mit Ursache (§3.5, §14.5)
- Show-Lock: install_validate/enable_new blockiert Strukturänderungen
(§26.3); Aktivieren bereits installierter Plugins bleibt erlaubt
"""
def __init__(self, show_lock: bool = False) -> None:
self._plugins: dict[str, PluginRecord] = {}
self._show_lock = show_lock
@property
def show_lock(self) -> bool:
return self._show_lock
def set_show_lock(self, enabled: bool) -> None:
"""§26.3: Show-Lock verhindert Plugininstallation/-update."""
self._show_lock = enabled
# ---------- Discovery & Validierung (§14.5) ----------
def discover(self, plugin_dir: Path) -> list[str]:
"""Scannt ein Verzeichnis mit Plugin-Ordnern.
Lädt plugin.json, validiert es (inkl. Shader-Existenz) und
überführt jedes Plugin in VALIDATED oder INCOMPATIBLE/QUARANTINED.
Rückgabe: Liste diagnostizierter Fehler (leer = alles sauber).
"""
errors: list[str] = []
if not plugin_dir.is_dir():
return [f"Plugin-Verzeichnis fehlt: {plugin_dir}"]
for child in sorted(plugin_dir.iterdir()):
if not child.is_dir():
continue
manifest_path = child / "plugin.json"
if not manifest_path.is_file():
continue # kein Plugin-Ordner (z. B. .git)
manifest, validation_errors = load_manifest(child)
if validation_errors:
pid = manifest.get("id") or child.name
errors.extend(f"{pid}: {e}" for e in validation_errors)
record = PluginRecord(
plugin_id=pid,
version=str(manifest.get("version", "0.0.0")),
state=LifecycleState.QUARANTINED,
last_error="; ".join(validation_errors),
manifest=manifest,
)
self._upsert(record)
continue
pid = manifest["id"]
record = PluginRecord(
plugin_id=pid,
version=manifest["version"],
state=LifecycleState.VALIDATED,
manifest=manifest,
)
self._upsert(record)
return errors
# ---------- Übergänge (§14.5) ----------
def advance(self, plugin_id: str, new_state: LifecycleState) -> LifecycleState:
"""Führt einen Lifecycle-Übergang aus; wirft bei illegaler Kante.
Show-Lock (§26.3): Übergänge, die Installation/Update bedeuten
(INSTALLED von DISCOVERED/VALIDATED), sind gesperrt. Aktivieren
bereits installierter Plugins (ENABLED/COMPILED/ACTIVE) bleibt
erlaubt (§17.7 Live: Layerparameter und Livefunktionen nutzbar).
"""
record = self._plugins.get(plugin_id)
if record is None:
raise KeyError(f"unbekanntes Plugin {plugin_id!r}")
current = record.state
if new_state not in _TRANSITIONS[current]:
raise InvalidTransitionError(
f"{plugin_id}: Übergang {current.value}{new_state.value} nicht erlaubt"
)
if (
self._show_lock
and new_state is LifecycleState.INSTALLED
and current in (LifecycleState.DISCOVERED, LifecycleState.VALIDATED)
):
raise InvalidTransitionError(
f"{plugin_id}: Installation im Show-Lock gesperrt (§26.3)"
)
record.state = new_state
record.last_error = None
return record.state
def quarantine(self, plugin_id: str, reason: str) -> None:
"""Fehlerfall: Plugin überbrücken, Projekt bleibt nutzbar (§3.5)."""
record = self._plugins.get(plugin_id)
if record is None:
raise KeyError(f"unbekanntes Plugin {plugin_id!r}")
record.state = LifecycleState.QUARANTINED
record.last_error = reason
# ---------- Abfragen ----------
def get(self, plugin_id: str) -> PluginRecord | None:
return self._plugins.get(plugin_id)
def by_state(self, state: LifecycleState) -> list[PluginRecord]:
return [r for r in self._plugins.values() if r.state is state]
def active_plugins(self) -> list[PluginRecord]:
return self.by_state(LifecycleState.ACTIVE)
def all(self) -> list[PluginRecord]:
return list(self._plugins.values())
def compatible_backends(self, plugin_id: str) -> frozenset[str]:
"""Gemeinsame Backends: deklariert im Manifest und lokal verfügbar."""
record = self._plugins.get(plugin_id)
if record is None:
return frozenset()
declared = set(record.manifest.get("entrypoints", {}))
supported = set(
record.manifest.get("capabilities", {}).get("supported_backends", [])
)
return frozenset(declared & supported)
# ---------- Interna ----------
def _upsert(self, record: PluginRecord) -> None:
existing = self._plugins.get(record.plugin_id)
if existing is not None and existing.version != record.version:
# Versionskonflikt: neuer Stand gewinnt nur ohne Show-Lock.
# Ein Update startet einen neuen Zyklus: discover() hat das
# neue Manifest bereits validiert (§14.5), also ist VALIDATED
# der korrekte Zustand eine erneute Installation ist nötig,
# der alte INSTALLED/ACTIVE-Status gilt nicht mehr.
if self._show_lock:
raise InvalidTransitionError(
f"{record.plugin_id}: Versionswechsel {existing.version}"
f"{record.version} im Show-Lock gesperrt (§26.3)"
)
self._plugins[record.plugin_id] = record