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
@@ -0,0 +1,14 @@
|
||||
# Kommandozeilenskripte - kein Webzugriff.
|
||||
#
|
||||
# Zweite Verteidigungslinie. Die erste ist die SAPI-Pruefung in den Skripten
|
||||
# selbst: sie wirkt auch dann, wenn mod_rewrite, AllowOverride oder diese
|
||||
# Datei nicht greifen.
|
||||
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all denied
|
||||
</IfModule>
|
||||
|
||||
<IfModule !mod_authz_core.c>
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</IfModule>
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Watchdog-Tick — Evaluationslauf von der Kommandozeile.
|
||||
*
|
||||
* Ersetzt den frueheren /watchdog/cli/tick.php. Macht dasselbe wie
|
||||
* GET /api/watchdog/v1/evaluate, aber ohne HTTP-Umweg:
|
||||
*
|
||||
* - kein Shared Key im Crontab und damit nicht in der Prozessliste
|
||||
* jedes Nutzers auf der Maschine (ps zeigt curl-Argumente),
|
||||
* - keine Abhaengigkeit von Webserver, TLS, DNS oder Timeouts,
|
||||
* - laeuft auch, wenn der Webserver gerade nicht erreichbar ist -
|
||||
* also genau dann, wenn das Erkennen von Ausfaellen wichtig ist.
|
||||
*
|
||||
* Cron-Eintrag (minuetlich):
|
||||
* * * * * * /usr/bin/php /pfad/zum/deploymentcenter/cli/tick.php --quiet
|
||||
*
|
||||
* Aufrufe:
|
||||
* php cli/tick.php Lauf mit lesbarer Zusammenfassung
|
||||
* php cli/tick.php --quiet nur bei Zustandswechseln und Fehlern Ausgabe
|
||||
* php cli/tick.php --json maschinenlesbares Ergebnis
|
||||
* php cli/tick.php --no-lock Sperre uebergehen (nur zur Fehlersuche)
|
||||
* php cli/tick.php --help
|
||||
*
|
||||
* Rueckgabewerte:
|
||||
* 0 Lauf erfolgreich
|
||||
* 1 Fehler
|
||||
* 2 uebersprungen, weil bereits ein Lauf aktiv ist
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Deploymentcenter\Core\Db;
|
||||
use Deploymentcenter\Core\Logger;
|
||||
use Deploymentcenter\Modules\Watchdog\Evaluator;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Nur Kommandozeile.
|
||||
//
|
||||
// Erste und wichtigste Verteidigungslinie: sie wirkt unabhaengig davon, ob
|
||||
// mod_rewrite, AllowOverride oder die .htaccess-Dateien greifen. Ohne sie
|
||||
// koennte ein Aufruf ueber den Webserver einen Evaluationslauf ohne jede
|
||||
// Authentifizierung ausloesen - der HTTP-Endpunkt verlangt dafuer aus gutem
|
||||
// Grund den Shared Key oder eine angemeldete Sitzung.
|
||||
// ---------------------------------------------------------------------------
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(403);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
exit("Dieses Skript laeuft ausschliesslich auf der Kommandozeile.\n");
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../src/bootstrap.php';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fehlerbehandlung fuer die Kommandozeile.
|
||||
//
|
||||
// Der Handler aus bootstrap.php ruft Http::fail() auf: der schickt JSON samt
|
||||
// Kopfzeilen und ist fuer eine HTTP-Antwort gedacht. Im Cron waere das eine
|
||||
// unbrauchbare Mail und - schlimmer - der Rueckgabewert 0, sodass ein
|
||||
// gescheiterter Lauf unbemerkt bliebe.
|
||||
// ---------------------------------------------------------------------------
|
||||
set_exception_handler(static function (\Throwable $e): void {
|
||||
Logger::error('Watchdog-Tick abgebrochen', [
|
||||
'error' => $e->getMessage(),
|
||||
'file' => $e->getFile() . ':' . $e->getLine(),
|
||||
]);
|
||||
|
||||
fwrite(STDERR, sprintf(
|
||||
"[FEHLER] %s: %s\n %s:%d\n",
|
||||
get_class($e),
|
||||
$e->getMessage(),
|
||||
$e->getFile(),
|
||||
$e->getLine()
|
||||
));
|
||||
|
||||
exit(1);
|
||||
});
|
||||
|
||||
$options = parseArguments($argv);
|
||||
|
||||
if ($options['help']) {
|
||||
printUsage();
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ueberlappende Laeufe verhindern.
|
||||
//
|
||||
// Bei minuetlichem Cron und einem Lauf, der laenger als eine Minute braucht -
|
||||
// etwa weil die Datenbank klemmt oder viele Webhooks zugestellt werden -
|
||||
// starten sonst mehrere Evaluatoren gleichzeitig. Die wuerden dieselben
|
||||
// Zustandswechsel doppelt protokollieren und doppelt alarmieren.
|
||||
// ---------------------------------------------------------------------------
|
||||
$lockHandle = null;
|
||||
|
||||
if (!$options['no-lock']) {
|
||||
$lockFile = DC_VAR . '/watchdog-tick.lock';
|
||||
|
||||
if (!is_dir(DC_VAR)) {
|
||||
@mkdir(DC_VAR, 0775, true);
|
||||
}
|
||||
|
||||
$lockHandle = @fopen($lockFile, 'c');
|
||||
|
||||
if ($lockHandle === false) {
|
||||
fwrite(STDERR, "[FEHLER] Sperrdatei nicht beschreibbar: {$lockFile}\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (!flock($lockHandle, LOCK_EX | LOCK_NB)) {
|
||||
// Kein Fehler, sondern der Normalfall bei einem langsamen Vorlauf.
|
||||
if (!$options['quiet']) {
|
||||
fwrite(STDERR, "[UEBERSPRUNGEN] Es laeuft bereits ein Tick.\n");
|
||||
}
|
||||
exit(2);
|
||||
}
|
||||
|
||||
// Der Rueckgabewert von ftruncate ist hier ohne Belang: die Datei traegt
|
||||
// nur die PID zur Fehlersuche, ihr Inhalt steuert nichts.
|
||||
ftruncate($lockHandle, 0);
|
||||
fwrite($lockHandle, (string)getmypid());
|
||||
fflush($lockHandle);
|
||||
}
|
||||
|
||||
// Kein try/finally um den Lauf: exit() fuehrt in PHP keine finally-Bloecke aus,
|
||||
// die Freigabe waere dort also nur zum Schein notiert. Der Handler oben beendet
|
||||
// den Prozess bei einer Exception - dabei gibt das Betriebssystem die Sperre
|
||||
// ohnehin frei. Fuer den regulaeren Weg steht die Freigabe am Ende.
|
||||
$startedAt = microtime(true);
|
||||
$db = Db::init();
|
||||
$result = Evaluator::run($db);
|
||||
$wallMs = (int)round((microtime(true) - $startedAt) * 1000);
|
||||
|
||||
if ($options['json']) {
|
||||
echo json_encode(
|
||||
$result + ['wall_ms' => $wallMs],
|
||||
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
|
||||
), PHP_EOL;
|
||||
} elseif (!($options['quiet'] && $result['changed'] === 0)) {
|
||||
// Im Cron soll nur etwas ankommen, wenn es etwas zu berichten gibt -
|
||||
// sonst erzeugt ein minuetlicher Job 1440 Mails am Tag.
|
||||
printSummary($result, $wallMs);
|
||||
}
|
||||
|
||||
releaseLock($lockHandle);
|
||||
exit(0);
|
||||
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* @param list<string> $argv
|
||||
* @return array{help:bool,json:bool,quiet:bool,no-lock:bool}
|
||||
*/
|
||||
function parseArguments(array $argv): array
|
||||
{
|
||||
$options = ['help' => false, 'json' => false, 'quiet' => false, 'no-lock' => false];
|
||||
|
||||
foreach (array_slice($argv, 1) as $arg) {
|
||||
switch ($arg) {
|
||||
case '-h':
|
||||
case '--help':
|
||||
$options['help'] = true;
|
||||
break;
|
||||
case '-j':
|
||||
case '--json':
|
||||
$options['json'] = true;
|
||||
break;
|
||||
case '-q':
|
||||
case '--quiet':
|
||||
$options['quiet'] = true;
|
||||
break;
|
||||
case '--no-lock':
|
||||
$options['no-lock'] = true;
|
||||
break;
|
||||
default:
|
||||
fwrite(STDERR, "Unbekannte Option: {$arg}\n\n");
|
||||
printUsage();
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Sperre frei. Ohne aktive Sperre ein No-Op.
|
||||
*
|
||||
* @param resource|null $handle
|
||||
*/
|
||||
function releaseLock($handle): void
|
||||
{
|
||||
if ($handle === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
flock($handle, LOCK_UN);
|
||||
fclose($handle);
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $result */
|
||||
function printSummary(array $result, int $wallMs): void
|
||||
{
|
||||
printf(
|
||||
"Watchdog-Tick: %d Monitor(e) geprueft, %d Zustandswechsel in %d ms (gesamt %d ms).\n",
|
||||
$result['checked'],
|
||||
$result['changed'],
|
||||
$result['duration_ms'],
|
||||
$wallMs
|
||||
);
|
||||
|
||||
foreach ($result['changes'] as $change) {
|
||||
printf(
|
||||
" %-28s %s -> %-8s %s%s\n",
|
||||
$change['source'],
|
||||
$change['from'],
|
||||
$change['to'],
|
||||
$change['reason'],
|
||||
$change['suppressed'] !== null
|
||||
? sprintf(' [Alarm unterdrueckt, %s ist selbst unten]', $change['suppressed'])
|
||||
: ''
|
||||
);
|
||||
}
|
||||
|
||||
if (($result['released_leases'] ?? 0) > 0) {
|
||||
printf(" %d abgelaufene Bugtracker-Lease(s) freigegeben.\n", $result['released_leases']);
|
||||
}
|
||||
|
||||
if (($result['purged_metrics'] ?? 0) > 0) {
|
||||
printf(" %d alte Metrikwerte abgeraeumt.\n", $result['purged_metrics']);
|
||||
}
|
||||
}
|
||||
|
||||
function printUsage(): void
|
||||
{
|
||||
echo <<<TXT
|
||||
Watchdog-Tick - Evaluationslauf von der Kommandozeile.
|
||||
|
||||
Stuft Monitore anhand ihres erwarteten Intervalls auf warning bzw. down,
|
||||
protokolliert Zustandswechsel, loest Webhooks aus, gibt abgelaufene
|
||||
Bugtracker-Leases frei und raeumt stuendlich den Metrik-Verlauf ab.
|
||||
|
||||
Aufruf:
|
||||
php cli/tick.php [Optionen]
|
||||
|
||||
Optionen:
|
||||
-q, --quiet Ausgabe nur bei Zustandswechseln (fuer Cron)
|
||||
-j, --json Ergebnis als JSON
|
||||
--no-lock Sperre gegen ueberlappende Laeufe uebergehen
|
||||
-h, --help Diese Hilfe
|
||||
|
||||
Rueckgabewerte:
|
||||
0 erfolgreich
|
||||
1 Fehler
|
||||
2 uebersprungen, es laeuft bereits ein Tick
|
||||
|
||||
Cron (minuetlich):
|
||||
* * * * * /usr/bin/php /pfad/zum/deploymentcenter/cli/tick.php --quiet
|
||||
|
||||
TXT;
|
||||
}
|
||||
Reference in New Issue
Block a user