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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Deploymentcenter Bot
2026-08-09 19:56:35 +02:00
co-authored by Claude Opus 5
parent 5f9b0c5596
commit 2388b5abe1
37 changed files with 5498 additions and 491 deletions
@@ -1,49 +1,117 @@
<Project>
<!-- MSBuild target to generate BuildInfo.g.cs automatically prior to compilation -->
<Target Name="GenerateDeploymentcenterBuildInfo" BeforeTargets="CoreCompile">
<!--
Erzeugt vor jeder Uebersetzung eine BuildInfo-Klasse mit Version,
UTC-Build-Datum und Git-Commit.
Einbindung in der .csproj des Consumers:
<Import Project="..\Deploymentcenter.Client\Deploymentcenter.BuildInfo.targets" />
Die Klasse entsteht im Namensraum des einbindenden Projekts
($(RootNamespace)), nicht in dem des SDK.
Die vorherige Fassung erzeugte "public static partial class BuildInfo" im
Namensraum Deploymentcenter.Client.Models. Dort liefert das SDK aber bereits
eine gleichnamige, nicht partielle Klasse aus: im Consumer entstand ein
zweiter Typ mit demselben vollen Namen in einer zweiten Assembly (CS0433),
und der generierte statische Konstruktor setzte Eigenschaften, die in seiner
Teilklasse gar nicht deklariert waren (CS0103). Einbinden war damit
unmoeglich - obwohl der UpdateService-Leitfaden genau dazu riet. Der
generierte Code deklariert seine Werte jetzt selbst und braucht keine
Gegenstelle im SDK.
Ueberschreibbare Eigenschaften:
DeploymentcenterBuildInfoNamespace Zielnamensraum (Vorgabe: RootNamespace)
DeploymentcenterBuildInfoClass Klassenname (Vorgabe: BuildInfo)
BuildChannel prod, beta, ... (Vorgabe: prod)
GenerateDeploymentcenterBuildInfo auf false setzen, um abzuschalten
-->
<PropertyGroup>
<GenerateDeploymentcenterBuildInfo Condition="'$(GenerateDeploymentcenterBuildInfo)' == ''">true</GenerateDeploymentcenterBuildInfo>
<DeploymentcenterBuildInfoClass Condition="'$(DeploymentcenterBuildInfoClass)' == ''">BuildInfo</DeploymentcenterBuildInfoClass>
</PropertyGroup>
<Target Name="GenerateDeploymentcenterBuildInfo"
BeforeTargets="CoreCompile"
Condition="'$(GenerateDeploymentcenterBuildInfo)' == 'true'">
<PropertyGroup>
<BuildInfoFile>$(IntermediateOutputPath)BuildInfo.g.cs</BuildInfoFile>
<!-- Zielnamensraum: ausdrueckliche Angabe, sonst RootNamespace, sonst Projektname. -->
<DeploymentcenterBuildInfoNamespace Condition="'$(DeploymentcenterBuildInfoNamespace)' == ''">$(RootNamespace)</DeploymentcenterBuildInfoNamespace>
<DeploymentcenterBuildInfoNamespace Condition="'$(DeploymentcenterBuildInfoNamespace)' == ''">$(MSBuildProjectName)</DeploymentcenterBuildInfoNamespace>
<BuildInfoFile>$(IntermediateOutputPath)DeploymentcenterBuildInfo.g.cs</BuildInfoFile>
<BuildDateUtc>$([System.DateTime]::UtcNow.ToString("o"))</BuildDateUtc>
<BuildVersion Condition="'$(Version)' != ''">$(Version)</BuildVersion>
<BuildVersion Condition="'$(BuildVersion)' == ''">1.0.0</BuildVersion>
<!-- Version aus <Version>, ersatzweise <AssemblyVersion>, sonst 0.0.0. -->
<BuildVersion Condition="'$(BuildVersion)' == ''">$(Version)</BuildVersion>
<BuildVersion Condition="'$(BuildVersion)' == ''">$(AssemblyVersion)</BuildVersion>
<BuildVersion Condition="'$(BuildVersion)' == ''">0.0.0</BuildVersion>
<BuildChannel Condition="'$(BuildChannel)' == ''">prod</BuildChannel>
</PropertyGroup>
<Exec Command="git rev-parse HEAD" ConsoleToMSBuild="true" IgnoreExitCode="true">
<Output TaskParameter="ConsoleOutput" PropertyName="GitCommitLong" />
<!--
ContinueOnError, damit ein Build ohne Git-Arbeitskopie oder ohne
installiertes Git nicht scheitert. IgnoreExitCode allein genuegte nicht:
fehlt die ausfuehrbare Datei, bricht Exec selbst ab.
-->
<Exec Command="git rev-parse HEAD"
ConsoleToMSBuild="true"
IgnoreExitCode="true"
ContinueOnError="true"
StandardErrorImportance="low">
<Output TaskParameter="ConsoleOutput" PropertyName="DcGitCommitLong" />
</Exec>
<Exec Command="git rev-parse --short HEAD" ConsoleToMSBuild="true" IgnoreExitCode="true">
<Output TaskParameter="ConsoleOutput" PropertyName="GitCommitShort" />
<Exec Command="git rev-parse --short HEAD"
ConsoleToMSBuild="true"
IgnoreExitCode="true"
ContinueOnError="true"
StandardErrorImportance="low">
<Output TaskParameter="ConsoleOutput" PropertyName="DcGitCommitShort" />
</Exec>
<PropertyGroup>
<GitCommitLong Condition="'$(GitCommitLong)' == ''">UNKNOWN_COMMIT</GitCommitLong>
<GitCommitShort Condition="'$(GitCommitShort)' == ''">UNKNOWN</GitCommitShort>
<DcGitCommitLong Condition="'$(DcGitCommitLong)' == ''">UNKNOWN_COMMIT</DcGitCommitLong>
<DcGitCommitShort Condition="'$(DcGitCommitShort)' == ''">UNKNOWN</DcGitCommitShort>
<!-- Zeilenumbrueche aus der Git-Ausgabe entfernen, sie wuerden das Literal sprengen. -->
<DcGitCommitLong>$(DcGitCommitLong.Trim())</DcGitCommitLong>
<DcGitCommitShort>$(DcGitCommitShort.Trim())</DcGitCommitShort>
</PropertyGroup>
<ItemGroup>
<BuildInfoLine Include="// &lt;auto-generated /&gt;" />
<BuildInfoLine Include="using System%3B" />
<BuildInfoLine Include="namespace Deploymentcenter.Client.Models" />
<BuildInfoLine Include="// Erzeugt von Deploymentcenter.BuildInfo.targets - nicht von Hand aendern." />
<BuildInfoLine Include="namespace $(DeploymentcenterBuildInfoNamespace)" />
<BuildInfoLine Include="{" />
<BuildInfoLine Include=" public static partial class BuildInfo" />
<BuildInfoLine Include=" /// &lt;summary&gt;Zur Uebersetzungszeit eingebettete Build-Daten.&lt;/summary&gt;" />
<BuildInfoLine Include=" public static class $(DeploymentcenterBuildInfoClass)" />
<BuildInfoLine Include=" {" />
<BuildInfoLine Include=" static BuildInfo()" />
<BuildInfoLine Include=" public const string Version = &quot;$(BuildVersion)&quot;%3B" />
<BuildInfoLine Include=" public const string GitCommit = &quot;$(DcGitCommitLong)&quot;%3B" />
<BuildInfoLine Include=" public const string GitCommitShort = &quot;$(DcGitCommitShort)&quot;%3B" />
<BuildInfoLine Include=" public const string BuildDateUtc = &quot;$(BuildDateUtc)&quot;%3B" />
<BuildInfoLine Include=" public const string Channel = &quot;$(BuildChannel)&quot;%3B" />
<!-- Include darf nicht leer sein, daher ein Leerzeichen statt einer Leerzeile. -->
<BuildInfoLine Include=" " />
<BuildInfoLine Include=" public static string Summary" />
<BuildInfoLine Include=" {" />
<BuildInfoLine Include=" Version = &quot;$(BuildVersion)&quot;%3B" />
<BuildInfoLine Include=" GitCommit = &quot;$(GitCommitLong)&quot;%3B" />
<BuildInfoLine Include=" GitCommitShort = &quot;$(GitCommitShort)&quot;%3B" />
<BuildInfoLine Include=" BuildDateUtc = &quot;$(BuildDateUtc)&quot;%3B" />
<BuildInfoLine Include=" Channel = &quot;$(BuildChannel)&quot;%3B" />
<BuildInfoLine Include=" get { return &quot;v&quot; + Version + &quot; (&quot; + GitCommitShort + &quot;) built on &quot; + BuildDateUtc + &quot; [&quot; + Channel + &quot;]&quot;%3B }" />
<BuildInfoLine Include=" }" />
<BuildInfoLine Include=" }" />
<BuildInfoLine Include="}" />
</ItemGroup>
<WriteLinesToFile File="$(BuildInfoFile)" Lines="@(BuildInfoLine)" Overwrite="true" WriteOnlyWhenDifferent="true" />
<WriteLinesToFile File="$(BuildInfoFile)"
Lines="@(BuildInfoLine)"
Overwrite="true"
WriteOnlyWhenDifferent="true" />
<ItemGroup>
<Compile Include="$(BuildInfoFile)" />
<FileWrites Include="$(BuildInfoFile)" />
</ItemGroup>
</Target>
</Project>