527 lines
19 KiB
PHP
527 lines
19 KiB
PHP
<?php
|
||
|
||
|
||
namespace App\Livewire\Ui\System;
|
||
|
||
use Illuminate\Support\Facades\Cache;
|
||
use Illuminate\Support\Str;
|
||
use Livewire\Component;
|
||
|
||
class UpdateCard extends Component
|
||
{
|
||
/* ===== Versionen / Status ===== */
|
||
public ?string $current = null; // normiert (ohne v)
|
||
public ?string $latest = null; // normiert (ohne v)
|
||
public ?string $displayCurrent = null; // hübsch mit v
|
||
public ?string $displayLatest = null; // hübsch mit v
|
||
public bool $hasUpdate = false;
|
||
|
||
public string $state = 'idle'; // idle | running
|
||
public bool $running = false; // low-level Wrapper-Status
|
||
public ?int $rc = null; // Rückgabecode vom Wrapper
|
||
|
||
/* ===== UI-Properties (nur fürs Blade) ===== */
|
||
public string $badgeText = '';
|
||
public string $badgeClass = '';
|
||
public string $badgeIcon = '';
|
||
public bool $showButton = false;
|
||
public bool $buttonDisabled = false;
|
||
public string $buttonLabel = '';
|
||
public ?string $progressLine = null; // Einzeilige Live-Statuszeile
|
||
public ?string $errorLine = null; // Fehlerzeile bei rc != 0
|
||
|
||
public ?string $message = null;
|
||
/* ===== Intern ===== */
|
||
public bool $postActionsDone = false;
|
||
protected string $cacheStartedAtKey = 'mw.update.started_at';
|
||
protected int $failsafeSeconds = 20 * 60;
|
||
|
||
private const VERSION_FILE = '/var/lib/mailwolt/version'; // normiert
|
||
private const VERSION_FILE_RAW = '/var/lib/mailwolt/version_raw'; // raw (z.B. v1.0.35-dirty)
|
||
private const BUILD_INFO = '/etc/mailwolt/build.info'; // Fallback
|
||
private const UPDATE_LOG = '/var/log/mailwolt-update.log'; // Wrapper-Log
|
||
|
||
/* ========================================================= */
|
||
|
||
public function mount(): void
|
||
{
|
||
$this->reloadVersionsAndStatus();
|
||
$this->recompute();
|
||
$this->progressLine = $this->tailUpdateLog();
|
||
|
||
if ($this->running) {
|
||
$this->state = 'running';
|
||
}
|
||
|
||
$this->recomputeUi();
|
||
}
|
||
|
||
public function render()
|
||
{
|
||
return view('livewire.ui.system.update-card');
|
||
}
|
||
|
||
/* ================== Aktionen ================== */
|
||
|
||
public function runUpdate(): void
|
||
{
|
||
// evtl. alte Einträge aufräumen
|
||
Cache::forget('mailwolt.update_available');
|
||
Cache::put($this->cacheStartedAtKey, time(), now()->addHour());
|
||
|
||
// Wrapper starten (setzt /var/lib/mailwolt/update/{state,rc} und schreibt Versionen)
|
||
@shell_exec('nohup sudo -n /usr/local/sbin/mailwolt-update >/dev/null 2>&1 &');
|
||
|
||
// Sofort ins Running gehen
|
||
$this->latest = null;
|
||
$this->displayLatest = null;
|
||
$this->hasUpdate = false;
|
||
$this->state = 'running';
|
||
$this->running = true;
|
||
$this->errorLine = null;
|
||
$this->progressLine = 'Update gestartet …';
|
||
|
||
$this->recomputeUi();
|
||
}
|
||
|
||
public function pollUpdate(): void
|
||
{
|
||
// 1) aktuellen Wrapper-Status einlesen
|
||
$this->refreshLowLevelState();
|
||
|
||
// 2) Failsafe
|
||
$started = (int)Cache::get($this->cacheStartedAtKey, 0);
|
||
if ($this->running && $started > 0 && (time() - $started) > $this->failsafeSeconds) {
|
||
$this->running = false;
|
||
$this->rc ??= 0; // wenn unklar, als erfolgreich werten
|
||
}
|
||
|
||
// 3) Abschluss?
|
||
if (!$this->running) {
|
||
Cache::forget($this->cacheStartedAtKey);
|
||
|
||
// Nachlauf: Versionen & Vergleich neu aufbauen
|
||
$this->reloadVersionsAndStatus();
|
||
$this->recompute();
|
||
|
||
if ($this->rc === 0 && !$this->postActionsDone) {
|
||
// Dienste neu starten (asynchron)
|
||
@shell_exec('nohup php /var/www/mailwolt/artisan mailwolt:restart-services >/dev/null 2>&1 &');
|
||
$this->postActionsDone = true;
|
||
|
||
$ver = $this->displayCurrent ?? 'aktuelle Version';
|
||
$this->progressLine = '';
|
||
$this->dispatch('reload-page', delay: 5000);
|
||
$this->dispatch('toast',
|
||
type: 'success',
|
||
title: 'Update erfolgreich',
|
||
text: "MailWolt wurde auf {$ver} aktualisiert.",
|
||
badge: 'System',
|
||
duration: 4000
|
||
);
|
||
} elseif ($this->rc !== null && $this->rc !== 0 && !$this->postActionsDone) {
|
||
// Fehlerfall
|
||
$this->postActionsDone = true;
|
||
$this->errorLine = "Update fehlgeschlagen (rc={$this->rc}).";
|
||
$this->dispatch('toast',
|
||
type: 'error',
|
||
title: 'Update fehlgeschlagen',
|
||
text: $this->progressLine ?: 'Bitte Logs prüfen: /var/log/mailwolt-update.log',
|
||
badge: 'System',
|
||
duration: 0
|
||
);
|
||
}
|
||
|
||
$this->state = 'idle';
|
||
}
|
||
|
||
// UI bei jedem Poll neu ableiten
|
||
$this->recomputeUi();
|
||
}
|
||
|
||
/* ================== Helpers ================== */
|
||
|
||
protected function reloadVersionsAndStatus(): void
|
||
{
|
||
$this->current = $this->readCurrentVersion();
|
||
|
||
// Update-Checker schreibt:
|
||
// - updates:latest (normiert)
|
||
// - updates:latest_raw (original)
|
||
$latNorm = Cache::get('updates:latest');
|
||
$latRaw = Cache::get('updates:latest_raw');
|
||
|
||
// Legacy-Fallback
|
||
if (!$latNorm && ($legacy = Cache::get('mailwolt.update_available'))) {
|
||
$latNorm = $this->normalizeVersion($legacy);
|
||
$latRaw = $legacy;
|
||
}
|
||
|
||
$this->latest = $latNorm ?: null;
|
||
$this->displayLatest = $latRaw ?: ($latNorm ? 'v' . $latNorm : null);
|
||
|
||
$this->refreshLowLevelState();
|
||
}
|
||
|
||
protected function recompute(): void
|
||
{
|
||
$curNorm = $this->normalizeVersion($this->current);
|
||
$latNorm = $this->normalizeVersion($this->latest);
|
||
|
||
$this->hasUpdate = ($curNorm && $latNorm)
|
||
? version_compare($latNorm, $curNorm, '>')
|
||
: false;
|
||
|
||
$this->displayCurrent = $curNorm ? 'v' . $curNorm : null;
|
||
|
||
if (!$this->displayLatest && $latNorm) {
|
||
$this->displayLatest = 'v' . $latNorm;
|
||
}
|
||
}
|
||
|
||
protected function recomputeUi(): void
|
||
{
|
||
if ($this->state === 'running') {
|
||
$this->badgeText = 'Update läuft';
|
||
$this->badgeIcon = 'ph-arrows-clockwise animate-spin';
|
||
$this->badgeClass = 'text-sky-200 bg-sky-500/10 border-sky-400/30';
|
||
$this->showButton = true;
|
||
$this->buttonDisabled = true;
|
||
$this->buttonLabel = 'Update läuft …';
|
||
return;
|
||
}
|
||
|
||
if ($this->rc !== null && $this->rc !== 0) {
|
||
$this->badgeText = 'Fehlgeschlagen';
|
||
$this->badgeIcon = 'ph-warning-circle';
|
||
$this->badgeClass = 'text-rose-200 bg-rose-500/10 border-rose-400/30';
|
||
$this->showButton = true;
|
||
$this->buttonDisabled = false;
|
||
$this->buttonLabel = 'Erneut versuchen';
|
||
return;
|
||
}
|
||
|
||
if ($this->hasUpdate) {
|
||
$this->badgeText = 'Update verfügbar';
|
||
$this->badgeIcon = 'ph-arrow-fat-line-up';
|
||
$this->badgeClass = 'text-yellow-200 bg-yellow-500/10 border-yellow-400/30';
|
||
$this->showButton = true;
|
||
$this->buttonDisabled = false;
|
||
$this->buttonLabel = 'Jetzt aktualisieren';
|
||
return;
|
||
}
|
||
|
||
// Aktuell
|
||
$this->badgeText = 'Aktuell';
|
||
$this->badgeIcon = 'ph-check-circle';
|
||
$this->badgeClass = 'text-emerald-200 bg-emerald-500/10 border-emerald-400/30';
|
||
$this->showButton = false;
|
||
$this->buttonDisabled = true;
|
||
$this->buttonLabel = '';
|
||
}
|
||
|
||
protected function finishUiIfNoUpdate(): void
|
||
{
|
||
if (!$this->hasUpdate) {
|
||
$this->state = 'idle';
|
||
Cache::forget('mailwolt.update_available'); // Legacy-Key
|
||
}
|
||
|
||
}
|
||
|
||
protected function refreshLowLevelState(): void
|
||
{
|
||
$state = @trim(@file_get_contents('/var/lib/mailwolt/update/state') ?: '');
|
||
$this->running = ($state === 'running');
|
||
|
||
$rcRaw = @trim(@file_get_contents('/var/lib/mailwolt/update/rc') ?: '');
|
||
$this->rc = is_numeric($rcRaw) ? (int)$rcRaw : null;
|
||
|
||
$this->progressLine = $this->tailUpdateLog();
|
||
}
|
||
|
||
protected function readCurrentVersion(): ?string
|
||
{
|
||
// 1) normierte Version vom Wrapper
|
||
$v = @trim(@file_get_contents(self::VERSION_FILE) ?: '');
|
||
if ($v !== '') return $v;
|
||
|
||
// 2) raw -> normieren
|
||
$raw = @trim(@file_get_contents(self::VERSION_FILE_RAW) ?: '');
|
||
if ($raw !== '') return $this->normalizeVersion($raw);
|
||
|
||
// 3) build.info
|
||
$build = @file_get_contents(self::BUILD_INFO);
|
||
if ($build) {
|
||
foreach (preg_split('/\R+/', $build) as $line) {
|
||
if (str_starts_with($line, 'version=')) {
|
||
$v = $this->normalizeVersion(trim(substr($line, 8)));
|
||
if ($v) return $v;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 4) Fallback auf config(app.version)
|
||
$v = $this->normalizeVersion(config('app.version') ?: '');
|
||
return $v ?: null;
|
||
}
|
||
|
||
protected function normalizeVersion(?string $v): ?string
|
||
{
|
||
if ($v === null) return null;
|
||
$v = trim($v);
|
||
if ($v === '') return null;
|
||
|
||
$v = ltrim($v, "vV \t\n\r\0\x0B"); // führendes v weg
|
||
$v = preg_replace('/-.*$/', '', $v); // Build-/dirty-Suffix kappen
|
||
return $v !== '' ? $v : null;
|
||
}
|
||
|
||
/** Einzeilige letzte Log-Zeile hübsch aufbereitet */
|
||
protected function tailUpdateLog(): ?string
|
||
{
|
||
$p = self::UPDATE_LOG;
|
||
if (!is_readable($p)) return null;
|
||
|
||
$lines = @file($p, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||
if (!$lines || !count($lines)) return null;
|
||
|
||
$last = trim(end($lines));
|
||
|
||
// Kosmetik
|
||
$last = preg_replace('/^\[\w\]\s*/', '', $last); // "[i] " etc.
|
||
$last = preg_replace('/^=+ .*? =+\s*$/', 'Update beendet', $last); // Banner zusammenfassen
|
||
$last = preg_replace('/^\d{4}-\d{2}-\d{2}T[^ ]+\s*::\s*/', '', $last); // "2025-.. :: " entfernen
|
||
|
||
return Str::limit($last, 120);
|
||
}
|
||
}
|
||
|
||
//namespace App\Livewire\Ui\System;
|
||
//
|
||
//use Illuminate\Support\Facades\Artisan;
|
||
//use Illuminate\Support\Facades\Cache;
|
||
//use Livewire\Component;
|
||
//
|
||
//class UpdateCard extends Component
|
||
//{
|
||
// public ?string $current = null;
|
||
// public ?string $latest = null;
|
||
// public ?string $displayCurrent = null;
|
||
// public ?string $displayLatest = null;
|
||
//
|
||
// public bool $hasUpdate = false;
|
||
// public string $state = 'idle';
|
||
// public ?string $message = null;
|
||
// public ?bool $messagePositive = null;
|
||
//
|
||
// public bool $running = false;
|
||
// public ?int $rc = null;
|
||
//
|
||
// // NEU: UI-Properties (nur fürs Blade ausgeben)
|
||
// public string $badgeText = '';
|
||
// public string $badgeClass = '';
|
||
// public string $badgeIcon = '';
|
||
// public bool $showButton = false;
|
||
// public bool $buttonDisabled = false;
|
||
// public string $buttonLabel = '';
|
||
// public ?string $progressLine = null; // Einzeiler unter der Version
|
||
// public ?string $errorLine = null; // optionaler Fehlertext
|
||
//
|
||
// public bool $postActionsDone = false;
|
||
//
|
||
// protected string $cacheStartedAtKey = 'mw.update.started_at';
|
||
// protected int $failsafeSeconds = 20 * 60;
|
||
// private const VERSION_FILE = '/var/lib/mailwolt/version'; // normiert, ohne "v"
|
||
// private const VERSION_FILE_RAW = '/var/lib/mailwolt/version_raw'; // original ("v1.0.25" o.ä.)
|
||
// private const BUILD_INFO = '/etc/mailwolt/build.info'; // Fallback
|
||
//
|
||
// public function mount(): void
|
||
// {
|
||
// $this->reloadVersionsAndStatus();
|
||
// $this->recompute();
|
||
//
|
||
// // falls ein Update bereits läuft (State-Datei existiert), gleich “running” zeigen
|
||
// if ($this->running) {
|
||
// $this->state = 'running';
|
||
// }
|
||
// }
|
||
//
|
||
// protected function reloadVersionsAndStatus(): void
|
||
// {
|
||
// $this->current = $this->readCurrentVersion();
|
||
//
|
||
// // Update-Checker soll beide Keys schreiben:
|
||
// // - updates:latest (normiert, ohne v)
|
||
// // - updates:latest_raw (Original)
|
||
// $latNorm = Cache::get('updates:latest');
|
||
// $latRaw = Cache::get('updates:latest_raw');
|
||
//
|
||
// // Falls dein alter Key noch benutzt wird, weiter kompatibel:
|
||
// if (!$latNorm && ($legacy = Cache::get('mailwolt.update_available'))) {
|
||
// $latNorm = $this->normalizeVersion($legacy);
|
||
// $latRaw = $legacy;
|
||
// }
|
||
//
|
||
// $this->latest = $latNorm ?: null; // für Vergleiche
|
||
// $this->displayLatest = $latRaw ?: ($latNorm ? 'v'.$latNorm : null);
|
||
//
|
||
// $this->refreshLowLevelState();
|
||
// }
|
||
//
|
||
// protected function recompute(): void
|
||
// {
|
||
// $curNorm = $this->normalizeVersion($this->current);
|
||
// $latNorm = $this->normalizeVersion($this->latest);
|
||
//
|
||
// $this->hasUpdate = ($curNorm && $latNorm)
|
||
// ? version_compare($latNorm, $curNorm, '>')
|
||
// : false;
|
||
//
|
||
// // Anzeige immer hübsch mit "v", Vergleich bleibt normiert
|
||
// $this->displayCurrent = $curNorm ? 'v'.$curNorm : null;
|
||
//
|
||
// // displayLatest NICHT mehr vom hasUpdate abhängig machen – bleibt informativ sichtbar
|
||
// if (!$this->displayLatest && $latNorm) {
|
||
// $this->displayLatest = 'v'.$latNorm;
|
||
// }
|
||
// }
|
||
//
|
||
// protected function finishUiIfNoUpdate(): void
|
||
// {
|
||
// if (!$this->hasUpdate) {
|
||
// $this->state = 'idle';
|
||
// $cur = $this->displayCurrent ?? '–';
|
||
// $this->message = "Du bist auf dem neuesten Stand ({$cur})";
|
||
// $this->messagePositive = true;
|
||
//
|
||
// // Nur den alten Legacy-Key aufräumen
|
||
// Cache::forget('mailwolt.update_available');
|
||
// }
|
||
// }
|
||
//
|
||
// protected function readCurrentVersion(): ?string
|
||
// {
|
||
// // 1) Wrapper-Datei (normiert, ohne „v“)
|
||
// $v = @trim(@file_get_contents(self::VERSION_FILE) ?: '');
|
||
// if ($v !== '') return $v;
|
||
//
|
||
// // 2) Raw -> normieren
|
||
// $raw = @trim(@file_get_contents(self::VERSION_FILE_RAW) ?: '');
|
||
// if ($raw !== '') return $this->normalizeVersion($raw);
|
||
//
|
||
// // 3) Fallback build.info (format: "version=…")
|
||
// $build = @file_get_contents(self::BUILD_INFO);
|
||
// if ($build) {
|
||
// foreach (preg_split('/\R+/', $build) as $line) {
|
||
// if (str_starts_with($line, 'version=')) {
|
||
// $v = $this->normalizeVersion(trim(substr($line, 8)));
|
||
// if ($v) return $v;
|
||
// }
|
||
// }
|
||
// }
|
||
//
|
||
// // 4) Noch ein letzter Fallback
|
||
// $v = $this->normalizeVersion(config('app.version') ?: '');
|
||
// return $v ?: null;
|
||
// }
|
||
//
|
||
// protected function normalizeVersion(?string $v): ?string
|
||
// {
|
||
// if ($v === null) return null;
|
||
// $v = trim($v);
|
||
// if ($v === '') return null;
|
||
//
|
||
// // führendes v entfernen, Build-Suffixe (z.B. "-3-gabcd" oder "-dirty") kappen
|
||
// $v = ltrim($v, "vV \t\n\r\0\x0B");
|
||
// $v = preg_replace('/-.*$/', '', $v); // alles nach erster '-' weg
|
||
// return $v !== '' ? $v : null;
|
||
// }
|
||
//
|
||
// public function runUpdate(): void
|
||
// {
|
||
// Cache::forget('mailwolt.update_available');
|
||
// Cache::put($this->cacheStartedAtKey, time(), now()->addHour());
|
||
//
|
||
// // Name korrigiert: Wrapper ist /usr/local/sbin/mailwolt-update
|
||
// @shell_exec('nohup sudo -n /usr/local/sbin/mailwolt-update >/dev/null 2>&1 &');
|
||
//
|
||
// $this->latest = null;
|
||
// $this->displayLatest = null;
|
||
// $this->hasUpdate = false;
|
||
// $this->state = 'running';
|
||
// $this->running = true;
|
||
// $this->message = 'Update läuft …';
|
||
// }
|
||
//
|
||
// protected function refreshLowLevelState(): void
|
||
// {
|
||
// // Wrapper-Status lesen
|
||
// $state = @trim(@file_get_contents('/var/lib/mailwolt/update/state') ?: '');
|
||
//
|
||
// // Läuft, wenn Datei "running" enthält – alles andere gilt als nicht laufend
|
||
// $this->running = ($state === 'running');
|
||
//
|
||
// // Rückgabecode (nur gesetzt, wenn Update beendet ist)
|
||
// $rcRaw = @trim(@file_get_contents('/var/lib/mailwolt/update/rc') ?: '');
|
||
// $this->rc = is_numeric($rcRaw) ? (int)$rcRaw : null;
|
||
// }
|
||
//
|
||
// public function pollUpdate(): void
|
||
// {
|
||
// // 1) Low-Level Status vom Wrapper (/var/lib/mailwolt/update/{state,rc})
|
||
// $this->refreshLowLevelState();
|
||
//
|
||
// // 2) failsafe: wenn zu lange läuft, als beendet markieren
|
||
// $started = (int) Cache::get($this->cacheStartedAtKey, 0);
|
||
// if ($this->running && $started > 0 && (time() - $started) > $this->failsafeSeconds) {
|
||
// $this->running = false;
|
||
// $this->rc ??= 0; // notfalls als “erfolgreich” werten
|
||
// }
|
||
//
|
||
// // 3) Wenn nicht mehr running ⇒ Abschluss behandeln
|
||
// if (!$this->running) {
|
||
// Cache::forget($this->cacheStartedAtKey);
|
||
//
|
||
// // Versionen neu laden und “hasUpdate” neu berechnen
|
||
// $this->reloadVersionsAndStatus();
|
||
// $this->recompute();
|
||
//
|
||
// // 3a) Erfolg: jetzt erst Dienste neu starten
|
||
// if ($this->rc === 0 && !$this->postActionsDone) {
|
||
// // Restart asynchron starten (dein Artisan-Command macht systemctl etc.)
|
||
// @shell_exec('nohup php /var/www/mailwolt/artisan mailwolt:restart-services >/dev/null 2>&1 &');
|
||
// $this->postActionsDone = true;
|
||
//
|
||
// // Toast
|
||
// $ver = $this->displayCurrent ?? 'aktuelle Version';
|
||
// $this->dispatch('toast',
|
||
// type: 'success',
|
||
// title: 'Update erfolgreich',
|
||
// text: "MailWolt wurde auf {$ver} aktualisiert. Dienste werden neu gestartet …",
|
||
// badge: 'System',
|
||
// duration: 5000
|
||
// );
|
||
//
|
||
// // Seite nach 5s neu laden
|
||
// $this->dispatch('reload-page', delay: 5000);
|
||
// }
|
||
// // 3b) Fehler: Meldung zeigen
|
||
// elseif ($this->rc !== null && $this->rc !== 0 && !$this->postActionsDone) {
|
||
// $this->postActionsDone = true;
|
||
// $this->dispatch('toast',
|
||
// type: 'error',
|
||
// title: 'Update fehlgeschlagen',
|
||
// text: "Update ist mit Rückgabecode {$this->rc} fehlgeschlagen. Bitte Logs prüfen.",
|
||
// badge: 'System',
|
||
// duration: 0
|
||
// );
|
||
// }
|
||
//
|
||
// // 4) UI zurück auf idle
|
||
// $this->state = 'idle';
|
||
// }
|
||
// }
|
||
//
|
||
//}
|