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

223 lines
7.2 KiB
PHP

<?php
namespace App\Livewire\Ui\System;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.dvx')]
#[Title('Installer · Mailwolt')]
class InstallerPage extends Component
{
/* ===== Run state ===== */
public string $state = 'idle'; // idle | running
public bool $running = false;
public ?int $rc = null;
public ?string $lowState = null;
public array $logLines = [];
public int $progressPct = 0;
public string $component = 'all';
public bool $postActionsDone = false;
/* ===== Component status ===== */
public array $componentStatus = [];
private const STATE_DIR = '/var/lib/mailwolt/install';
private const INSTALL_LOG = '/var/log/mailwolt-install.log';
private const COMPONENTS = [
'nginx' => ['label' => 'Nginx', 'service' => 'nginx'],
'postfix' => ['label' => 'Postfix', 'service' => 'postfix'],
'dovecot' => ['label' => 'Dovecot', 'service' => 'dovecot'],
'rspamd' => ['label' => 'Rspamd', 'service' => 'rspamd'],
'fail2ban' => ['label' => 'Fail2ban', 'service' => 'fail2ban'],
'certbot' => ['label' => 'Certbot', 'service' => null, 'binary' => 'certbot'],
];
/* ========================================================= */
public function mount(): void
{
$this->refreshLowLevelState();
$this->readLogLines();
if ($this->running) {
$this->state = 'running';
}
$this->checkComponentStatus();
$this->recalcProgress();
}
public function render()
{
return view('livewire.ui.system.installer-page');
}
/* ================== Aktionen ================== */
public function openConfirmModal(string $component = 'all'): void
{
$this->component = $component;
$this->dispatch('openModal',
component: 'ui.system.modal.installer-confirm-modal',
arguments: ['component' => $component]
);
}
public function runInstaller(string $component = 'all'): void
{
if ($this->running || $this->state === 'running') {
$this->dispatch('toast', type: 'warn', badge: 'Installer',
title: 'Läuft bereits',
text: 'Ein Installer-Prozess ist bereits aktiv.',
duration: 3000);
return;
}
$this->component = $component;
$this->state = 'running';
$this->running = true;
$this->rc = null;
$this->postActionsDone = false;
$this->logLines = ['Installer gestartet …'];
$this->progressPct = 5;
$safeComponent = preg_replace('/[^a-z0-9_-]/i', '', $component);
@shell_exec("nohup sudo -n /usr/local/sbin/mailwolt-install {$safeComponent} >/dev/null 2>&1 &");
}
public function pollStatus(): void
{
$this->refreshLowLevelState();
$this->readLogLines();
$this->recalcProgress();
if ($this->rc !== null) {
$this->running = false;
}
if ($this->lowState === 'done') {
usleep(300_000);
$this->readLogLines();
$this->progressPct = 100;
if ($this->rc === 0 && !$this->postActionsDone) {
@shell_exec('nohup php /var/www/mailwolt/artisan health:collect >/dev/null 2>&1 &');
$this->postActionsDone = true;
$this->dispatch('toast', type: 'done', badge: 'Installer',
title: 'Installation abgeschlossen',
text: 'Die Komponente wurde erfolgreich installiert/konfiguriert.',
duration: 6000);
} elseif ($this->rc !== null && $this->rc !== 0 && !$this->postActionsDone) {
$this->postActionsDone = true;
$this->dispatch('toast', type: 'error', badge: 'Installer',
title: 'Installation fehlgeschlagen',
text: "Rückgabecode: {$this->rc}. Bitte Log prüfen.",
duration: 0);
}
$this->state = 'idle';
$this->checkComponentStatus();
}
}
public function checkComponentStatus(): void
{
$statuses = [];
foreach (self::COMPONENTS as $key => $info) {
$installed = false;
$active = false;
// Check if binary exists
$binary = $info['binary'] ?? $key;
$which = @trim(@shell_exec("which {$binary} 2>/dev/null") ?: '');
$installed = $which !== '';
// Check service active state
if ($installed && isset($info['service']) && $info['service']) {
$svcState = @trim(@shell_exec("systemctl is-active {$info['service']} 2>/dev/null") ?: '');
$active = ($svcState === 'active');
} elseif ($installed && $key === 'certbot') {
$active = true; // certbot is a one-shot tool, if installed it's "OK"
}
$statuses[$key] = [
'label' => $info['label'],
'installed' => $installed,
'active' => $active,
];
}
$this->componentStatus = $statuses;
}
public function clearLog(): void
{
@file_put_contents(self::INSTALL_LOG, '');
$this->logLines = [];
$this->dispatch('toast', type: 'done', badge: 'Installer',
title: 'Log geleert', text: '', duration: 2500);
}
/* ================== Helpers ================== */
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 !== 'done');
$this->rc = ($this->lowState === 'done' && is_numeric($rcRaw)) ? (int) $rcRaw : null;
}
protected function readLogLines(): void
{
$p = self::INSTALL_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 ([
'Installation gestartet' => 10,
'Nginx' => 20,
'Postfix' => 35,
'Dovecot' => 50,
'Rspamd' => 65,
'Fail2ban' => 78,
'SSL' => 88,
'Installation beendet' => 100,
] as $needle => $val) {
if (stripos($text, $needle) !== false) {
$pct = max($pct, $val);
}
}
if ($this->lowState === 'done') {
$pct = 100;
}
$this->progressPct = $pct;
}
}