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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user