306 lines
10 KiB
PHP
306 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Ui\System;
|
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Str;
|
|
use Livewire\Attributes\Layout;
|
|
use Livewire\Attributes\Title;
|
|
use Livewire\Component;
|
|
|
|
#[Layout('layouts.dvx')]
|
|
#[Title('Updates · Mailwolt')]
|
|
class UpdatePage extends Component
|
|
{
|
|
/* ===== Version ===== */
|
|
public ?string $current = null;
|
|
public ?string $latest = null;
|
|
public ?string $displayCurrent = null;
|
|
public ?string $displayLatest = null;
|
|
public bool $hasUpdate = false;
|
|
|
|
/* ===== Run state ===== */
|
|
public string $state = 'idle'; // idle | running
|
|
public bool $running = false;
|
|
public ?int $rc = null;
|
|
public ?string $lowState = null; // running | done | null
|
|
public array $logLines = [];
|
|
public int $progressPct = 0;
|
|
|
|
public bool $postActionsDone = false;
|
|
|
|
protected string $cacheStartedAtKey = 'mw.update.started_at';
|
|
protected int $failsafeSeconds = 20 * 60;
|
|
|
|
private const VERSION_FILE = '/var/lib/mailwolt/version';
|
|
private const VERSION_FILE_RAW = '/var/lib/mailwolt/version_raw';
|
|
private const BUILD_INFO = '/etc/mailwolt/build.info';
|
|
private const UPDATE_LOG = '/var/log/mailwolt-update.log';
|
|
private const STATE_DIR = '/var/lib/mailwolt/update';
|
|
|
|
/* ========================================================= */
|
|
|
|
public function mount(): void
|
|
{
|
|
$this->reloadVersionsAndStatus();
|
|
$this->recompute();
|
|
$this->readLogLines();
|
|
|
|
if ($this->running) {
|
|
$this->state = 'running';
|
|
}
|
|
$this->recalcProgress();
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
return view('livewire.ui.system.update-page');
|
|
}
|
|
|
|
/* ================== Aktionen ================== */
|
|
|
|
public function checkForUpdates(): void
|
|
{
|
|
@shell_exec('php ' . base_path('artisan') . ' mailwolt:check-updates 2>&1');
|
|
$this->reloadVersionsAndStatus();
|
|
$this->recompute();
|
|
|
|
if ($this->hasUpdate) {
|
|
$this->dispatch('toast', type: 'done', badge: 'Updates',
|
|
title: 'Update verfügbar',
|
|
text: "Version {$this->displayLatest} ist verfügbar.",
|
|
duration: 4000);
|
|
} else {
|
|
$this->dispatch('toast', type: 'done', badge: 'Updates',
|
|
title: 'Alles aktuell',
|
|
text: 'Es sind keine Updates verfügbar.',
|
|
duration: 3000);
|
|
}
|
|
}
|
|
|
|
public function runUpdate(): void
|
|
{
|
|
if ($this->running || $this->state === 'running') {
|
|
$this->dispatch('toast', type: 'warn', badge: 'Updates',
|
|
title: 'Läuft bereits',
|
|
text: 'Ein Update-Prozess ist bereits aktiv.',
|
|
duration: 3000);
|
|
return;
|
|
}
|
|
|
|
Cache::forget('mailwolt.update_available');
|
|
Cache::put($this->cacheStartedAtKey, time(), now()->addHour());
|
|
|
|
@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->rc = null;
|
|
$this->postActionsDone = false;
|
|
$this->logLines = ['Update gestartet …'];
|
|
$this->progressPct = 5;
|
|
}
|
|
|
|
public function pollStatus(): void
|
|
{
|
|
$this->refreshLowLevelState();
|
|
$this->readLogLines();
|
|
$this->recalcProgress();
|
|
|
|
if ($this->rc !== null) {
|
|
$this->running = false;
|
|
}
|
|
|
|
// Failsafe
|
|
$started = (int) Cache::get($this->cacheStartedAtKey, 0);
|
|
if ($this->running && $started > 0 && (time() - $started) > $this->failsafeSeconds) {
|
|
$this->running = false;
|
|
$this->lowState = 'done';
|
|
$this->rc ??= 0;
|
|
}
|
|
|
|
if ($this->lowState === 'done') {
|
|
Cache::forget($this->cacheStartedAtKey);
|
|
usleep(300_000);
|
|
|
|
$this->reloadVersionsAndStatus();
|
|
$this->recompute();
|
|
$this->readLogLines();
|
|
$this->progressPct = 100;
|
|
|
|
if ($this->rc === 0 && !$this->postActionsDone) {
|
|
@shell_exec('nohup php /var/www/mailwolt/artisan optimize:clear >/dev/null 2>&1 &');
|
|
@shell_exec('nohup php /var/www/mailwolt/artisan health:collect >/dev/null 2>&1 &');
|
|
@shell_exec('nohup php /var/www/mailwolt/artisan settings:sync >/dev/null 2>&1 &');
|
|
$this->postActionsDone = true;
|
|
|
|
$ver = $this->displayCurrent ?? 'aktuelle Version';
|
|
$this->dispatch('toast', type: 'done', badge: 'Updates',
|
|
title: 'Update abgeschlossen',
|
|
text: "Mailwolt wurde auf {$ver} aktualisiert.",
|
|
duration: 6000);
|
|
} elseif ($this->rc !== null && $this->rc !== 0 && !$this->postActionsDone) {
|
|
$this->postActionsDone = true;
|
|
$this->dispatch('toast', type: 'error', badge: 'Updates',
|
|
title: 'Update fehlgeschlagen',
|
|
text: "Rückgabecode: {$this->rc}. Bitte Log prüfen.",
|
|
duration: 0);
|
|
}
|
|
|
|
$this->state = 'idle';
|
|
}
|
|
}
|
|
|
|
public function clearLog(): void
|
|
{
|
|
// Only admins / allow via gate if you have policy; basic protection:
|
|
@file_put_contents(self::UPDATE_LOG, '');
|
|
$this->logLines = [];
|
|
$this->dispatch('toast', type: 'done', badge: 'Updates',
|
|
title: 'Log geleert', text: '', duration: 2500);
|
|
}
|
|
|
|
/* ================== Helpers ================== */
|
|
|
|
protected function reloadVersionsAndStatus(): void
|
|
{
|
|
$this->current = $this->readCurrentVersion();
|
|
|
|
$latNorm = Cache::get('updates:latest');
|
|
$latRaw = Cache::get('updates:latest_raw');
|
|
|
|
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 refreshLowLevelState(): void
|
|
{
|
|
$state = @trim(@file_get_contents(self::STATE_DIR . '/state') ?: '');
|
|
$rcRaw = @trim(@file_get_contents(self::STATE_DIR . '/rc') ?: '');
|
|
|
|
$this->lowState = $state !== '' ? $state : null;
|
|
$this->running = ($this->lowState === 'running');
|
|
$this->rc = ($this->lowState === 'done' && is_numeric($rcRaw)) ? (int) $rcRaw : null;
|
|
}
|
|
|
|
protected function readLogLines(): void
|
|
{
|
|
$p = self::UPDATE_LOG;
|
|
if (!is_readable($p)) {
|
|
$this->logLines = [];
|
|
return;
|
|
}
|
|
$lines = @file($p, FILE_IGNORE_NEW_LINES) ?: [];
|
|
$this->logLines = array_slice($lines, -100);
|
|
}
|
|
|
|
protected function recalcProgress(): void
|
|
{
|
|
if ($this->state !== 'running' && $this->lowState !== 'running') {
|
|
if ($this->lowState === 'done') {
|
|
$this->progressPct = 100;
|
|
}
|
|
return;
|
|
}
|
|
|
|
$text = implode("\n", $this->logLines);
|
|
$pct = 5;
|
|
foreach ([
|
|
'Update gestartet' => 10,
|
|
'Composer' => 25,
|
|
'npm ci' => 40,
|
|
'npm run build' => 60,
|
|
'migrate' => 75,
|
|
'optimize' => 85,
|
|
'Version aktualisiert' => 95,
|
|
'Update beendet' => 100,
|
|
] as $needle => $val) {
|
|
if (stripos($text, $needle) !== false) {
|
|
$pct = max($pct, $val);
|
|
}
|
|
}
|
|
|
|
if ($this->lowState === 'done') {
|
|
$pct = 100;
|
|
}
|
|
|
|
$this->progressPct = $pct;
|
|
}
|
|
|
|
protected function readCurrentVersion(): ?string
|
|
{
|
|
$v = @trim(@file_get_contents(self::VERSION_FILE) ?: '');
|
|
if ($v !== '') return $v;
|
|
|
|
// Fallback: git tag (lokal immer, production wenn Datei fehlt)
|
|
$tag = @trim((string) shell_exec('git -C ' . escapeshellarg(base_path()) . ' describe --tags --abbrev=0 2>/dev/null'));
|
|
$v = $this->normalizeVersion($tag);
|
|
if ($v) return $v;
|
|
|
|
$raw = @trim(@file_get_contents(self::VERSION_FILE_RAW) ?: '');
|
|
if ($raw !== '') return $this->normalizeVersion($raw);
|
|
|
|
$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;
|
|
}
|
|
}
|
|
}
|
|
|
|
$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");
|
|
$v = preg_replace('/-.*$/', '', $v);
|
|
return $v !== '' ? $v : null;
|
|
}
|
|
|
|
protected function getBuildTimestamp(): ?string
|
|
{
|
|
$build = @file_get_contents(self::BUILD_INFO);
|
|
if (!$build) return null;
|
|
foreach (preg_split('/\R+/', $build) as $line) {
|
|
if (str_starts_with($line, 'built_at=') || str_starts_with($line, 'date=')) {
|
|
$parts = explode('=', $line, 2);
|
|
return trim($parts[1] ?? '');
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|