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
+83
-6
@@ -32,6 +32,7 @@ use Deploymentcenter\Modules\Bugtracker\BugRepo;
|
||||
use Deploymentcenter\Modules\Bugtracker\IgnoreRules;
|
||||
use Deploymentcenter\Modules\License\Audit;
|
||||
use Deploymentcenter\Modules\License\KeyGen;
|
||||
use Deploymentcenter\Modules\UpdateService\ReleaseSigner;
|
||||
use Deploymentcenter\Modules\UpdateService\UpdateManager;
|
||||
use Deploymentcenter\Modules\Watchdog\Evaluator;
|
||||
use Deploymentcenter\Modules\Watchdog\EventLog;
|
||||
@@ -716,25 +717,49 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
|
||||
$updateManager = new UpdateManager($pdo);
|
||||
|
||||
$channel = trim((string)($_POST['channel'] ?? 'prod'));
|
||||
$platform = UpdateManager::normalizePlatform((string)($_POST['platform'] ?? ''));
|
||||
$size = (int)($_POST['size_bytes'] ?? 0);
|
||||
|
||||
// Auch der Weg ueber die Oberflaeche wird signiert, sonst
|
||||
// haetten von Hand eingetragene Releases keine Vertrauenskette.
|
||||
$signature = ReleaseSigner::sign(ReleaseSigner::canonical(
|
||||
$productSlug,
|
||||
$version,
|
||||
$channel,
|
||||
$platform,
|
||||
$hash !== '' ? $hash : null,
|
||||
$url,
|
||||
$size
|
||||
));
|
||||
|
||||
$result = $updateManager->addRelease(
|
||||
$productSlug,
|
||||
$version,
|
||||
trim((string)($_POST['channel'] ?? 'prod')),
|
||||
$channel,
|
||||
trim((string)($_POST['release_notes'] ?? '')),
|
||||
$url,
|
||||
$hash !== '' ? $hash : null,
|
||||
trim((string)($_POST['git_commit'] ?? '')),
|
||||
(int)($_POST['size_bytes'] ?? 0),
|
||||
$size,
|
||||
null,
|
||||
isset($_POST['is_critical']),
|
||||
$actor
|
||||
$actor,
|
||||
$platform,
|
||||
$signature
|
||||
);
|
||||
|
||||
dc_flash(sprintf(
|
||||
'Release %s fuer "%s" %s.%s',
|
||||
'Release %s (%s, %s) fuer "%s" %s.%s%s',
|
||||
e($version),
|
||||
e($channel),
|
||||
e($platform),
|
||||
e($productSlug),
|
||||
$result['created'] ? 'veroeffentlicht' : 'aktualisiert',
|
||||
$signature === null
|
||||
? ' Hinweis: unsigniert, kein Signierschluessel hinterlegt.'
|
||||
: '',
|
||||
$result['auto_resolved'] > 0
|
||||
? sprintf(' %d Bugtracker-Item(s) automatisch geschlossen.', $result['auto_resolved'])
|
||||
: ''
|
||||
@@ -1654,7 +1679,18 @@ $csrfField = Csrf::field();
|
||||
Der Evaluator lief noch nie.
|
||||
<?php endif; ?>
|
||||
<div style="font-size:0.78rem; margin-top:0.5rem; font-family:'Roboto Mono',monospace; opacity:0.85;">
|
||||
Cron einrichten: * * * * * curl -fsS -H "Authorization: Bearer <SHARED_KEY>" <?= e($baseUrl) ?>/api/watchdog/v1/evaluate > /dev/null
|
||||
<!--
|
||||
Der interne Aufruf steht zuerst: er braucht keinen Schluessel
|
||||
im Crontab - der taucht sonst in der Prozessliste jedes
|
||||
Nutzers auf - und laeuft auch dann, wenn der Webserver
|
||||
gerade nicht erreichbar ist.
|
||||
-->
|
||||
Empfohlen (intern):<br>
|
||||
* * * * * /usr/bin/php <?= e(DC_ROOT) ?>/cli/tick.php --quiet
|
||||
<div style="margin-top:0.45rem; opacity:0.75;">
|
||||
Alternativ über HTTP:<br>
|
||||
* * * * * curl -fsS -H "Authorization: Bearer <SHARED_KEY>" <?= e($baseUrl) ?>/api/watchdog/v1/evaluate > /dev/null
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" action="index.php#tab-watchdog" style="flex-shrink:0;">
|
||||
@@ -1698,6 +1734,7 @@ $csrfField = Csrf::field();
|
||||
<th>Hierarchie / Entity Source</th>
|
||||
<th>Typ</th>
|
||||
<th>Status</th>
|
||||
<th>Version</th>
|
||||
<th>Letzte Meldung</th>
|
||||
<th>Zuletzt Gesehen</th>
|
||||
</tr>
|
||||
@@ -1727,6 +1764,13 @@ $csrfField = Csrf::field();
|
||||
● <?= strtoupper($m['state']) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (!empty($m['app_version'])): ?>
|
||||
<code><?= htmlspecialchars((string)$m['app_version']) ?></code>
|
||||
<?php else: ?>
|
||||
<span style="color:var(--text-muted);">–</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($m['last_message'] ?? '-') ?></td>
|
||||
<td><?= htmlspecialchars($m['last_seen_utc'] ?? 'Nie') ?></td>
|
||||
</tr>
|
||||
@@ -2358,6 +2402,7 @@ Antwort:
|
||||
<th>Name</th>
|
||||
<th>Typ</th>
|
||||
<th>Status</th>
|
||||
<th>Version</th>
|
||||
<th>Zuletzt gesehen</th>
|
||||
<th>Kurz-Info / Message</th>
|
||||
<th>Aktionen</th>
|
||||
@@ -2383,6 +2428,13 @@ Antwort:
|
||||
</td>
|
||||
<td><code><?= htmlspecialchars($m['type']) ?></code></td>
|
||||
<td><span class="badge badge-<?= $m['state'] === 'up' ? 'up' : 'down' ?>"><?= strtoupper($m['state']) ?></span></td>
|
||||
<td>
|
||||
<?php if (!empty($m['app_version'])): ?>
|
||||
<code><?= htmlspecialchars((string)$m['app_version']) ?></code>
|
||||
<?php else: ?>
|
||||
<span style="color:var(--text-muted);">–</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($m['last_seen_utc'] ?? 'Nie') ?></td>
|
||||
<td><span style="color:var(--text-muted); font-size:0.8rem;"><?= htmlspecialchars($m['last_message'] ?? '-') ?></span></td>
|
||||
<td style="display:flex; gap:0.35rem;">
|
||||
@@ -2566,6 +2618,7 @@ Antwort:
|
||||
<tr>
|
||||
<th>Projekt</th>
|
||||
<th>Kanal</th>
|
||||
<th>Plattform</th>
|
||||
<th>Version</th>
|
||||
<th>Git Commit</th>
|
||||
<th>Größe</th>
|
||||
@@ -2588,7 +2641,15 @@ Antwort:
|
||||
<tr>
|
||||
<td><strong><?= htmlspecialchars($r['product_slug']) ?></strong></td>
|
||||
<td><span class="badge <?= $channelClass ?>"><?= strtoupper(htmlspecialchars($r['channel'] ?? 'prod')) ?></span></td>
|
||||
<td><code>v<?= htmlspecialchars($r['version']) ?></code> <?php if (!empty($r['is_critical'])): ?><span class="badge badge-down">KRITISCH</span><?php endif; ?></td>
|
||||
<td>
|
||||
<?php $platformLabel = (string)($r['platform'] ?? 'any'); ?>
|
||||
<code style="font-size:0.75rem;<?= $platformLabel === 'any' ? 'color:var(--text-muted);' : '' ?>"><?= htmlspecialchars($platformLabel) ?></code>
|
||||
</td>
|
||||
<td>
|
||||
<code>v<?= htmlspecialchars($r['version']) ?></code>
|
||||
<?php if (!empty($r['is_critical'])): ?><span class="badge badge-down">KRITISCH</span><?php endif; ?>
|
||||
<?php if (empty($r['manifest_signature'])): ?><span class="badge badge-warning" title="Ohne Signatur - der Agent kann die Herkunft nicht prüfen.">UNSIGNIERT</span><?php endif; ?>
|
||||
</td>
|
||||
<td><code style="color:var(--text-muted);"><?= htmlspecialchars($r['git_commit'] ?? 'n/a') ?></code></td>
|
||||
<td><span style="font-family:'Roboto Mono', monospace; font-size:0.8rem;"><?= $sizeFormatted ?></span></td>
|
||||
<td><?= htmlspecialchars($r['release_notes'] ?? '-') ?></td>
|
||||
@@ -2633,6 +2694,22 @@ Antwort:
|
||||
<option value="dev">dev (Entwicklung)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Plattform</label>
|
||||
<select name="platform" class="form-input">
|
||||
<option value="any" selected>any (plattformunabhängig)</option>
|
||||
<option value="win-x64">win-x64</option>
|
||||
<option value="win-arm64">win-arm64</option>
|
||||
<option value="linux-x64">linux-x64</option>
|
||||
<option value="linux-arm64">linux-arm64</option>
|
||||
<option value="linux-musl-x64">linux-musl-x64</option>
|
||||
<option value="osx-x64">osx-x64</option>
|
||||
<option value="osx-arm64">osx-arm64</option>
|
||||
</select>
|
||||
<div style="font-size:0.7rem; color:var(--text-muted); margin-top:0.25rem;">
|
||||
Clients ohne Plattformangabe sehen ausschließlich <code>any</code>.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Version (z. B. 1.2.0)</label>
|
||||
<input type="text" name="version" class="form-input" required placeholder="1.2.0">
|
||||
|
||||
Reference in New Issue
Block a user