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:
co-authored by
Claude Opus 5
parent
5f9b0c5596
commit
2388b5abe1
@@ -16,6 +16,18 @@
|
||||
>
|
||||
> Umstellungsschritte: **[UPGRADE.md](./UPGRADE.md)**
|
||||
|
||||
> **⚠️ Änderungen in Version 2.1 — betreffen jede bestehende Integration**
|
||||
> - **Ein HTTP-Fehler ist kein Lizenzurteil mehr.** `ValidateAsync` fällt jetzt
|
||||
> bei jedem erfolglosen Aufruf auf den Offline-Cache zurück, nicht nur bei
|
||||
> einer Exception. Der Status `unknown_error` entfällt, siehe **[§6](#6-fehlerfälle-richtig-behandeln)**.
|
||||
> - **`cache_ttl_hours` wird ausgewertet.** Die Offline-Gnadenfrist ist damit
|
||||
> tatsächlich begrenzt, siehe **[§3](#3-zustandsspeicher-statestorecs--cache-härtung)**.
|
||||
> - `ValidateAsync` nimmt `appVersion` und einen `CancellationToken` entgegen.
|
||||
> - Neu: `LicenseClient.TryGetCachedKey()` und `RevalidateAsync()`.
|
||||
> - `DeactivateAsync` sendet den Schlüssel nur noch als `Authorization: Bearer`.
|
||||
> Der zusätzliche `X-Watchdog-Key` ist entfallen.
|
||||
> - Standard-Zeitgrenze von 15 s statt 100 s.
|
||||
|
||||
---
|
||||
|
||||
## 0. Antwortformat — bitte beachten
|
||||
@@ -123,12 +135,18 @@ public class Program
|
||||
string licenseKey = "LLAB1-98A72-B3C4D-5E6F7-89012";
|
||||
|
||||
// 3. Online-Validierung durchführen
|
||||
LicenseValidationResult res = await client.ValidateAsync(ProductSlug, licenseKey, ServerUrl);
|
||||
LicenseValidationResult res = await client.ValidateAsync(
|
||||
ProductSlug, licenseKey, ServerUrl, appVersion: BuildInfo.Version);
|
||||
|
||||
if (res.IsValid)
|
||||
{
|
||||
Console.WriteLine($"[✔] Lizenz gültig! (Status: {res.Status}, Cached: {res.IsCached})");
|
||||
}
|
||||
else if (res.IsTransient)
|
||||
{
|
||||
// Kein Urteil, nur keine Verbindung. Weiterlaufen lassen.
|
||||
Console.WriteLine($"[!] Lizenz vorerst nicht prüfbar: {res.Message}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"[✖] Lizenz ungültig: {res.Message}");
|
||||
@@ -138,6 +156,53 @@ public class Program
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Signatur
|
||||
|
||||
```csharp
|
||||
Task<LicenseValidationResult> ValidateAsync(
|
||||
string productSlug,
|
||||
string licenseKey,
|
||||
string serverBaseUrl,
|
||||
string? appVersion = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
```
|
||||
|
||||
`appVersion` landet in der Aktivierungsliste des WebUI. Ohne Angabe greift
|
||||
`LicenseClient.DefaultAppVersion` und danach die Version der Startassembly.
|
||||
Einmal beim Start setzen genügt:
|
||||
|
||||
```csharp
|
||||
LicenseClient.DefaultAppVersion = BuildInfo.Version;
|
||||
```
|
||||
|
||||
> Vorher stand hier fest `"1.0.0"`. In der Verwaltungsansicht trug dadurch jede
|
||||
> Installation dieselbe Version, obwohl die Spalte `app_version` dafür da ist.
|
||||
|
||||
### 2.3 Ergebnisfelder
|
||||
|
||||
| Feld | Bedeutung |
|
||||
|---|---|
|
||||
| `IsValid` | Darf die Anwendung laufen? |
|
||||
| `Status` | `valid`, `valid_offline`, `revoked`, `expired`, `not_found`, `activation_limit`, `suspended`, `cache_expired`, `clock_rollback`, `server_unavailable` |
|
||||
| `IsTransient` | **Neu.** Wahr, wenn kein Urteil vorliegt, sondern nur die Verbindung scheiterte |
|
||||
| `IsCached` | Ergebnis stammt aus dem Offline-Cache |
|
||||
| `ExpiresAt` | Ablauf der Lizenz (Unix-Zeit) |
|
||||
| `CacheExpiresAt` | **Neu.** Ende der Offline-Gnadenfrist (Unix-Zeit) |
|
||||
|
||||
### 2.4 Zwischengespeicherten Schlüssel lesen
|
||||
|
||||
```csharp
|
||||
// Für --license-status: was liegt lokal vor?
|
||||
string? key = LicenseClient.TryGetCachedKey(ProductSlug);
|
||||
LocalCacheData? state = LicenseClient.TryGetCachedState(ProductSlug);
|
||||
|
||||
// Erneut prüfen, ohne den Schlüssel selbst vorhalten zu müssen
|
||||
var res = await client.RevalidateAsync(ProductSlug, ServerUrl);
|
||||
```
|
||||
|
||||
`ValidateAsync` legt den Schlüssel bei erfolgreicher Prüfung selbst im
|
||||
verschlüsselten Cache ab — eine eigene Ablage braucht es dafür nicht.
|
||||
|
||||
---
|
||||
|
||||
## 3. Zustandsspeicher (`StateStore.cs`) & Cache-Härtung
|
||||
@@ -148,11 +213,46 @@ public class Program
|
||||
- **Linux**: Dateirechte `0600` (`chmod 600 state.dat`).
|
||||
- **Sicherheitsvorgabe**: Kein Klartext-Rückfall! Beschädigte oder manipulierte Cache-Dateien werden strikt als Cache-Fehltreffer behandelt.
|
||||
|
||||
### 3.1 Offline-Gnadenfrist
|
||||
|
||||
Der Server meldet in jeder Antwort `cache_ttl_hours` — den projektspezifischen
|
||||
Wert aus `dc_projects.default_cache_ttl_hours`, den du im WebUI unter
|
||||
**Projekte** einstellst. Der Client trägt daraus `CacheExpiresAt` in den Cache
|
||||
ein: **frühestens** die TTL, spätestens der Ablauf der Lizenz selbst.
|
||||
|
||||
> Vorher wurde das Feld ignoriert und stattdessen das Ablaufdatum der Lizenz als
|
||||
> Cache-Ablauf eingetragen. Bei einer Lizenz bis 2040 war die Frist ohne
|
||||
> Serverkontakt damit praktisch unbegrenzt — die Einstellung im WebUI blieb
|
||||
> wirkungslos.
|
||||
|
||||
Wer eine eigene Anbindung schreibt: `cache_ttl_hours` auswerten, nicht durch
|
||||
`expires_at` ersetzen.
|
||||
|
||||
### 3.2 Schema-Stand des Caches
|
||||
|
||||
`state.dat` trägt jetzt Schema 3 (zusätzlich `CacheExpiresAt`, `CacheTtlHours`).
|
||||
Schema 2 wird weiterhin gelesen; die Frist wird dort aus `IssuedAt` plus der
|
||||
Standard-TTL von 168 h abgeleitet. Ein SDK-Wechsel erzwingt also **keine**
|
||||
Online-Prüfung.
|
||||
|
||||
### 3.3 Migration fremder Cache-Dateien
|
||||
|
||||
Eine Datei ohne `LLS2`-Kopf wird nur übernommen, wenn ihr `ProductSlug` zu
|
||||
diesem Produkt passt und sie einen Lizenzschlüssel enthält.
|
||||
|
||||
> Vorher wurde beliebiges JSON nach `LocalCacheData` deserialisiert und sofort
|
||||
> zurückgeschrieben. Passte kein einziges Feld, entstand ein Standardobjekt, das
|
||||
> die ursprüngliche Datei überschrieb. Da LicenseLabrador denselben Pfad und
|
||||
> Dateinamen verwendet — `GetStorageDirectory` berücksichtigt eigens
|
||||
> `LICENSELABRADOR_STORAGE_DIR` —, zerstörte das dessen Cache still.
|
||||
|
||||
---
|
||||
|
||||
## 4. Kopfloser Betrieb (Headless Services / systemd)
|
||||
|
||||
Für Hintergrunddienste (ohne GUI) stehen folgende CLI-Schalter am Anwendungshost zur Verfügung:
|
||||
> **Diese Schalter baut deine Anwendung selbst** — das SDK bringt keine
|
||||
> CLI mit. Es liefert die Bausteine dafür; die folgende Empfehlung ist eine
|
||||
> Konvention, damit alle Dienste gleich zu bedienen sind.
|
||||
|
||||
```bash
|
||||
# Status der Hardware-ID und des lokalen Caches ausgeben
|
||||
@@ -165,6 +265,52 @@ my-service --license-set-key LLAB1-98A72-B3C4D-5E6F7-89012
|
||||
my-service --license-deactivate
|
||||
```
|
||||
|
||||
Umsetzung mit den Bausteinen des SDK:
|
||||
|
||||
```csharp
|
||||
var client = new LicenseClient();
|
||||
|
||||
switch (args[0])
|
||||
{
|
||||
case "--license-status":
|
||||
{
|
||||
var hw = HardwareId.GetHardwareId(ProductSlug);
|
||||
var state = LicenseClient.TryGetCachedState(ProductSlug);
|
||||
|
||||
Console.WriteLine($"HWID v2: {hw.HardwareId} ({hw.HwidSource})");
|
||||
Console.WriteLine($"Schlüssel: {LicenseClient.TryGetCachedKey(ProductSlug) ?? "keiner"}");
|
||||
Console.WriteLine($"Cache: {(state == null ? "leer" : state.Status)}");
|
||||
|
||||
if (state != null && state.CacheExpiresAt > 0)
|
||||
{
|
||||
var until = DateTimeOffset.FromUnixTimeSeconds(state.CacheExpiresAt);
|
||||
Console.WriteLine($"Offline noch bis: {until:u}");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
case "--license-set-key":
|
||||
// ValidateAsync legt den Schlüssel bei Erfolg selbst im Cache ab.
|
||||
var res = await client.ValidateAsync(ProductSlug, args[1], ServerUrl);
|
||||
Console.WriteLine(res.Message);
|
||||
return res.IsValid ? 0 : 1;
|
||||
|
||||
case "--license-deactivate":
|
||||
string? key = LicenseClient.TryGetCachedKey(ProductSlug);
|
||||
if (key == null) { Console.Error.WriteLine("Kein Schlüssel hinterlegt."); return 1; }
|
||||
|
||||
bool ok = await client.DeactivateAsync(
|
||||
ProductSlug, key, ServerUrl,
|
||||
Environment.GetEnvironmentVariable("DC_SHARED_KEY") ?? "");
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
```
|
||||
|
||||
> `TryGetCachedKey` und `TryGetCachedState` sind neu in 2.1. Vorher gab es
|
||||
> keinen Weg, den Schlüssel aus dem eigenen Cache zu lesen — jede Anwendung
|
||||
> musste ihn ein zweites Mal selbst ablegen oder direkt `StateStore.Load()`
|
||||
> aufrufen.
|
||||
|
||||
### Deaktivierung braucht Authentifizierung
|
||||
|
||||
`/api/license/v1/deactivate` gibt einen Aktivierungsplatz frei und ist deshalb
|
||||
@@ -192,13 +338,12 @@ curl -X POST https://dc.mhdf.de/api/license/v1/deactivate \
|
||||
Alternativ genügt für Einzelfälle der Knopf **Freigeben** in der Hardware-Liste
|
||||
des WebUI — das ist der übliche Weg und braucht keinen Schlüssel im Feld.
|
||||
|
||||
### Was passiert bei Ratenbegrenzung
|
||||
|
||||
`/validate` ist auf 120 Anfragen pro Minute und IP begrenzt. Darüber kommt
|
||||
`429` mit `{"status":"error","error":{"code":"rate_limited"}}` — hier greift
|
||||
ausnahmsweise das Umschlagformat, weil die Drosselung vor der Lizenzlogik
|
||||
zuschlägt. Ein Client sollte in dem Fall den lokalen Cache verwenden und es
|
||||
später erneut versuchen, statt die Anwendung zu blockieren.
|
||||
> **Nur noch `Authorization: Bearer`.** Frühere SDK-Fassungen schickten denselben
|
||||
> Wert zusätzlich als `X-Watchdog-Key`. Der Name führte in die Irre — hier gehört
|
||||
> der `shared_key` hin, kein Watchdog-Token — und der Server prüft
|
||||
> `X-Watchdog-Key` **vor** `Authorization`. Ein dort versehentlich eingetragenes
|
||||
> Watchdog-Token hätte das richtige Bearer-Token stillschweigend verdrängt und
|
||||
> zu einem unerklärlichen 401 geführt.
|
||||
|
||||
---
|
||||
|
||||
@@ -208,3 +353,66 @@ Wenn ein bestehender Windows-Client auf Hardware-ID v2 aktualisiert wird:
|
||||
- Der Client schickt `hardware_id` (v2) **und** `legacy_hardware_id` (v1) mit.
|
||||
- Der Server findet die alte Aktivierung unter `legacy_hardware_id` und zieht den Datenbank-Eintrag lautlos auf v2 um.
|
||||
- Es wird kein zusätzlicher Aktivierungsplatz verbraucht!
|
||||
|
||||
---
|
||||
|
||||
## 6. Fehlerfälle richtig behandeln
|
||||
|
||||
**Nur ein gelesenes Urteil ist ein Urteil.** Ein HTTP-Fehler sagt nichts über
|
||||
die Lizenz aus — er sagt, dass die Frage nicht beantwortet wurde.
|
||||
|
||||
`ValidateAsync` fällt deshalb bei **jedem** erfolglosen Aufruf auf den
|
||||
Offline-Cache zurück: Netzfehler, Zeitüberschreitung, `429`, `5xx`, und auch bei
|
||||
einer erfolgreichen Antwort, die sich nicht auswerten lässt (etwa der
|
||||
Fehlerseite eines Proxys mit Statuscode 200).
|
||||
|
||||
```csharp
|
||||
var res = await client.ValidateAsync(ProductSlug, key, ServerUrl);
|
||||
|
||||
if (res.IsValid)
|
||||
{
|
||||
// valid oder valid_offline — weiterarbeiten
|
||||
}
|
||||
else if (res.IsTransient)
|
||||
{
|
||||
// server_unavailable, cache_expired: kein Urteil, nur keine Auskunft.
|
||||
// Warnen, weiterlaufen lassen, später erneut prüfen.
|
||||
logger.Warn(res.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
// revoked, expired, not_found, activation_limit, suspended, clock_rollback
|
||||
// Das ist ein Urteil. Jetzt darf beendet werden.
|
||||
Environment.Exit(1);
|
||||
}
|
||||
```
|
||||
|
||||
| Status | `IsTransient` | Bedeutung |
|
||||
|---|---|---|
|
||||
| `valid` | – | Vom Server bestätigt |
|
||||
| `valid_offline` | – | Aus dem Cache, Gnadenfrist läuft noch |
|
||||
| `server_unavailable` | **ja** | Keine Verbindung **und** kein brauchbarer Cache |
|
||||
| `cache_expired` | **ja** | Gnadenfrist abgelaufen, Server weiterhin stumm |
|
||||
| `revoked`, `expired`, `not_found`, `activation_limit`, `suspended` | nein | Endgültig |
|
||||
| `clock_rollback` | nein | Systemuhr zurückgedreht, Online-Prüfung nötig |
|
||||
|
||||
> **Was sich geändert hat:** Der Cache-Zweig lag zuvor ausschließlich im
|
||||
> `catch`. Ein `429` oder `500` wirft aber keine Exception — die Prüfung fiel aus
|
||||
> dem Erfolgszweig heraus und endete als `unknown_error`, **ohne den Cache auch
|
||||
> nur zu befragen**. Ein Server-Schluckauf entzog damit die Lizenz, ein gezogenes
|
||||
> Netzkabel nicht. Der Status `unknown_error` existiert nicht mehr.
|
||||
|
||||
### Ratenbegrenzung
|
||||
|
||||
`/validate` ist auf 120 Anfragen pro Minute und IP begrenzt. Darüber kommt
|
||||
`429` mit `{"status":"error","error":{"code":"rate_limited"}}` — hier greift
|
||||
ausnahmsweise das Umschlagformat, weil die Drosselung vor der Lizenzlogik
|
||||
zuschlägt. Hinter einem NAT mit vielen Installationen ist das erreichbar. Der
|
||||
Client verwendet in diesem Fall den lokalen Cache und versucht es später erneut.
|
||||
|
||||
### Zeitgrenze
|
||||
|
||||
Der intern erzeugte `HttpClient` hat jetzt 15 s statt der 100 s des .NET-Standards
|
||||
— sonst stand eine Anwendung beim Start bis zu anderthalb Minuten still, wenn
|
||||
der Server nicht antwortete. Ein selbst übergebener `HttpClient` wird nicht
|
||||
verändert; dessen Zeitgrenze verantwortet der Aufrufer.
|
||||
|
||||
@@ -6,6 +6,26 @@
|
||||
> jetzt der semantischen Versionsordnung, `1.10.0` gilt also korrekt als neuer
|
||||
> als `1.9.0`. Umstellungsschritte: **[UPGRADE.md](./UPGRADE.md)**.
|
||||
|
||||
> **⚠️ Geändert in Version 2.1**
|
||||
> - `Deploymentcenter.BuildInfo.targets` erzeugt die Klasse jetzt im Namensraum
|
||||
> des einbindenden Projekts. Die vorherige Fassung war nicht einbindbar
|
||||
> (CS0433), siehe **[§2B](#b-msbuild-buildinfo-generierung)**.
|
||||
> - Der API-Rückfall in `CheckForUpdateAsync` liest die Antwort jetzt korrekt.
|
||||
> Fehlte die `latest.json`, kamen zuvor weder Download-Adresse noch Prüfsumme,
|
||||
> Changelog oder Kritikalität an, siehe **[§2C](#c-zwei-quellen-zwei-formate)**.
|
||||
|
||||
> **⚠️ Geändert in Version 2.2 — bitte vollständig lesen, bevor das nächste
|
||||
> Release gebaut wird.** Umstellungsschritte: **[UPGRADE.md §15](./UPGRADE.md#15-umstellung-auf-22)**.
|
||||
> - **Releases tragen eine Plattform.** Ohne sie überschrieben sich `win-x64`
|
||||
> und `linux-x64` gegenseitig, siehe **[§1A](#1a-plattformen)**.
|
||||
> - **Konfigurationsdateien überleben ein Update.** Bisher überschrieb jedes
|
||||
> Update die eingerichteten Werte des Zielsystems, siehe **[§3A](#3a-ausschließen-oder-schützen)**.
|
||||
> - **Anwenden mit Rollback.** Ein Abbruch hinterlässt keine halbe Installation
|
||||
> mehr, und entfernte Dateien werden aufgeräumt, siehe **[§4B](#4b-wie-ein-update-angewendet-wird)**.
|
||||
> - **Releases sind signiert**, siehe **[§6](#6-signatur-der-releases)**.
|
||||
> - `LaunchUpdateAgent` übergibt jetzt Neustart, Prozesskennung und Plattform,
|
||||
> siehe **[§2A](#a-referenz-auf-deploymentcenterclient)**.
|
||||
|
||||
|
||||
Das **UpdateService-Modul** des Deploymentcenters bietet ein unternehmensweites, leichtgewichtiges Update-, Rollback- und Reparatur-Schema auf Basis eines LEMP-Stacks (Nginx Static Files + PHP API).
|
||||
|
||||
@@ -16,10 +36,56 @@ Das **UpdateService-Modul** des Deploymentcenters bietet ein unternehmensweites,
|
||||
- **Kein dauerhafter Background-Dienst**: Hauptanwendungen prüfen beim Start einmalig schnell und netzwerktolerant auf verfügbare Updates und Dateiintegrität.
|
||||
- **Entkoppelte Ausführung**: Bei Handlungsbedarf beendet sich die Hauptanwendung sauber und übergibt die Kontrolle an den eigenständigen Console Agent (`update-agent.exe` / `update-agent`).
|
||||
- **3-Kanal-System**: Kanäle `prod` (Produktiv), `beta` (Vorab-Test), `dev` (Entwicklung).
|
||||
- **Plattform-Dimension**: je Kanal getrennte Pakete für `win-x64`, `linux-x64` usw.
|
||||
- **Statische LEMP-Verteilung**: Downloads und Versionen-Manifeste (`latest.json`, `manifest.json`, `package.tar.gz`) werden über Nginx extrem performant bereitgestellt.
|
||||
|
||||
---
|
||||
|
||||
## 1A. Plattformen
|
||||
|
||||
Ein Release wird durch **vier** Angaben bestimmt: Projekt, Kanal, Version und
|
||||
Plattform. Die Plattform ist eine .NET-Laufzeitkennung (`win-x64`,
|
||||
`linux-x64`, `linux-musl-arm64`, `osx-arm64`, …) oder `any` für Pakete, die
|
||||
überall laufen.
|
||||
|
||||
Zuvor gab es diese Dimension nicht. Wer für zwei Plattformen baute, veröffentlichte
|
||||
beide Pakete unter derselben Version im selben Kanal — das zweite überschrieb
|
||||
das erste, und ein Linux-System zog sich das Windows-Paket. Behelfe waren
|
||||
getrennte Projekt-Slugs (`myapp-win`, `myapp-linux`) oder zweckentfremdete
|
||||
Kanäle; beides trug nicht weit.
|
||||
|
||||
### Auswahlregel
|
||||
|
||||
| Client schickt | bekommt |
|
||||
|---|---|
|
||||
| `platform=win-x64` | Pakete mit `win-x64`, ersatzweise `any` |
|
||||
| `platform=linux-x64` | Pakete mit `linux-x64`, ersatzweise `any` |
|
||||
| nichts | **ausschließlich** `any` |
|
||||
|
||||
Der letzte Fall ist Absicht. Ein Client, der seine Kennung nicht nennt, soll
|
||||
lieber kein Update bekommen als das Paket einer fremden Plattform. Alle vor
|
||||
Version 2.2 veröffentlichten Releases stehen auf `any` und bleiben damit für
|
||||
bereits ausgelieferte Anwendungen erreichbar.
|
||||
|
||||
Je Version gewinnt das plattformgenaue Paket; gibt es keines, wird das
|
||||
plattformunabhängige genommen.
|
||||
|
||||
### Ablage
|
||||
|
||||
Plattformunabhängige Releases behalten den bisherigen Pfad, alle anderen
|
||||
bekommen eine Zwischenebene:
|
||||
|
||||
```text
|
||||
/releases/myapp/prod/1.4.3/package.tar.gz ← platform = any
|
||||
/releases/myapp/prod/win-x64/1.4.3/package.tar.gz ← platform = win-x64
|
||||
/releases/myapp/prod/linux-x64/1.4.3/package.tar.gz ← platform = linux-x64
|
||||
```
|
||||
|
||||
Ohne diese Rücksicht wären alle bestehenden Installationen von einem Tag auf
|
||||
den anderen von ihren Updates abgeschnitten gewesen.
|
||||
|
||||
---
|
||||
|
||||
## 2. Integration in .NET Client-Anwendungen
|
||||
|
||||
### A. Referenz auf `Deploymentcenter.Client`
|
||||
@@ -37,32 +103,118 @@ var checkResult = await updateClient.CheckForUpdateAsync(
|
||||
projectId: "myapp",
|
||||
currentVersion: BuildInfo.Version,
|
||||
channel: "prod"
|
||||
// platform: entfällt - ohne Angabe die Kennung des laufenden Systems
|
||||
);
|
||||
|
||||
if (checkResult.UpdateAvailable)
|
||||
{
|
||||
Console.WriteLine($"[UPDATE] Neues Release v{checkResult.LatestRelease.Version} verfügbar!");
|
||||
|
||||
// UpdateAgent starten und Hauptanwendung beenden
|
||||
var agentPath = UpdateClient.ResolveAgentPath();
|
||||
if (agentPath == null)
|
||||
{
|
||||
Console.WriteLine("[UPDATE] Kein update-agent gefunden.");
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateClient.LaunchUpdateAgent(
|
||||
agentPath: "update-agent.exe",
|
||||
projectId: "myapp",
|
||||
channel: "prod",
|
||||
action: "update",
|
||||
version: "latest",
|
||||
agentPath: agentPath,
|
||||
projectId: "myapp",
|
||||
channel: "prod",
|
||||
action: "update",
|
||||
version: "latest",
|
||||
currentVersion: BuildInfo.Version,
|
||||
exitCurrentApp: true
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Nicht mehr `"update-agent.exe"` fest verdrahten.** `ResolveAgentPath()`
|
||||
liefert den plattformrichtigen Namen — unter Linux und macOS trägt das Binary
|
||||
keine Endung, ein fester `.exe`-Name wird dort nie gefunden, und die Anwendung
|
||||
meldet stumm „kein Agent vorhanden".
|
||||
|
||||
Drei Dinge erledigt `LaunchUpdateAgent` seit 2.2 von selbst:
|
||||
|
||||
| Was | Warum |
|
||||
|---|---|
|
||||
| `--restart` mit dem Pfad der eigenen Anwendung | Zuvor wurde der Parameter nie übergeben. Der Agent unterstützte ihn, bekam ihn aber nie zu sehen — die Anwendung schloss sich und blieb zu |
|
||||
| `--wait-for-pid` mit der eigenen Prozesskennung | Zuvor wurde der Agent gestartet und sofort `Environment.Exit(0)` gerufen. Bei langsamem Herunterfahren (Kestrel, EF, Log-Flush) kopierte er über noch gesperrte Dateien |
|
||||
| `--platform` mit der Kennung des Systems | Verhindert, dass ein Paket der falschen Plattform gezogen wird |
|
||||
|
||||
Abschaltbar über `restartPath: ""` bzw. `waitForCurrentProcess: false`, wenn
|
||||
ein Dienst-Manager den Neustart übernimmt.
|
||||
|
||||
`currentVersion` ist wichtig für Installationen ohne `manifest.json` — siehe
|
||||
**[§4A](#4a-welche-version-ist-installiert)**.
|
||||
|
||||
### B. MSBuild BuildInfo Generierung
|
||||
|
||||
Binde das `Deploymentcenter.BuildInfo.targets` Script in deine `.csproj` ein, damit Version, UTC-Build-Datum und Git Commit-Hash automatisch zur Übersetzungszeit generiert werden:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<Version>1.4.3</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="..\Deploymentcenter.Client\Deploymentcenter.BuildInfo.targets" />
|
||||
```
|
||||
|
||||
Die Klasse entsteht im Namensraum deines Projekts (`$(RootNamespace)`), nicht im
|
||||
SDK. Du erreichst sie also ohne `using`:
|
||||
|
||||
```csharp
|
||||
Console.WriteLine(BuildInfo.Version); // "1.4.3" aus <Version>
|
||||
Console.WriteLine(BuildInfo.Summary); // v1.4.3 (5f9b0c5) built on ... [prod]
|
||||
```
|
||||
|
||||
Verfügbare Werte: `Version`, `GitCommit`, `GitCommitShort`, `BuildDateUtc`,
|
||||
`Channel`, `Summary`. Ohne Git-Arbeitskopie stehen die Commit-Felder auf
|
||||
`UNKNOWN`, der Build läuft trotzdem durch.
|
||||
|
||||
Überschreibbare MSBuild-Eigenschaften:
|
||||
|
||||
| Eigenschaft | Wirkung |
|
||||
|---|---|
|
||||
| `DeploymentcenterBuildInfoNamespace` | Zielnamensraum (Vorgabe: `$(RootNamespace)`) |
|
||||
| `DeploymentcenterBuildInfoClass` | Klassenname (Vorgabe: `BuildInfo`) |
|
||||
| `BuildChannel` | `prod`, `beta`, `dev` (Vorgabe: `prod`) |
|
||||
| `GenerateDeploymentcenterBuildInfo` | auf `false` setzen, um die Erzeugung abzuschalten |
|
||||
|
||||
> **Nicht auf `Deploymentcenter.Client.Models` zeigen lassen.** Dort liefert das
|
||||
> SDK bereits eine gleichnamige Klasse aus; `partial` verbindet Teilklassen nur
|
||||
> innerhalb derselben Assembly. Genau daran scheiterte die vorherige Fassung des
|
||||
> Targets: sie erzeugte fest in diesen Namensraum, wodurch im Consumer ein
|
||||
> zweiter Typ mit demselben vollen Namen entstand (CS0433) und der generierte
|
||||
> statische Konstruktor Eigenschaften setzte, die es dort nicht gab (CS0103).
|
||||
> Ein Import war damit unmöglich.
|
||||
|
||||
### C. Zwei Quellen, zwei Formate
|
||||
|
||||
`CheckForUpdateAsync` fragt zuerst die statische
|
||||
`/releases/{projectId}/{channel}/latest.json` ab und fällt auf
|
||||
`/api/updateservice/v1/check` zurück. **Die beiden liefern unterschiedliche
|
||||
Feldnamen:**
|
||||
|
||||
| Bedeutung | `latest.json` (Packager) | API-Antwort (Datenbank) |
|
||||
|---|---|---|
|
||||
| Download-Adresse | `packageUrl` | `download_url` |
|
||||
| Prüfsumme | `sha256` | `sha256_hash` |
|
||||
| Änderungshinweise | `changelog` | `release_notes` |
|
||||
| Größe | `sizeBytes` | `size_bytes` |
|
||||
| Kritisch | `isCritical` | `is_critical` **auf oberster Ebene** |
|
||||
|
||||
Das SDK bildet beide ab: `VersionInfo` für die `latest.json`, `ApiReleaseInfo`
|
||||
für die API-Antwort. `UpdateCheckResult.LatestRelease` ist in beiden Fällen ein
|
||||
`VersionInfo` — für die API wird übersetzt.
|
||||
|
||||
> Wer die API selbst anspricht, muss das berücksichtigen. Die vorherige SDK-Fassung
|
||||
> deserialisierte die API-Antwort direkt nach `VersionInfo`; von beiden Formaten
|
||||
> stimmt nur `version` überein. Über den API-Weg kam deshalb nichts weiter an —
|
||||
> und `IsCritical` wurde aus dem Release-Objekt statt vom Wurzelfeld gelesen und
|
||||
> war damit immer `false`. Da dieser Zweig genau der Rückfall ist, wenn die
|
||||
> `latest.json` fehlt, degradierte die Update-Prüfung still.
|
||||
|
||||
---
|
||||
|
||||
## 3. Packaging & Deployment CLI (`pack-and-deploy`)
|
||||
@@ -72,10 +224,16 @@ Das Packaging-Tool verpackt den `dotnet publish`-Output, berechnet Hashes, erzeu
|
||||
### Aufruf-Beispiel:
|
||||
|
||||
```bash
|
||||
# Automatisierter Release-Publish via CLI
|
||||
pack-and-deploy --project myapp --version 1.4.0 --channel prod --publish-dir ./bin/Release/net8.0/publish --changelog "Fehlerbehebungen und Performance-Optimierung"
|
||||
pack-and-deploy --project myapp --version 1.4.0 --channel prod \
|
||||
--platform win-x64 \
|
||||
--publish-dir ./bin/Release/net8.0/win-x64/publish \
|
||||
--changelog "Fehlerbehebungen und Performance-Optimierung"
|
||||
```
|
||||
|
||||
Ohne `--platform` versucht der Packager, die Kennung aus dem Publish-Pfad zu
|
||||
lesen (`.../net8.0/linux-x64/publish` → `linux-x64`). Gelingt das nicht, gilt
|
||||
das Release als plattformunabhängig und es erscheint eine Warnung.
|
||||
|
||||
### Konfiguration (`packager.config.json`)
|
||||
|
||||
> Diese Datei enthält Zugangsdaten und ist per `.gitignore` von der
|
||||
@@ -98,13 +256,19 @@ pack-and-deploy --project myapp --version 1.4.0 --channel prod --publish-dir ./b
|
||||
"appsettings.Development.json",
|
||||
"*.log",
|
||||
"logs/**"
|
||||
],
|
||||
"preservePatterns": [
|
||||
"appsettings.json",
|
||||
"settings.json",
|
||||
".env"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`apiToken` braucht das Recht `updateservice:publish`. Ohne Token baut und lädt
|
||||
der Packager das Paket zwar hoch, meldet es aber nicht beim Deploymentcenter an
|
||||
und beendet sich mit Rückgabewert 2.
|
||||
und beendet sich mit Rückgabewert 2. Ohne Registrierung entsteht auch **keine
|
||||
Signatur**.
|
||||
|
||||
### Alternative: Umgebungsvariablen
|
||||
|
||||
@@ -118,7 +282,8 @@ export DC_FTP_PASS='...'
|
||||
export DC_TOKEN='dc_sub_...'
|
||||
|
||||
pack-and-deploy --project myapp --version 1.4.0 --channel prod \
|
||||
--publish-dir ./bin/Release/net8.0/publish
|
||||
--platform linux-x64 \
|
||||
--publish-dir ./bin/Release/net8.0/linux-x64/publish
|
||||
```
|
||||
|
||||
### Rückgabewerte
|
||||
@@ -126,7 +291,7 @@ pack-and-deploy --project myapp --version 1.4.0 --channel prod \
|
||||
| Wert | Bedeutung |
|
||||
|---|---|
|
||||
| `0` | Paket gebaut, hochgeladen und im Deploymentcenter registriert |
|
||||
| `1` | Konfiguration unvollständig oder Publish-Verzeichnis fehlt — nichts wurde ausgeführt |
|
||||
| `1` | Konfiguration unvollständig, Publish-Verzeichnis fehlt oder Versionskonflikt — nichts wurde ausgeführt |
|
||||
| `2` | Teilweise fehlgeschlagen: FTP-Upload oder Registrierung ging schief |
|
||||
|
||||
Zuvor lieferte das Werkzeug in allen Fällen `0` und meldete „successfully
|
||||
@@ -134,6 +299,65 @@ published", selbst wenn FTP-Upload und API-Aufruf beide fehlgeschlagen waren.
|
||||
|
||||
---
|
||||
|
||||
## 3A. Ausschließen oder schützen
|
||||
|
||||
Das sind zwei verschiedene Dinge, und die Unterscheidung ist der Grund, warum
|
||||
Updates bisher Konfigurationen zerstört haben.
|
||||
|
||||
| | `excludePatterns` | `preservePatterns` |
|
||||
|---|---|---|
|
||||
| Im Paket? | nein | ja |
|
||||
| Bei der Erstinstallation? | fehlt | wird geschrieben |
|
||||
| Beim Update? | — | vorhandene Datei bleibt unangetastet |
|
||||
| Wofür | Build-Artefakte, Logs, Entwicklungs-Einstellungen | Konfigurationsvorlagen |
|
||||
|
||||
Eine `appsettings.json` gehört ins Paket — sonst ist eine Erstinstallation
|
||||
unvollständig. Sie darf beim Update nur nicht über die eingerichteten Werte des
|
||||
Zielsystems geschrieben werden. Genau dafür ist `preservePatterns` da; die
|
||||
Liste wandert ins `manifest.json` und wird vom Agenten ausgewertet.
|
||||
|
||||
> **Vorher:** `IsExcluded` verstand ausschließlich `*.endung` und exakte
|
||||
> Namen. Die mitgelieferte Beispielkonfiguration enthielt `logs/**` und
|
||||
> `scratch/**` — beides traf **nie** zu. Und eine `settings.json` mit
|
||||
> Datenbankpasswort und DC-Token stand auf keiner der beiden Listen: sie wurde
|
||||
> mitgeliefert und überschrieb beim Update die Konfiguration jedes Zielsystems.
|
||||
|
||||
Die Muster sind jetzt echte Globs:
|
||||
|
||||
| Muster | trifft |
|
||||
|---|---|
|
||||
| `*.pdb` | jede `.pdb` in jedem Unterverzeichnis |
|
||||
| `logs/**` | alles unterhalb von `logs/` |
|
||||
| `wwwroot/*.css` | nur direkt in `wwwroot/`, nicht darunter |
|
||||
| `wwwroot/**/*.css` | auch in Unterverzeichnissen |
|
||||
| `appsettings*.json` | `appsettings.json`, `appsettings.Production.json`, … |
|
||||
|
||||
Der Packager warnt zusätzlich von sich aus, wenn eine Datei nach Zugangsdaten
|
||||
aussieht und auf keiner der beiden Listen steht.
|
||||
|
||||
### Versionsgegenprobe
|
||||
|
||||
Der Packager liest die Version aus der Hauptassembly und bricht bei einer
|
||||
Abweichung zu `--version` ab:
|
||||
|
||||
```
|
||||
[FEHLER] Versionskonflikt:
|
||||
--version sagt : 1.0.2
|
||||
MyApp.dll sagt : 1.0.1
|
||||
```
|
||||
|
||||
Der Grund dafür ist unangenehm genug, um dafür abzubrechen: Wird `1.0.1` als
|
||||
`1.0.2` veröffentlicht, aktualisieren alle Clients, melden danach weiterhin
|
||||
`1.0.1`, halten das Release erneut für neu — und aktualisieren bei jedem Start
|
||||
wieder. Eine Endlosschleife über die gesamte Installationsbasis.
|
||||
|
||||
Üblicher Auslöser: `<Version>` steht nur in einem der beteiligten Projekte. Der
|
||||
Wert gehört in die `Directory.Build.props`. Notausgang für bewusste
|
||||
Abweichungen: `--ignore-version-mismatch`. Lässt sich die Assembly nicht
|
||||
bestimmen, wird nur gewarnt — `--main-assembly` gibt sie gezielt an.
|
||||
|
||||
---
|
||||
|
||||
## 4. Standalone UpdateAgent (`update-agent`)
|
||||
|
||||
Der `update-agent` kann sowohl interaktiv (Spectre.Console Terminal UI) als auch im Headless CLI-Modus betrieben werden.
|
||||
@@ -157,16 +381,92 @@ update-agent --project myapp --channel prod --action repair
|
||||
update-agent --project myapp --channel prod --action list
|
||||
```
|
||||
|
||||
Zusätzliche Parameter seit 2.2:
|
||||
|
||||
| Parameter | Wirkung |
|
||||
|---|---|
|
||||
| `--platform <rid>` | Laufzeitkennung; Vorgabe ist die des laufenden Systems |
|
||||
| `--current-version <ver>` | Installierte Version, wenn keine `manifest.json` vorliegt |
|
||||
| `--wait-for-pid <pid>` | Vor dem Anwenden auf das Ende dieses Prozesses warten |
|
||||
| `--wait-timeout <sek>` | Geduld dabei (Vorgabe 60). Läuft der Prozess danach noch, wird **nichts** verändert |
|
||||
| `--pubkey <datei>` | Öffentlicher Schlüssel zur Signaturprüfung |
|
||||
| `--require-signature` | Ohne gültige Signatur nicht installieren |
|
||||
|
||||
---
|
||||
|
||||
## 4a. Prüf-Endpunkte (für eigene Anbindungen)
|
||||
## 4A. Welche Version ist installiert?
|
||||
|
||||
Es gab zwei Antworten darauf, und sie widersprachen sich: Die Anwendung
|
||||
verglich `BuildInfo.Version` (einkompiliert), der Agent las `manifest.json` im
|
||||
Zielverzeichnis. Fehlte diese Datei — etwa bei einer von Hand aufgesetzten
|
||||
Installation — meldete der Agent „Unbekannt" und hielt **jede** Version für
|
||||
neuer. Die Reparatur suchte dann auf dem Server nach einer Version namens
|
||||
„Unbekannt" und brach genau dann ab, wenn man sie braucht.
|
||||
|
||||
Die Reihenfolge ist jetzt:
|
||||
|
||||
1. `--current-version`, falls übergeben — die Anwendung kennt ihre eigene Version am sichersten
|
||||
2. `manifest.json` im Zielverzeichnis
|
||||
3. sonst `0.0.0`, und die Reparatur greift auf `latest` zurück
|
||||
|
||||
Deshalb sollte `LaunchUpdateAgent` immer `currentVersion: BuildInfo.Version`
|
||||
mitgeben.
|
||||
|
||||
---
|
||||
|
||||
## 4B. Wie ein Update angewendet wird
|
||||
|
||||
Die Stelle war als „Atomic Replace with Backup" kommentiert und war
|
||||
tatsächlich eine Kopierschleife: kein Backup, kein Rollback, kein Aufräumen.
|
||||
Brach sie in der Mitte ab — gesperrte Datei, volle Platte —, blieb eine halb
|
||||
aktualisierte Installation zurück, aus der kein Weg zurückführte.
|
||||
|
||||
Der Ablauf ist jetzt:
|
||||
|
||||
1. **Plan bilden.** Welche Dateien werden geschrieben, welche sind geschützt,
|
||||
welche gehören nicht mehr zum Release?
|
||||
2. **Sichern.** Jede Datei, die überschrieben oder entfernt wird, wandert
|
||||
vorher nach `.dc-update-backup/`.
|
||||
3. **Anwenden.** Schreiben, dann verwaiste Dateien entfernen.
|
||||
4. **Bei einem Fehler:** vollständiger Rollback aus dem Backup, danach wird die
|
||||
Ursache gemeldet. Die Installation bleibt auf dem alten Stand lauffähig.
|
||||
5. **Bei Erfolg:** Backup löschen, leer gewordene Verzeichnisse entfernen.
|
||||
|
||||
### Verwaiste Dateien
|
||||
|
||||
Eine DLL, die es im neuen Release nicht mehr gibt, blieb bisher für immer im
|
||||
Verzeichnis liegen — bei .NET ein realer Weg in kaputte Assembly-Auflösung.
|
||||
Sie wird jetzt entfernt, aber **nur**, wenn sie in der `manifest.json` der
|
||||
Vorversion stand. Ohne dieses Wissen wird nichts gelöscht; Dateien, die nicht
|
||||
aus einem Release stammen, bleiben in jedem Fall unangetastet.
|
||||
|
||||
### Der Agent im Paket
|
||||
|
||||
`--target-dir` zeigt in der Vorgabe auf das Verzeichnis des Agenten selbst.
|
||||
Liegt der Agent im Paket, kopierte er sich also unter laufendem Betrieb über
|
||||
sich selbst — unter Windows eine Zugriffsverletzung mitten im Update.
|
||||
|
||||
Eine laufende ausführbare Datei lässt sich unter Windows nicht überschreiben,
|
||||
aber umbenennen. Der Agent legt sich deshalb als `update-agent.exe.dc-old` zur
|
||||
Seite, schreibt die neue Fassung und entfernt den Rest beim nächsten Start.
|
||||
|
||||
### Ausführungsrechte
|
||||
|
||||
Wird unter Windows für `linux-x64` gebaut, kennt das tar-Archiv keine
|
||||
Unix-Rechte und alles landet als `644` — die Anwendung ließe sich auf dem
|
||||
Zielsystem nicht starten. Der Agent setzt das Ausführungsbit beim Anwenden für
|
||||
Dateien ohne Endung (der .NET-Apphost) und für `*.sh`.
|
||||
|
||||
---
|
||||
|
||||
## 4C. Prüf-Endpunkte (für eigene Anbindungen)
|
||||
|
||||
Die Lese-Endpunkte sind bewusst **ohne Token** erreichbar, damit ausgelieferte
|
||||
Anwendungen ohne Anpassung weiter nach Updates suchen können. Sie liefern nur
|
||||
Release-Metadaten, die über die Download-URL ohnehin öffentlich sind.
|
||||
|
||||
```bash
|
||||
GET /api/updateservice/v1/check?product=myapp&version=1.4.2&channel=prod
|
||||
GET /api/updateservice/v1/check?product=myapp&version=1.4.2&channel=prod&platform=win-x64
|
||||
```
|
||||
|
||||
```json
|
||||
@@ -175,25 +475,33 @@ GET /api/updateservice/v1/check?product=myapp&version=1.4.2&channel=prod
|
||||
"update_available": true,
|
||||
"current_version": "1.4.2",
|
||||
"latest_version": "1.4.3",
|
||||
"platform": "win-x64",
|
||||
"is_critical": false,
|
||||
"latest_release": {
|
||||
"version": "1.4.3",
|
||||
"download_url": "https://dc.mhdf.de/releases/myapp/prod/1.4.3/package.tar.gz",
|
||||
"platform": "win-x64",
|
||||
"download_url": "https://dc.mhdf.de/releases/myapp/prod/win-x64/1.4.3/package.tar.gz",
|
||||
"sha256_hash": "e3b0c442...",
|
||||
"git_commit": "a21536f",
|
||||
"size_bytes": 8412160,
|
||||
"release_notes": "Behebt den Login-Fehler.",
|
||||
"manifest_signature": "hsuQVhef...",
|
||||
"is_critical": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **Ohne `platform` werden ausschließlich Releases mit `platform=any`
|
||||
> berücksichtigt.** Wer die Endpunkte selbst anspricht und für mehrere
|
||||
> Plattformen ausliefert, muss den Parameter mitschicken.
|
||||
|
||||
Weitere Endpunkte:
|
||||
|
||||
| Aufruf | Zweck |
|
||||
|---|---|
|
||||
| `GET .../latest?product=myapp&channel=prod` | Höchstes Release, unabhängig von der Client-Version |
|
||||
| `GET .../releases?product=myapp` | Alle Releases, nach Versionsordnung sortiert |
|
||||
| `GET .../latest?product=myapp&channel=prod&platform=win-x64` | Höchstes Release, unabhängig von der Client-Version |
|
||||
| `GET .../releases?product=myapp` | Alle Releases, nach Versionsordnung sortiert. `platform` filtert hier **exakt** — der Endpunkt listet den Bestand, er wählt kein Paket aus |
|
||||
| `GET .../pubkey` | Öffentlicher Schlüssel zur Signaturprüfung |
|
||||
|
||||
### Versionsvergleich
|
||||
|
||||
@@ -232,14 +540,97 @@ v1.4.3", der Packager veröffentlicht v1.4.3, und das Item schließt sich selbst
|
||||
|
||||
```text
|
||||
/var/www/releases/ (oder /public_html/releases/)
|
||||
└── {ProjectId}/ # z.B. myapp, polytrader
|
||||
└── {ProjectId}/ # z.B. myapp, polytrader
|
||||
├── prod/
|
||||
│ ├── latest.json # Kanal-Übersicht & neueste Version
|
||||
│ ├── 1.4.0/
|
||||
│ │ ├── package.tar.gz # Das gezippte Release
|
||||
│ ├── latest.json # nur platform = any
|
||||
│ ├── 1.4.0/ # nur platform = any
|
||||
│ │ ├── package.tar.gz
|
||||
│ │ ├── package.tar.gz.sha256
|
||||
│ │ └── manifest.json # Einzeldateien + Hashes
|
||||
│ └── 1.3.9/
|
||||
│ │ └── manifest.json # Einzeldateien, Hashes, preserve-Liste
|
||||
│ ├── win-x64/
|
||||
│ │ ├── latest.json # eigene Historie je Plattform
|
||||
│ │ └── 1.4.0/
|
||||
│ │ ├── package.tar.gz
|
||||
│ │ ├── package.tar.gz.sha256
|
||||
│ │ └── manifest.json
|
||||
│ └── linux-x64/
|
||||
│ ├── latest.json
|
||||
│ └── 1.4.0/
|
||||
├── beta/
|
||||
└── dev/
|
||||
```
|
||||
|
||||
Jede Plattform führt ihre eigene `latest.json`. Der Agent fragt zuerst den
|
||||
plattformspezifischen Pfad ab und fällt auf den plattformlosen zurück — Pakete
|
||||
einer fremden Plattform werden dabei verworfen.
|
||||
|
||||
### Aufbewahrung
|
||||
|
||||
`latest.json` führt die letzten 15 Versionen. Ältere Versionsverzeichnisse
|
||||
bleiben auf dem Server liegen, sind über den Agenten aber nicht mehr
|
||||
auswählbar. Der Packager weist beim Herausfallen einer Version ausdrücklich
|
||||
darauf hin; wer weiter zurück muss, holt das Paket von Hand.
|
||||
|
||||
---
|
||||
|
||||
## 6. Signatur der Releases
|
||||
|
||||
Der SHA256 eines Pakets stammt aus derselben Quelle wie das Paket selbst. Wer
|
||||
den Webroot oder die FTP-Zugangsdaten kontrolliert, tauscht beide gemeinsam
|
||||
aus — der Hash schützt dann gegen Übertragungsfehler, nicht gegen Manipulation.
|
||||
Ausgerechnet auf dem Pfad, der fremden Code ausführt.
|
||||
|
||||
### Warum kein HMAC
|
||||
|
||||
Beim Lizenzmodul wird mit HMAC signiert, und das geht dort auf, weil der
|
||||
**Server** prüft. Ein Update wird auf dem Zielsystem geprüft. Ein HMAC bräuchte
|
||||
dort denselben geheimen Schlüssel wie auf dem Server; wer ihn ausliest, kann
|
||||
beliebige Pakete signieren — die Signatur verlöre genau die Eigenschaft, wegen
|
||||
der es sie gibt.
|
||||
|
||||
Deshalb asymmetrisch: der Server signiert mit einem privaten RSA-Schlüssel, der
|
||||
Agent prüft mit dem öffentlichen.
|
||||
|
||||
### Einrichten
|
||||
|
||||
```bash
|
||||
openssl genrsa -out /etc/dc/release-signing.pem 2048
|
||||
chmod 600 /etc/dc/release-signing.pem
|
||||
```
|
||||
|
||||
```php
|
||||
// config/config.php
|
||||
'release_private_key' => dc_env('DC_RELEASE_SIGNING_KEY', '/etc/dc/release-signing.pem'),
|
||||
```
|
||||
|
||||
Signiert wird beim Veröffentlichen, serverseitig. **Der Packager bekommt den
|
||||
Schlüssel nicht zu sehen** — er läuft auf Entwicklerrechnern, und der Schlüssel
|
||||
wäre so gut geschützt wie das schwächste dieser Systeme.
|
||||
|
||||
Signiert wird eine kanonische Zeile, nicht das Manifest-JSON: JSON-Ausgabe ist
|
||||
nicht bytestabil (Schlüsselreihenfolge, Escaping, Zahlenformat), eine Signatur
|
||||
darüber wäre unzuverlässig prüfbar.
|
||||
|
||||
```
|
||||
dc-release-v1\n{product_slug}\n{version}\n{channel}\n{platform}\n
|
||||
{sha256_hash klein}\n{download_url}\n{size_bytes}
|
||||
```
|
||||
|
||||
### Prüfen
|
||||
|
||||
Der Agent holt den öffentlichen Schlüssel einmalig von
|
||||
`/api/updateservice/v1/pubkey` und legt ihn als `dc-release-pubkey.pem` neben
|
||||
sich ab. Meldet der Server später einen **anderen** Schlüssel, wird gewarnt und
|
||||
weiterhin der hinterlegte benutzt — ein untergeschobener Server fällt damit
|
||||
auf. War der Wechsel beabsichtigt, die Datei löschen.
|
||||
|
||||
| Lage | Verhalten |
|
||||
|---|---|
|
||||
| Signatur gültig | Installation läuft |
|
||||
| Signatur ungültig | **Abbruch**, immer |
|
||||
| Release unsigniert | Hinweis, Installation läuft |
|
||||
| Kein öffentlicher Schlüssel | Hinweis, Installation läuft |
|
||||
| `--require-signature` gesetzt | Die letzten beiden Fälle brechen ebenfalls ab |
|
||||
|
||||
Ohne hinterlegten Schlüssel bleibt also alles funktionsfähig — es fehlt nur die
|
||||
Vertrauenskette, und darauf wird bei jedem Update hingewiesen.
|
||||
|
||||
+217
@@ -3,6 +3,16 @@
|
||||
Diese Fassung enthält Sicherheitskorrekturen, die das Verhalten der
|
||||
Schnittstellen ändern. Bitte in dieser Reihenfolge vorgehen.
|
||||
|
||||
> **Version 2.1** ergänzt Korrekturen im .NET-SDK und ein neues Feld am
|
||||
> Heartbeat. Sie erfordert keine Konfigurationsänderung, aber eine
|
||||
> Datenbankmigration und ein Nachziehen der Clients:
|
||||
> **[§14 Umstellung auf 2.1](#14-umstellung-auf-21)**.
|
||||
>
|
||||
> **Version 2.2** überarbeitet den UpdateService grundlegend: Plattform-Dimension,
|
||||
> signierte Releases, Staging-Swap mit Rollback und geschützte Konfigurationsdateien.
|
||||
> **Vor dem nächsten Release einzuspielen** —
|
||||
> **[§15 Umstellung auf 2.2](#15-umstellung-auf-22)**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Vor dem Deployment: Zugangsdaten wechseln
|
||||
@@ -102,10 +112,38 @@ Die Migration ist additiv und legt an bzw. korrigiert:
|
||||
bisher vollständig — der Zustand änderte sich nur beim Eintreffen eines
|
||||
Heartbeats, ein ausgefallener Server blieb dauerhaft grün.
|
||||
|
||||
### Empfohlen: interner Aufruf (seit 2.1)
|
||||
|
||||
```bash
|
||||
* * * * * /usr/bin/php /pfad/zum/deploymentcenter/cli/tick.php --quiet
|
||||
```
|
||||
|
||||
Das entspricht dem früheren `watchdog/cli/tick.php`. Gegenüber dem HTTP-Weg:
|
||||
|
||||
- **Kein Schlüssel im Crontab.** `ps` zeigt die Argumente laufender Prozesse
|
||||
jedem Nutzer der Maschine — bei einem `curl -H "Authorization: Bearer …"`
|
||||
also auch den Shared Key.
|
||||
- **Keine Abhängigkeit von Webserver, TLS oder DNS.** Der Evaluator läuft auch
|
||||
dann, wenn der Webserver klemmt — also gerade dann, wenn das Erkennen von
|
||||
Ausfällen wichtig ist.
|
||||
- **Keine überlappenden Läufe.** Das Skript sperrt sich per `flock` selbst; ein
|
||||
langsamer Vorlauf führt nicht zu doppelten Alarmen.
|
||||
|
||||
`--quiet` gibt nur bei Zustandswechseln und Fehlern etwas aus — sonst erzeugt
|
||||
ein minütlicher Job 1440 Cron-Mails am Tag. Rückgabewerte: `0` erfolgreich,
|
||||
`1` Fehler, `2` übersprungen (es lief bereits ein Tick).
|
||||
|
||||
### Alternativ: über die Schnittstelle
|
||||
|
||||
Sinnvoll, wenn der Cron auf einer anderen Maschine läuft als die Anwendung:
|
||||
|
||||
```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.
|
||||
|
||||
Solange der Job fehlt, zeigt das WebUI oben einen Warnhinweis mit einer
|
||||
Schaltfläche für einen einmaligen Lauf.
|
||||
|
||||
@@ -279,3 +317,182 @@ X-DC-Signature: sha256=<hex(hmac_sha256(secret, timestamp + "." + body))>
|
||||
```
|
||||
|
||||
Nach 20 Fehlversuchen in Folge deaktiviert sich ein Webhook selbst.
|
||||
|
||||
---
|
||||
|
||||
## 14. Umstellung auf 2.1
|
||||
|
||||
Diese Fassung korrigiert Fehler im .NET-SDK, ergänzt ein Feld am Heartbeat und
|
||||
vervollständigt die OpenAPI-Beschreibung. **Keine Konfigurationsänderung nötig.**
|
||||
|
||||
### 14.1 Serverseitig
|
||||
|
||||
- [ ] Migration **008** ausführen (Spalte `app_version` in `watchdog_monitors`).
|
||||
Läuft wie die übrigen über den Migrator, siehe [§4](#4-migration-ausführen).
|
||||
- [ ] `/api/openapi.json` abrufen und prüfen, dass die Lizenz-Endpunkte und
|
||||
`/api/watchdog/v1/event`, `/events`, `/status` jetzt enthalten sind.
|
||||
|
||||
Bestehende Agenten laufen unverändert weiter; alle neuen Felder sind optional.
|
||||
|
||||
### 14.2 Clientseitig (`Deploymentcenter.Client`)
|
||||
|
||||
Die folgenden Änderungen betreffen jede bestehende Integration:
|
||||
|
||||
| Änderung | Was zu tun ist |
|
||||
|---|---|
|
||||
| `unknown_error` entfällt | Wer diesen Status abfängt, prüft stattdessen `IsTransient` |
|
||||
| Cache-Rückfall bei jedem HTTP-Fehler | Eigene Behelfslösungen für 429/5xx können entfallen |
|
||||
| `cache_ttl_hours` wird ausgewertet | Offline-Gnadenfrist prüfen: Vorgabe je Projekt im WebUI |
|
||||
| `app_version` ist Parameter | `LicenseClient.DefaultAppVersion` beim Start setzen |
|
||||
| `X-Watchdog-Key` entfällt bei `DeactivateAsync` | Nur noch `Authorization: Bearer` |
|
||||
| `BuildInfo.targets` erzeugt in `$(RootNamespace)` | Import ist jetzt möglich; ggf. `using` entfernen |
|
||||
| API-Rückfall der Update-Prüfung liefert vollständige Daten | Eigene Umgehungen können entfallen |
|
||||
|
||||
Ausführlich: **[LICENSE_INTEGRATION_GUIDE §6](./LICENSE_INTEGRATION_GUIDE.md)**
|
||||
und **[UPDATESERVICE_INTEGRATION_GUIDE §2B/§2C](./UPDATESERVICE_INTEGRATION_GUIDE.md)**.
|
||||
|
||||
### 14.3 Cache-Schema
|
||||
|
||||
`state.dat` trägt jetzt Schema 3. Schema 2 wird weiterhin gelesen — ein
|
||||
SDK-Wechsel erzwingt **keine** erneute Online-Prüfung. Ein Rückschritt auf ein
|
||||
älteres SDK dagegen schon: das kennt Schema 3 nicht und behandelt den Cache als
|
||||
Fehltreffer.
|
||||
|
||||
---
|
||||
|
||||
## 15. Umstellung auf 2.2
|
||||
|
||||
Diese Fassung behebt eine Reihe von Fehlern im Update-Weg, die zusammengenommen
|
||||
verhindert haben, dass für mehr als eine Plattform ausgeliefert werden konnte —
|
||||
und die im Fehlerfall halb aktualisierte Installationen hinterließen.
|
||||
|
||||
**Diese Umstellung gehört vor das nächste Release**, nicht danach.
|
||||
|
||||
### 15.1 Was sich ändert
|
||||
|
||||
| Bereich | Vorher | Jetzt |
|
||||
|---|---|---|
|
||||
| Plattformen | nicht vorhanden — `win-x64` und `linux-x64` überschrieben sich | eigene Dimension `platform`, getrennte Ablage |
|
||||
| Konfigurationsdateien | wurden beim Update überschrieben | `preservePatterns` schützt sie |
|
||||
| Ausschlussmuster | nur `*.endung` und exakte Namen | echte Globs (`**`, `*`, `?`) |
|
||||
| Abbruch beim Anwenden | halb aktualisierte Installation | Backup, automatischer Rollback |
|
||||
| Entfernte Dateien | blieben für immer liegen | werden aufgeräumt |
|
||||
| Agent im Paket | überschrieb sich selbst | wird zur Seite gelegt und ersetzt |
|
||||
| Übergabe an den Agenten | Wettlauf, kein Neustart | PID-Handschlag und `--restart` |
|
||||
| Versionsangabe | ungeprüft | wird gegen die Assembly gehalten |
|
||||
| Paketherkunft | nur SHA256 aus derselben Quelle | RSA-Signatur des Servers |
|
||||
| `manifest_json` | Spalte blieb immer leer | wird befüllt, API ist vollwertiger Rückfall |
|
||||
|
||||
### 15.2 Serverseitig
|
||||
|
||||
- [ ] Migration **009** ausführen (`platform`, `manifest_signature`, neuer
|
||||
Unique-Key). Additiv; bestehende Releases erhalten `platform = 'any'` und
|
||||
bleiben für alle Clients erreichbar.
|
||||
- [ ] Signierschlüssel erzeugen und in `config/config.php` eintragen:
|
||||
|
||||
```bash
|
||||
openssl genrsa -out /etc/dc/release-signing.pem 2048
|
||||
chmod 600 /etc/dc/release-signing.pem
|
||||
chown www-data:www-data /etc/dc/release-signing.pem
|
||||
```
|
||||
|
||||
```php
|
||||
'release_private_key' => dc_env('DC_RELEASE_SIGNING_KEY', '/etc/dc/release-signing.pem'),
|
||||
```
|
||||
|
||||
- [ ] Prüfen, dass der öffentliche Schlüssel ausgeliefert wird:
|
||||
|
||||
```bash
|
||||
curl https://dc.mhdf.de/api/updateservice/v1/pubkey
|
||||
```
|
||||
|
||||
> **Bewusst RSA und kein HMAC.** Bei einem HMAC braucht der Prüfende denselben
|
||||
> geheimen Schlüssel wie der Signierende. Der Agent läuft auf fremden Systemen;
|
||||
> ein dort hinterlegter Schlüssel ließe sich auslesen und zum Signieren
|
||||
> beliebiger Pakete verwenden — die Signatur wäre wertlos. Beim Lizenzmodul geht
|
||||
> HMAC auf, weil dort der Server prüft.
|
||||
|
||||
Ohne Schlüssel bleibt alles funktionsfähig; Releases sind dann unsigniert, und
|
||||
der Agent weist bei jedem Update darauf hin.
|
||||
|
||||
### 15.3 Packager
|
||||
|
||||
- [ ] `packager.config.json` um `preservePatterns` ergänzen (Vorlage in
|
||||
`packager.config.example.json`).
|
||||
- [ ] **Prüfen, welche Konfigurationsdateien im Publish-Verzeichnis liegen.**
|
||||
Der Packager warnt jetzt von sich aus vor Dateien, die nach Zugangsdaten
|
||||
aussehen und weder ausgeschlossen noch geschützt sind. Diese Warnung ernst
|
||||
nehmen: eine `settings.json` mit Datenbankpasswort, die bisher mitgeliefert
|
||||
wurde, hat beim Update die Konfiguration jedes Zielsystems überschrieben.
|
||||
- [ ] Aufrufe um `--platform` ergänzen:
|
||||
|
||||
```bash
|
||||
pack-and-deploy --project myapp --version 1.4.3 --channel prod \
|
||||
--platform win-x64 \
|
||||
--publish-dir ./bin/Release/net8.0/win-x64/publish
|
||||
```
|
||||
|
||||
Ohne `--platform` versucht der Packager, die Kennung aus dem Publish-Pfad
|
||||
abzuleiten. Gelingt das nicht, gilt das Release als plattformunabhängig und es
|
||||
erscheint eine Warnung.
|
||||
|
||||
- [ ] Die Version wird jetzt gegen die Hauptassembly geprüft. Weicht sie ab,
|
||||
bricht der Vorgang ab. Das ist der Fall, in dem `<Version>` nur in einem
|
||||
der beteiligten Projekte steht — der Wert gehört in die
|
||||
`Directory.Build.props`, damit alle denselben tragen. Notausgang für
|
||||
bewusste Abweichungen: `--ignore-version-mismatch`.
|
||||
|
||||
### 15.4 Clientseitig
|
||||
|
||||
`UpdateClient.CheckForUpdateAsync` hat einen zusätzlichen Parameter `platform`
|
||||
**vor** dem `CancellationToken`. Wer den Token bisher positionell übergeben hat,
|
||||
bekommt einen Übersetzungsfehler — kein stilles Fehlverhalten.
|
||||
|
||||
Der Aufruf des Agenten sollte umgestellt werden:
|
||||
|
||||
```csharp
|
||||
var agentPath = UpdateClient.ResolveAgentPath(); // plattformrichtiger Name
|
||||
|
||||
if (agentPath != null && checkResult.UpdateAvailable)
|
||||
{
|
||||
UpdateClient.LaunchUpdateAgent(
|
||||
agentPath: agentPath,
|
||||
projectId: "myapp",
|
||||
channel: "prod",
|
||||
currentVersion: BuildInfo.Version, // für Ziele ohne manifest.json
|
||||
exitCurrentApp: true); // Neustart und PID-Handschlag
|
||||
// sind jetzt Vorgabe
|
||||
}
|
||||
```
|
||||
|
||||
| Änderung | Wirkung |
|
||||
|---|---|
|
||||
| `ResolveAgentPath()` | Findet `update-agent` bzw. `update-agent.exe` — ein fest verdrahteter `.exe`-Name wird unter Linux nie gefunden |
|
||||
| `--restart` wird übergeben | Die Anwendung startet nach dem Update wieder. Bisher blieb sie einfach zu |
|
||||
| `--wait-for-pid` wird übergeben | Der Agent wartet auf das Ende der Anwendung, statt über gesperrte Dateien zu kopieren |
|
||||
| `--platform` wird übergeben | Es kann kein Paket einer fremden Plattform mehr gezogen werden |
|
||||
|
||||
### 15.5 Den Agenten aus dem Paket nehmen
|
||||
|
||||
Liegt `update-agent` im Publish-Verzeichnis, wandert er ins Paket und wird beim
|
||||
Update mit ausgeliefert. Das ist zulässig — der Agent legt seine laufende Datei
|
||||
inzwischen zur Seite und ersetzt sie —, aber es lohnt sich, bewusst zu
|
||||
entscheiden:
|
||||
|
||||
- **Mitliefern:** Der Agent aktualisiert sich selbst mit. Die alte Fassung bleibt
|
||||
als `update-agent.exe.dc-old` liegen und wird beim nächsten Start entfernt.
|
||||
- **Ausschließen:** `"update-agent*"` in `excludePatterns`. Dann wird der Agent
|
||||
getrennt gepflegt.
|
||||
|
||||
### 15.6 Prüfen
|
||||
|
||||
- [ ] `curl "https://dc.mhdf.de/api/updateservice/v1/check?product=myapp&version=0.0.0&channel=prod&platform=win-x64"`
|
||||
liefert das Windows-Paket, mit `platform=linux-x64` das Linux-Paket.
|
||||
- [ ] Ohne `platform` kommen ausschließlich Releases mit `platform=any`. Das ist
|
||||
Absicht: ein Client, der seine Kennung nicht nennt, soll lieber kein Update
|
||||
bekommen als das falsche.
|
||||
- [ ] Im WebUI zeigt die Release-Übersicht eine Spalte **Plattform** und
|
||||
kennzeichnet unsignierte Releases.
|
||||
- [ ] Ein Update auf einem Testsystem: `appsettings.json` behält danach die
|
||||
eingerichteten Werte, und eine Datei, die es im neuen Release nicht mehr
|
||||
gibt, ist verschwunden.
|
||||
|
||||
@@ -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**.
|
||||
|
||||
Reference in New Issue
Block a user