feat(updateservice): Plattform-Dimension, signierte Releases, Update mit Rollback

Behebt eine Reihe zusammenhaengender Fehler im Update-Weg, die zusammen
verhindert haben, fuer mehr als eine Plattform auszuliefern - und die im
Fehlerfall halb aktualisierte Installationen hinterliessen.

Server
- Migration 009: Spalte platform samt neuem Unique-Key. Zuvor verdraengte das
  zuletzt veroeffentlichte Paket alle anderen Plattformen derselben Version,
  weil ON DUPLICATE KEY auf (slug, version, channel) griff. Ein Linux-System
  zog sich damit das Windows-Paket.
- Aufloesungsregel: je Version das plattformgenaue Paket, sonst das
  plattformunabhaengige. Ein Client ohne Plattformangabe sieht ausschliesslich
  'any' - lieber kein Update als das falsche.
- manifest_json wird endlich befuellt; die Spalte blieb bisher immer leer,
  wodurch die API nie der Rueckfall sein konnte, als der sie gedacht war.
- Releases werden serverseitig mit RSA-SHA256 signiert, neuer Endpunkt
  /api/updateservice/v1/pubkey. Bewusst kein HMAC: der Pruefende laeuft auf
  fremden Systemen und darf den Signierschluessel nicht besitzen.

Packager
- Bricht ab, statt die Versionshistorie zu verlieren. Schlug das Lesen der
  bestehenden latest.json fehl, ersetzte ein leeres catch die komplette
  Historie durch einen einzigen Eintrag - ohne jede Meldung.
- Echte Glob-Muster. Zuvor trafen "logs/**" und "scratch/**" aus der
  mitgelieferten Beispielkonfiguration nie zu.
- preservePatterns: Konfigurationsvorlagen werden ausgeliefert, ersetzen am
  Ziel aber keine vorhandene Datei. Eine settings.json mit Zugangsdaten
  ueberschrieb bisher beim Update die Konfiguration jedes Zielsystems.
- Warnt vor Dateien, die nach Zugangsdaten aussehen und auf keiner Liste stehen.
- Prueft --version gegen die Hauptassembly. Eine Abweichung fuehrte zu einer
  Endlosschleife: Clients aktualisieren, melden weiter die alte Version,
  halten das Release erneut fuer neu.
- --platform mit Ableitung aus dem Publish-Pfad.

Agent
- Anwenden mit Plan, Backup und vollstaendigem Rollback. Die Stelle war als
  "Atomic Replace with Backup" kommentiert und war eine Kopierschleife.
- Verwaiste Dateien werden entfernt, aber nur solche aus dem Manifest der
  Vorversion. Was nicht aus einem Release stammt, bleibt liegen.
- Das laufende Agent-Binary wird zur Seite gelegt statt ueberschrieben.
- API-Rueckfall in FetchManifestAsync; bisher nur im SDK vorhanden, weshalb
  die Anwendung "Update verfuegbar" und der Agent "kein Release" sagen konnte.
- Installierte Version aus --current-version oder manifest.json statt des
  Textes "Unbekannt", der als 0 gelesen wurde und jede Version neuer erscheinen
  liess. Reparatur funktioniert damit auch ohne manifest.json.
- Setzt das Ausfuehrungsbit fuer Linux-Pakete, die unter Windows gebaut wurden.

SDK
- ResolveAgentPath() liefert den plattformrichtigen Namen; ein fest verdrahtetes
  "update-agent.exe" wird unter Linux nie gefunden.
- LaunchUpdateAgent uebergibt jetzt --restart (wurde nie uebergeben, die
  Anwendung blieb nach dem Update zu), --wait-for-pid (kein Wettlauf mehr mit
  dem Herunterfahren) und --platform.

