mailwolt/app/Livewire/Ui/System/UpdateCard.php

541 lines
19 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?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->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();
if ($this->rc !== null) {
$this->running = false;
}
// 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 &');
@shell_exec('nohup php /var/www/mailwolt/artisan settings:sync >/dev/null 2>&1 &');
@shell_exec('nohup php /var/www/mailwolt/artisan spamav:collect >/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') ?: '');
$rcRaw = @trim(@file_get_contents('/var/lib/mailwolt/update/rc') ?: '');
$this->rc = is_numeric($rcRaw) ? (int)$rcRaw : null;
$this->running = ($this->rc === null) && ($state !== 'done');
$this->progressLine = $this->tailUpdateLog();
// $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';
// }
// }
//
//}