fix(station): Manifest-Merge robust gegen Fehlform, Version pro Eintrag
Fixrunde 2 zu "Migrationsimages beim Deploy signieren":
- N1 (Important): das Merge-Skript im SSH-Heredoc von deploy.sh fing beim
Bestandsmanifest nur Lesefehler und kaputtes JSON ab (OSError/ValueError).
War die Datei gueltiges JSON mit falscher Form ("[]", "null", ein String,
oder ein images-Eintrag, der selbst kein Objekt ist), krachte .get() mit
AttributeError - und das VOR der Installation von
server.py/products.py/CLI/Unit und vor dem Neustart, liess einen Pi also
mit neuen Images, aber altem Stationscode zurueck. Jetzt wird alte wie
neue Manifest-Struktur konsequent auf isinstance(dict)/isinstance(list)
geprueft, jede Fehlform faellt auf ein leeres Manifest zurueck statt
abzustuerzen. Neues Szenario 21 deckt alle vier Faelle aus der
Review-Tabelle ab (kaputt, leer, falsch geformtes JSON, kaputter Eintrag).
- N2 (Important): das zusammengefuehrte Manifest hat "ninlight_version"
bisher blind vom neuen Manifest uebernommen, obwohl uebernommene
Alt-Eintraege (deren Images unveraendert liegen bleiben, siehe Fixrunde 1
I1) eine andere Firmware-Version tragen koennen - eine konkrete falsche
Version beim Protokollieren ist schlechter als das vorherige "unbekannt".
Die Version wandert jetzt PRO EINTRAG ins Manifest (deploy.sh erzeugt sie
dort, der Merge behaelt sie); das manifestweite Feld bleibt nur als
Bestandskompatibilitaet und wird nur gesetzt, wenn wirklich alle Eintraege
uebereinstimmen. server.py liest beim Ausliefern jetzt die Version DES
AUSGELIEFERTEN EINTRAGS (Station.image_version(target, variant) ersetzt
manifest_version()) und faellt nur auf das manifestweite Feld zurueck,
wenn der Eintrag selbst keine eigene traegt. test_server.py bekommt einen
neuen Test fuer den Vorrang des Eintrags vor dem manifestweiten Feld,
Szenario 14 in test_deploy.sh prueft jetzt zusaetzlich unterschiedliche
Versionen zwischen zusammengefuehrten Eintraegen und dass das
manifestweite Feld dann fehlt statt falsch zu sein. Die Spec
(docs/superpowers/specs/.../design.md, Abschnitt 4, lokal/nicht
versioniert) ist nachgezogen. Die dadurch ueberfluessig gewordene
"sorted(versions)[0]"-Stelle aus Fixrunde 1 ist entfallen.
- Kleinigkeit: die Remote-Meldung, wenn migrate/ von diesem Rechner leer
ist, behauptet nicht mehr "kein Schluesselverzeichnis" (das stimmt im
Fall "Verzeichnis vorhanden, aber leer" nicht) - sie nennt jetzt keinen
bestimmten Grund mehr, nur die Konsequenz.
- Kleinigkeit: --exclude=manifest.json ist jetzt --exclude=/manifest.json,
konsistent mit dem daneben verankerten --exclude=/migrate.
Alle drei Aenderungen (N1, N2, beide Kleinigkeiten) sind vor dem Fix per
Rot-Probe an einer Kopie von deploy.sh verifiziert: mit den urspruenglichen
Fehlern schlagen Szenario 21 (N1) bzw. Szenario 14 (N2) zuverlaessig fehl.
Bewusst nicht angefasst (Review-Vorgabe): der fehlende HOME-Testfall, und
dass der Merge kein os.path.exists() prueft.
This commit is contained in:
+44
-12
@@ -77,7 +77,11 @@ class Station:
|
||||
# wurde - das ist es, was die Anzeige gross darstellt (Spec 5.2).
|
||||
self.last_seen: dict | None = None
|
||||
self._manifest_mtime: float | None = None
|
||||
self._manifest_version = "unbekannt"
|
||||
# Der geparste Inhalt von manifest.json (oder {}, wenn sie fehlt/kaputt/falsch
|
||||
# geformt ist) statt nur einer einzelnen Version - seit Fixrunde 2 (N2) traegt
|
||||
# jeder Eintrag seine eigene Version, image_version() muss also den ganzen
|
||||
# Eintragssatz vorhalten, nicht nur einen String.
|
||||
self._manifest_cache: dict = {}
|
||||
# Wird True, wenn write_control() zuletzt fehlgeschlagen ist (z.B. volles oder
|
||||
# schreibgeschuetztes /run) - claim_delivery() liest das, um "armed" nicht
|
||||
# dauerhaft scharf zu lassen, nur weil das Verbrauchen der Freigabe nicht auf
|
||||
@@ -104,26 +108,54 @@ class Station:
|
||||
def migrate_image_path(self, target: str, variant: str) -> Path:
|
||||
return self.image_dir / "migrate" / f"{target}_{variant}.img"
|
||||
|
||||
def manifest_version(self) -> str:
|
||||
"""Version aus <image_dir>/migrate/manifest.json, neu eingelesen bei jeder
|
||||
Aenderung der Datei (mtime-Vergleich). Fehlt sie oder ist sie kaputt, ist die
|
||||
Version "unbekannt" - die Migrationsimages selbst tragen keinen ninlight-Trailer,
|
||||
aus dem der Server sie lesen koennte (Spec Abschnitt 4)."""
|
||||
def _load_manifest(self) -> dict:
|
||||
"""Geparster Inhalt von <image_dir>/migrate/manifest.json, neu eingelesen bei
|
||||
jeder Aenderung der Datei (mtime-Vergleich). Fehlt sie, ist sie unlesbar/kaputtes
|
||||
JSON, oder ist ihr Wurzelelement kein Objekt, ist das Ergebnis {} - dieselbe
|
||||
Formpruefung wie beim Merge in deploy.sh (Fixrunde 2, N1), damit eine falsch
|
||||
geformte, aber syntaktisch gueltige Datei nicht mit einem AttributeError den
|
||||
Server abschiesst."""
|
||||
path = self.image_dir / "migrate" / "manifest.json"
|
||||
try:
|
||||
mtime = path.stat().st_mtime
|
||||
except OSError:
|
||||
self._manifest_mtime = None
|
||||
self._manifest_version = "unbekannt"
|
||||
return self._manifest_version
|
||||
self._manifest_cache = {}
|
||||
return self._manifest_cache
|
||||
if mtime != self._manifest_mtime:
|
||||
try:
|
||||
data = json.loads(path.read_text())
|
||||
self._manifest_version = data.get("ninlight_version", "unbekannt")
|
||||
except (OSError, ValueError):
|
||||
self._manifest_version = "unbekannt"
|
||||
data = {}
|
||||
if not isinstance(data, dict):
|
||||
data = {}
|
||||
self._manifest_cache = data
|
||||
self._manifest_mtime = mtime
|
||||
return self._manifest_version
|
||||
return self._manifest_cache
|
||||
|
||||
def image_version(self, target: str, variant: str) -> str:
|
||||
"""Firmware-Version fuer genau das Migrationsimage, das gerade ausgeliefert
|
||||
wird - die Migrationsimages selbst tragen keinen ninlight-Trailer, aus dem der
|
||||
Server sie lesen koennte (Spec Abschnitt 4). Bevorzugt die Version aus dem
|
||||
passenden Eintrag in manifest.json; nur wenn der Eintrag keine eigene traegt
|
||||
(Bestandskompatibilitaet mit einem manifestweiten Feld), faellt es auf das
|
||||
Feld auf oberster Ebene zurueck. Ein einzelnes manifestweites Feld allein
|
||||
waere seit dem Zusammenfuehren mehrerer Deploys (deploy.sh, Fixrunde 1 I1)
|
||||
schlicht nicht mehr wahr: uebernommene Alt-Eintraege koennen eine andere
|
||||
Version tragen als die gerade neu signierten (Fixrunde 2, N2)."""
|
||||
data = self._load_manifest()
|
||||
images = data.get("images")
|
||||
if isinstance(images, list):
|
||||
for entry in images:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
if entry.get("target") == target and entry.get("variant") == variant:
|
||||
version = entry.get("ninlight_version")
|
||||
if isinstance(version, str) and version:
|
||||
return version
|
||||
break
|
||||
top = data.get("ninlight_version")
|
||||
return top if isinstance(top, str) and top else "unbekannt"
|
||||
|
||||
def read_control(self):
|
||||
"""Phase und Freigabe. Fehlt die Datei oder ist sie unlesbar/kaputt, gilt "auto" -
|
||||
@@ -373,7 +405,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
return
|
||||
|
||||
target, variant, name = product.target, product.variant, product.name
|
||||
version = self.station.manifest_version()
|
||||
version = self.station.image_version(target, variant)
|
||||
# claim_delivery() liest die Phase UND verbraucht eine armed-Freigabe atomar unter
|
||||
# station.lock (Fixrunde 1, C1) - zwischen dem Lesen und dem Verbrauchen darf keine
|
||||
# zweite gleichzeitige Anfrage hindurchschluepfen, siehe die Methode selbst.
|
||||
|
||||
Reference in New Issue
Block a user