Enthaelt ausserdem die bislang nicht committete Arbeit an Watchdog, Lizenz-
Client und cli/tick.php samt Migration 008; die betroffenen Dateien liessen
sich nicht getrennt stagen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Deploymentcenter Bot
2026-08-09 19:56:35 +02:00
co-authored by Claude Opus 5
parent 5f9b0c5596
commit 2388b5abe1
37 changed files with 5498 additions and 491 deletions
+102 -3
View File
@@ -3,7 +3,7 @@
Überwachung von Anwendungen, Diensten und Servern über Heartbeats.
> **Stand:** Version 2.0 — vollständig überarbeitet. Wer eine ältere Integration
> betreibt, findet die Änderungen in Abschnitt 8.
> betreibt, findet die Änderungen in Abschnitt 9.
---
@@ -26,10 +26,11 @@ ausgefallen.
> Dienst bliebe dauerhaft grün.
>
> ```bash
> * * * * * curl -fsS -H "Authorization: Bearer <SHARED_KEY>" https://dc.mhdf.de/api/watchdog/v1/evaluate > /dev/null
> * * * * * /usr/bin/php /pfad/zum/deploymentcenter/cli/tick.php --quiet
> ```
>
> Fehlt der Job, zeigt das WebUI oben einen Warnhinweis.
> Fehlt der Job, zeigt das WebUI oben einen Warnhinweis. Details und die
> HTTP-Variante: **[§11](#11-der-evaluator-tick)**.
---
@@ -72,6 +73,7 @@ Content-Type: application/json
"message": "Verarbeite Warteschlange",
"group": "Applications",
"os": ".NET 8 Service",
"version": "1.4.3",
"checks": { "db": { "ok": true },
"market_feed": { "ok": false, "message": "Letzter Tick vor 14 min" } },
@@ -89,9 +91,31 @@ Content-Type: application/json
| `message` | nein | Kurztext, erscheint im Dashboard |
| `group` | nein | Gruppierung im Dashboard |
| `os` | nein | Plattform, steuert auch die Icon-Erkennung |
| `version` | nein | Version der laufenden Anwendung (Alias: `app_version`) |
| `checks` | nein | Selbst ermittelter Gesundheitszustand, siehe 4. |
| `metrics` | nein | Numerische Werte, siehe 5. |
### Welche Version läuft dort?
`version` erscheint als eigene Spalte in der Monitorliste und kommt im
Heartbeat-Antwortobjekt als `app_version` zurück. Damit ist auf einen Blick
sichtbar, ob ein Ausfall zeitlich zu einem Rollout passt — „Monitor X ist seit
dem Rollout von 1.4.3 unten".
Das Feld ist optional und **überschreibt einen früheren Wert nicht mit `null`**:
ein Agent, der es nicht mitschickt, löscht die zuletzt gemeldete Version nicht.
Bestehende Agenten laufen also unverändert weiter.
```csharp
// Am einfachsten aus der vom Build erzeugten Klasse, siehe
// UPDATESERVICE_INTEGRATION_GUIDE §2B
version = BuildInfo.Version
```
> Der Fehler-Stream führt `build`, der Bugtracker `build_version`, die
> Aktivierungsliste des Lizenzmoduls `app_version` — nur der Watchdog konnte
> bis 2.1 nicht sagen, welche Version tatsächlich läuft.
### Zulässige Werte für `status`
| Wert | Zustand | Wirkung |
@@ -217,6 +241,7 @@ public sealed class WatchdogReporter
interval = 60,
message,
os = Environment.OSVersion.ToString(),
version = BuildInfo.Version, // erscheint in der Monitorliste
checks,
metrics
};
@@ -372,6 +397,8 @@ Zulässige `kind`-Werte: `started`, `stopped_graceful`, `crash_suspected`,
| Geplantes Beenden | nicht möglich | `status: "stopped"` bzw. `"maintenance"` |
| Alarme bei Ausfall eines Hosts | eine Meldung je Kind | Kinder werden unterdrückt |
| Antwortformat | uneinheitlich | `{"status":"success",…}` bzw. `{"status":"error","error":{"code":…}}` |
| Laufende Version | nicht übermittelbar | Feld `version` (seit 2.1) |
| OpenAPI-Beschreibung | `/event`, `/events`, `/status` fehlten, ebenso `group` und `type` am Ping | vollständig |
Bestehende Agenten mit `wd_live_`-Token und einfachem `ok`-Ping laufen
unverändert weiter — die neuen Felder sind alle optional.
@@ -389,3 +416,75 @@ unverändert weiter — die neuen Felder sind alle optional.
4. **`checks` nutzen.** Ein Heartbeat sagt nur, dass ein Thread läuft.
5. **Hierarchie pflegen**, wenn Dienste auf gemeinsamen Hosts laufen — sonst
bringt ein Hostausfall eine Alarmlawine.
6. **`version` mitschicken.** Ohne sie lässt sich ein Ausfall nicht mit einem
Rollout in Verbindung bringen.
---
## 11. Der Evaluator-Tick
Der Evaluator ist die Komponente, die Monitore anhand ihres erwarteten
Intervalls herabstuft. Er läuft **nicht** von selbst — ohne einen Cron-Eintrag
sind die Zustände im Dashboard wertlos.
Er erledigt in einem Durchlauf:
- Monitore anhand `expected_interval_sec` auf `warning` bzw. `down` stufen
- Zustandswechsel im Ereignisprotokoll festhalten und Webhooks auslösen
- Alarme für Kinder eines ausgefallenen Hosts unterdrücken
- abgelaufene Bugtracker-Leases freigeben
- stündlich den Metrik-Verlauf abräumen
### Interner Aufruf (empfohlen)
```bash
* * * * * /usr/bin/php /pfad/zum/deploymentcenter/cli/tick.php --quiet
```
Entspricht dem früheren `watchdog/cli/tick.php`. Kein Schlüssel im Crontab,
keine Abhängigkeit von Webserver, TLS oder DNS — und damit auch dann
lauffähig, wenn der Webserver gerade das Problem ist.
| Option | Wirkung |
|---|---|
| `-q`, `--quiet` | Ausgabe nur bei Zustandswechseln und Fehlern (für Cron) |
| `-j`, `--json` | Ergebnis maschinenlesbar |
| `--no-lock` | Sperre gegen überlappende Läufe übergehen (nur zur Fehlersuche) |
| `-h`, `--help` | Hilfe |
| Rückgabewert | Bedeutung |
|---|---|
| `0` | Lauf erfolgreich |
| `1` | Fehler — Meldung auf `stderr`, Einzelheiten im Log unter `var/log/` |
| `2` | Übersprungen, es lief bereits ein Tick |
Das Skript sperrt sich über `var/watchdog-tick.lock` selbst. Braucht ein Lauf
länger als eine Minute, überspringt der nächste Cron-Aufruf — sonst würden
zwei Evaluatoren dieselben Zustandswechsel doppelt melden.
> Der Aufruf ist auf `PHP_SAPI === 'cli'` beschränkt und `cli/` ist zusätzlich
> per `.htaccess` gesperrt. Über den Webserver ist das Skript also nicht
> erreichbar — sonst ließe sich ein Evaluationslauf ohne jede Authentifizierung
> auslösen, während der HTTP-Endpunkt dafür bewusst den Shared Key verlangt.
### Über die Schnittstelle
Sinnvoll, wenn der Cron auf einer anderen Maschine läuft:
```bash
* * * * * curl -fsS -H "Authorization: Bearer <SHARED_KEY>" https://dc.mhdf.de/api/watchdog/v1/evaluate > /dev/null
```
Beide Wege rufen denselben Code auf und schreiben denselben Lauf-Vermerk in
`watchdog_cron_jobs`. **Nur einen von beiden einrichten** — zwei parallele
Zeitpläne bringen keinen Gewinn, nur die Gefahr überlappender Läufe.
### Prüfen, ob er läuft
```bash
php cli/tick.php --json
curl https://dc.mhdf.de/api/health -H "Authorization: Bearer <SHARED_KEY>"
```
`/api/health` meldet unter `checks.evaluator` den letzten Lauf. Im WebUI zeigt
das Watchdog-Modul denselben Zustand als Abzeichen **AKTIV** / **INAKTIV**.