refactor: integrate diagnostics into Commandocentrum and split EmulatorUpdateService

- Add DiagnosticRunner integration to Commandocentrum for system health display
- Refactor EmulatorUpdateService from 2524 lines to 395 lines (facade pattern)
- Extract EmulatorStatusService, EmulatorJarService, EmulatorSourceService
- Extract EmulatorBuildService, EmulatorSqlService, EmulatorBackupService
- Add shared EmulatorConfiguration trait for dependency injection
- Preserve backward compatibility on all public methods
This commit is contained in:
root
2026-05-19 20:20:43 +02:00
parent b1739cabbf
commit f5666c104d
17 changed files with 2743 additions and 7197 deletions
File diff suppressed because it is too large Load Diff
@@ -9,6 +9,7 @@ use App\Models\Miscellaneous\WebsitePermission;
use App\Models\StaffActivity; use App\Models\StaffActivity;
use App\Services\AutoDetectService; use App\Services\AutoDetectService;
use App\Services\CatalogService; use App\Services\CatalogService;
use App\Services\Diagnostics\DiagnosticRunner;
use App\Services\EmulatorUpdateService; use App\Services\EmulatorUpdateService;
use App\Services\RconService; use App\Services\RconService;
use App\Services\SettingsService; use App\Services\SettingsService;
@@ -61,9 +62,13 @@ final class Commandocentrum extends Page implements HasForms
public string $catalogSyncUrl = ''; public string $catalogSyncUrl = '';
/** @var array<\App\Services\Diagnostics\DiagnosticResult> */
public array $diagnostics = [];
public function mount(): void public function mount(): void
{ {
$this->fillForm(); $this->fillForm();
$this->runDiagnostics();
} }
protected function fillForm(): void protected function fillForm(): void
@@ -147,6 +152,22 @@ final class Commandocentrum extends Page implements HasForms
->content(fn (): HtmlString => $this->renderServerInfo()), ->content(fn (): HtmlString => $this->renderServerInfo()),
]), ]),
Section::make('🩺 Systeem Gezondheid')
->description('Automatische systeem diagnostiek')
->icon('heroicon-o-heart')
->afterHeader([
Action::make('refresh_diagnostics')
->label('Vernieuwen')
->icon('heroicon-o-arrow-path')
->color('info')
->action('refreshDiagnostics'),
])
->schema([
Placeholder::make('diagnostics')
->label('')
->content(fn (): HtmlString => $this->renderDiagnostics()),
]),
Section::make('🏨 Hotel Status') Section::make('🏨 Hotel Status')
->description('Emulator en Nitro status') ->description('Emulator en Nitro status')
->icon('heroicon-o-building-office') ->icon('heroicon-o-building-office')
@@ -1941,4 +1962,111 @@ final class Commandocentrum extends Page implements HasForms
->color($color) ->color($color)
->send(); ->send();
} }
public function refreshDiagnostics(): void
{
$this->runDiagnostics();
$this->notify('Success', 'Diagnostiek vernieuwd', 'success');
}
private function runDiagnostics(): void
{
$runner = app(DiagnosticRunner::class);
$this->diagnostics = $runner->runAll();
}
private function renderDiagnostics(): HtmlString
{
if ($this->diagnostics === []) {
$this->runDiagnostics();
}
$errors = array_filter($this->diagnostics, fn ($r) => $r->status === 'error');
$warnings = array_filter($this->diagnostics, fn ($r) => $r->status === 'warning');
$ok = array_filter($this->diagnostics, fn ($r) => $r->status === 'ok');
$errorCount = count($errors);
$warningCount = count($warnings);
$okCount = count($ok);
$overallStatus = $errorCount > 0 ? 'error' : ($warningCount > 0 ? 'warning' : 'ok');
$overallColor = match ($overallStatus) {
'error' => '#ef4444',
'warning' => '#f59e0b',
default => '#22c55e',
};
$overallLabel = match ($overallStatus) {
'error' => 'Kritieke Problemen',
'warning' => 'Waarschuwingen',
default => 'Gezond',
};
$html = '<div style="display:flex;flex-direction:column;gap:16px;">';
// Summary cards
$html .= '<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;">';
$html .= $this->getSummaryCardHtml('Gezond', $okCount, '#22c55e', 'heroicon-o-check-circle');
$html .= $this->getSummaryCardHtml('Waarschuwingen', $warningCount, '#f59e0b', 'heroicon-o-exclamation-triangle');
$html .= $this->getSummaryCardHtml('Fouten', $errorCount, '#ef4444', 'heroicon-o-x-circle');
$html .= '</div>';
// Overall status banner
$html .= '<div style="background:{$overallColor}15;border:1px solid {$overallColor}30;border-radius:12px;padding:16px;display:flex;align-items:center;gap:12px;">';
$html .= '<div style="width:12px;height:12px;border-radius:50%;background:{$overallColor};"></div>';
$html .= '<span style="font-weight:700;color:{$overallColor};font-size:16px;">Systeem Status: {$overallLabel}</span>';
$html .= '</div>';
// Detailed results
if ($errorCount > 0 || $warningCount > 0) {
$html .= '<div style="display:flex;flex-direction:column;gap:8px;">';
foreach ($this->diagnostics as $result) {
if ($result->status === 'ok') {
continue;
}
$color = $result->status === 'error' ? '#ef4444' : '#f59e0b';
$icon = $result->status === 'error'
? '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="' . $color . '" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>'
: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="' . $color . '" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>';
$html .= '<div style="background:#fff;border:1px solid ' . $color . '30;border-radius:10px;padding:14px 16px;display:flex;align-items:flex-start;gap:12px;">';
$html .= '<div style="flex-shrink:0;margin-top:2px;">' . $icon . '</div>';
$html .= '<div style="flex:1;">';
$html .= '<div style="font-weight:600;color:#1e293b;font-size:14px;">' . e($result->name) . '</div>';
$html .= '<div style="color:#64748b;font-size:13px;margin-top:2px;">' . e($result->message) . '</div>';
if ($result->fix) {
$html .= '<div style="background:#f8fafc;border-radius:6px;padding:8px 12px;margin-top:8px;font-size:12px;color:#475569;font-family:monospace;">💡 ' . e($result->fix) . '</div>';
}
$html .= '</div></div>';
}
$html .= '</div>';
}
$html .= '</div>';
return new HtmlString($html);
}
private function getSummaryCardHtml(string $label, int $count, string $color, string $icon): string
{
$iconSvg = match ($icon) {
'heroicon-o-check-circle' => '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />',
'heroicon-o-exclamation-triangle' => '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />',
'heroicon-o-x-circle' => '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" />',
default => '',
};
return <<<HTML
<div style="background:#fff;border-radius:12px;padding:16px;border:1px solid #e2e8f0;box-shadow:0 1px 3px rgba(0,0,0,0.06);">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px;">
<div style="background:{$color}15;padding:8px;border-radius:10px;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="{$color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
{$iconSvg}
</svg>
</div>
<span style="font-size:12px;font-weight:600;color:#64748b;">{$label}</span>
</div>
<div style="font-size:28px;font-weight:800;color:{$color};line-height:1;">{$count}</div>
</div>
HTML;
}
} }
+124
View File
@@ -0,0 +1,124 @@
<?php
namespace App\Services\Diagnostics;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class DatabaseDiagnostic
{
/**
* @return array<DiagnosticResult>
*/
public function runAll(): array
{
return [
$this->checkConnection(),
$this->checkMigrations(),
$this->checkRequiredTables(),
$this->checkRadioTables(),
];
}
public function checkConnection(): DiagnosticResult
{
try {
DB::connection()->getPdo();
return DiagnosticResult::ok('Database Connection', 'Connected to ' . DB::connection()->getDatabaseName());
} catch (\Exception $e) {
return DiagnosticResult::error('Database Connection', $e->getMessage(), 'Check DB credentials in .env');
}
}
public function checkMigrations(): DiagnosticResult
{
try {
$pending = DB::table('migrations')->exists()
? count($this->getPendingMigrations())
: 'unknown';
if ($pending === 'unknown') {
return DiagnosticResult::warning('Migrations', 'Migrations table not found');
}
if ($pending > 0) {
return DiagnosticResult::warning('Migrations', "{$pending} pending migrations", 'Run: php artisan migrate');
}
return DiagnosticResult::ok('Migrations', 'All migrations up to date');
} catch (\Exception $e) {
return DiagnosticResult::error('Migrations', $e->getMessage());
}
}
public function checkRequiredTables(): DiagnosticResult
{
$requiredTables = [
'users', 'permissions', 'website_settings', 'website_articles',
'website_shop_articles', 'website_shop_categories',
];
$missing = [];
foreach ($requiredTables as $table) {
if (! Schema::hasTable($table)) {
$missing[] = $table;
}
}
if ($missing !== []) {
return DiagnosticResult::error(
'Required Tables',
'Missing: ' . implode(', ', $missing),
'Run: php artisan migrate'
);
}
return DiagnosticResult::ok('Required Tables', 'All required tables exist');
}
public function checkRadioTables(): DiagnosticResult
{
$radioTables = [
'radio_ranks', 'radio_banners', 'radio_schedules',
'radio_shouts', 'radio_history',
];
$missing = [];
foreach ($radioTables as $table) {
if (! Schema::hasTable($table)) {
$missing[] = $table;
}
}
if ($missing !== []) {
return DiagnosticResult::warning(
'Radio Tables',
'Missing: ' . implode(', ', $missing),
'Run radio migration seeder'
);
}
return DiagnosticResult::ok('Radio Tables', 'All radio tables exist');
}
/**
* @return array<string>
*/
private function getPendingMigrations(): array
{
$migrated = DB::table('migrations')->pluck('migration')->toArray();
$allMigrations = [];
$migrationPath = database_path('migrations');
if (is_dir($migrationPath)) {
foreach (scandir($migrationPath) as $file) {
if (str_ends_with($file, '.php')) {
$allMigrations[] = pathinfo($file, PATHINFO_FILENAME);
}
}
}
return array_diff($allMigrations, $migrated);
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace App\Services\Diagnostics;
interface DiagnosticCheck
{
public function name(): string;
public function run(): DiagnosticResult;
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Services\Diagnostics;
class DiagnosticResult
{
public function __construct(
public readonly string $name,
public readonly string $status = 'ok',
public readonly string $message = '',
public readonly ?string $fix = null,
) {}
public static function ok(string $name, string $message = ''): self
{
return new self($name, 'ok', $message);
}
public static function warning(string $name, string $message, ?string $fix = null): self
{
return new self($name, 'warning', $message, $fix);
}
public static function error(string $name, string $message, ?string $fix = null): self
{
return new self($name, 'error', $message, $fix);
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
namespace App\Services\Diagnostics;
class DiagnosticRunner
{
/**
* @var array<DiagnosticResult>
*/
private array $results = [];
/**
* @return array<DiagnosticResult>
*/
public function runAll(): array
{
$this->results = [];
$diagnostics = [
new DatabaseDiagnostic(),
new SecurityDiagnostic(),
new SystemDiagnostic(),
new HttpDiagnostic(),
];
foreach ($diagnostics as $diagnostic) {
$this->results = array_merge($this->results, $diagnostic->runAll());
}
return $this->results;
}
public function hasErrors(): bool
{
return array_any($this->results, fn ($r) => $r->status === 'error');
}
public function hasWarnings(): bool
{
return array_any($this->results, fn ($r) => $r->status === 'warning');
}
public function getErrors(): array
{
return array_filter($this->results, fn ($r) => $r->status === 'error');
}
public function getWarnings(): array
{
return array_filter($this->results, fn ($r) => $r->status === 'warning');
}
public function getOk(): array
{
return array_filter($this->results, fn ($r) => $r->status === 'ok');
}
}
+96
View File
@@ -0,0 +1,96 @@
<?php
namespace App\Services\Diagnostics;
use Illuminate\Support\Facades\Http;
class HttpDiagnostic
{
/**
* @return array<DiagnosticResult>
*/
public function runAll(): array
{
return [
$this->checkAppUrl(),
$this->checkSslCertificate(),
$this->checkHttpRedirect(),
];
}
public function checkAppUrl(): DiagnosticResult
{
$appUrl = config('app.url');
if (empty($appUrl) || $appUrl === 'http://localhost') {
return DiagnosticResult::warning(
'App URL',
'APP_URL not configured properly',
'Set APP_URL in .env to your domain'
);
}
if (! str_starts_with($appUrl, 'https://') && app()->environment('production')) {
return DiagnosticResult::warning(
'App URL',
'Not using HTTPS in production',
'Configure SSL and update APP_URL'
);
}
return DiagnosticResult::ok('App URL', $appUrl);
}
public function checkSslCertificate(): DiagnosticResult
{
$appUrl = config('app.url');
if (! str_starts_with($appUrl, 'https://')) {
return DiagnosticResult::warning('SSL', 'Not using HTTPS');
}
$host = parse_url($appUrl, PHP_URL_HOST);
if (! $host) {
return DiagnosticResult::warning('SSL', 'Could not parse host from APP_URL');
}
try {
$response = Http::timeout(5)->get($appUrl);
if ($response->successful()) {
return DiagnosticResult::ok('SSL', 'Certificate valid');
}
return DiagnosticResult::warning('SSL', 'HTTPS endpoint returned ' . $response->status());
} catch (\Exception $e) {
return DiagnosticResult::error('SSL', $e->getMessage(), 'Check SSL certificate configuration');
}
}
public function checkHttpRedirect(): DiagnosticResult
{
$appUrl = config('app.url');
if (! str_starts_with($appUrl, 'https://')) {
return DiagnosticResult::ok('HTTP Redirect', 'Not applicable (no HTTPS)');
}
$httpUrl = str_replace('https://', 'http://', $appUrl);
try {
$response = Http::timeout(5)->withoutRedirecting()->get($httpUrl);
if (in_array($response->status(), [301, 302])) {
return DiagnosticResult::ok('HTTP Redirect', 'HTTP redirects to HTTPS');
}
return DiagnosticResult::warning(
'HTTP Redirect',
"HTTP returns {$response->status()} instead of redirect",
'Configure web server to redirect HTTP to HTTPS'
);
} catch (\Exception $e) {
return DiagnosticResult::warning('HTTP Redirect', 'Could not test: ' . $e->getMessage());
}
}
}
+90
View File
@@ -0,0 +1,90 @@
<?php
namespace App\Services\Diagnostics;
use Illuminate\Support\Facades\File;
class SecurityDiagnostic
{
/**
* @return array<DiagnosticResult>
*/
public function runAll(): array
{
return [
$this->checkAppKey(),
$this->checkDebugMode(),
$this->checkEnvFile(),
$this->checkFilePermissions(),
];
}
public function checkAppKey(): DiagnosticResult
{
$key = config('app.key');
if (empty($key)) {
return DiagnosticResult::error('App Key', 'No application key set', 'Run: php artisan key:generate');
}
return DiagnosticResult::ok('App Key', 'Application key is set');
}
public function checkDebugMode(): DiagnosticResult
{
if (config('app.debug') && app()->environment('production')) {
return DiagnosticResult::error(
'Debug Mode',
'Debug mode is enabled in production',
'Set APP_DEBUG=false in .env'
);
}
return DiagnosticResult::ok('Debug Mode', 'Debug mode is ' . (config('app.debug') ? 'enabled (dev)' : 'disabled'));
}
public function checkEnvFile(): DiagnosticResult
{
$envPath = base_path('.env');
if (! File::exists($envPath)) {
return DiagnosticResult::error('.env File', '.env file not found', 'Copy .env.example to .env');
}
$content = File::get($envPath);
if (! str_contains($content, "\n") && strlen($content) > 500) {
return DiagnosticResult::warning(
'.env File',
'File appears to be on a single line',
'Ensure .env has proper line breaks'
);
}
return DiagnosticResult::ok('.env File', 'File exists and is properly formatted');
}
public function checkFilePermissions(): DiagnosticResult
{
$directories = [
storage_path(),
bootstrap_path('cache'),
];
$issues = [];
foreach ($directories as $dir) {
if (! is_writable($dir)) {
$issues[] = $dir;
}
}
if ($issues !== []) {
return DiagnosticResult::error(
'File Permissions',
'Not writable: ' . implode(', ', $issues),
'Run: chmod -R 775 storage bootstrap/cache'
);
}
return DiagnosticResult::ok('File Permissions', 'All directories are writable');
}
}
+100
View File
@@ -0,0 +1,100 @@
<?php
namespace App\Services\Diagnostics;
use Illuminate\Support\Facades\Redis;
class SystemDiagnostic
{
/**
* @return array<DiagnosticResult>
*/
public function runAll(): array
{
return [
$this->checkPhpExtensions(),
$this->checkPhpVersion(),
$this->checkCache(),
$this->checkSession(),
$this->checkRedis(),
];
}
public function checkPhpExtensions(): DiagnosticResult
{
$required = ['pdo_mysql', 'curl', 'json', 'mbstring', 'xml', 'zip', 'bcmath'];
$missing = array_filter($required, fn ($ext) => ! extension_loaded($ext));
if ($missing !== []) {
return DiagnosticResult::error(
'PHP Extensions',
'Missing: ' . implode(', ', $missing),
'Install: sudo apt install php-' . implode(' php-', $missing)
);
}
return DiagnosticResult::ok('PHP Extensions', 'All required extensions loaded');
}
public function checkPhpVersion(): DiagnosticResult
{
$current = PHP_VERSION_ID;
$min = 80100; // PHP 8.1
if ($current < $min) {
return DiagnosticResult::error(
'PHP Version',
'Current: ' . PHP_VERSION . ' (minimum: 8.1)',
'Upgrade PHP to 8.1 or higher'
);
}
return DiagnosticResult::ok('PHP Version', PHP_VERSION);
}
public function checkCache(): DiagnosticResult
{
try {
Cache::put('diagnostic_test', 'ok', 10);
$value = Cache::get('diagnostic_test');
if ($value === 'ok') {
return DiagnosticResult::ok('Cache', 'Working (' . config('cache.default') . ')');
}
return DiagnosticResult::warning('Cache', 'Cache returned unexpected value');
} catch (\Exception $e) {
return DiagnosticResult::error('Cache', $e->getMessage());
}
}
public function checkSession(): DiagnosticResult
{
$driver = config('session.driver');
if ($driver === 'file' && app()->environment('production')) {
return DiagnosticResult::warning(
'Session',
'Using file sessions in production',
'Consider using redis or database sessions'
);
}
return DiagnosticResult::ok('Session', "Driver: {$driver}");
}
public function checkRedis(): DiagnosticResult
{
if (config('redis.default.host') === '127.0.0.1') {
try {
Redis::ping();
return DiagnosticResult::ok('Redis', 'Connected');
} catch (\Exception $e) {
return DiagnosticResult::warning('Redis', 'Not reachable: ' . $e->getMessage());
}
}
return DiagnosticResult::ok('Redis', 'Not configured (optional)');
}
}
+80
View File
@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace App\Services\Emulator;
use Illuminate\Support\Facades\Process;
class EmulatorBackupService
{
use EmulatorConfiguration;
public function __construct()
{
$this->loadConfiguration();
}
public function getList(): array
{
$backups = [];
try {
$backupDir = $this->jarPath . '/backup';
if (! is_dir($backupDir)) {
return $backups;
}
$result = Process::timeout(5)->run("ls -1t {$backupDir}/*.jar 2>/dev/null");
if ($result->successful()) {
$lines = array_filter(explode("\n", trim($result->output())));
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line === '0' || ! str_contains($line, '.jar')) {
continue;
}
$filename = basename($line);
$version = $this->extractVersionFromFilename($filename);
preg_match('/(\d{4}-\d{2}-\d{2}_\d{2}-\d{2})/', $line, $dateMatches);
$date = $dateMatches[1] ?? date('Y-m-d_H-i');
$backups[] = [
'name' => $filename,
'jar' => $filename,
'date' => $date,
'version' => $version,
];
}
}
} catch (\Exception) {
}
return $backups;
}
public function restore(string $backupName): array
{
try {
$backupDir = $this->jarPath . '/backup';
$backupPath = $backupDir . '/' . $backupName;
$targetPath = $this->jarPath . '/' . $backupName;
if (! file_exists($backupPath)) {
return ['success' => false, 'error' => 'Backup niet gevonden'];
}
if (file_exists($targetPath)) {
unlink($targetPath);
}
copy($backupPath, $targetPath);
chmod($targetPath, 0755);
return ['success' => true, 'message' => 'Backup hersteld: ' . $backupName];
} catch (\Exception $e) {
return ['success' => false, 'error' => $e->getMessage()];
}
}
}
@@ -0,0 +1,273 @@
<?php
declare(strict_types=1);
namespace App\Services\Emulator;
use App\Services\Emulator\EmulatorSqlService;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
class EmulatorBuildService
{
use EmulatorConfiguration;
public function __construct()
{
$this->loadConfiguration();
}
public function buildFromSource(bool $force = false): array
{
$repo = $this->sourceRepo ?: $this->githubRepo;
$branch = $this->sourceBranch ?: $this->githubBranch ?: 'main';
if (! $repo) {
return ['success' => false, 'error' => 'Geen source repo geconfigureerd'];
}
$sourcePath = $this->emulatorSourcePath;
$serviceName = $this->emulatorService;
Log::info('[EmulatorBuild] Starting source build', [
'repo' => $repo,
'branch' => $branch,
'path' => $sourcePath,
]);
$this->ensureJavaInstalled();
Process::timeout(10)->run('mkdir -p ' . escapeshellarg(dirname((string) $sourcePath)));
Process::timeout(10)->run('mkdir -p ' . escapeshellarg((string) $this->jarPath));
Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg(dirname((string) $sourcePath)));
$maxRetries = 2;
$lastError = '';
for ($retry = 0; $retry <= $maxRetries; $retry++) {
Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg(dirname((string) $sourcePath)) . ' 2>/dev/null || true');
Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg((string) $sourcePath) . ' 2>/dev/null || true');
if ($retry > 0) {
Log::info('[EmulatorBuild] Retry attempt', ['attempt' => $retry]);
Process::timeout(30)->run('rm -rf ' . escapeshellarg((string) $sourcePath));
}
try {
$existsCheck = Process::timeout(5)->run('[ -d ' . escapeshellarg((string) $sourcePath) . " ] && echo 'exists'");
$sourceExists = $existsCheck->successful() && trim($existsCheck->output()) === 'exists';
$gitCheck = Process::timeout(5)->run('[ -d ' . escapeshellarg((string) $sourcePath) . "/.git ] && echo 'git'");
$isGitRepo = $gitCheck->successful() && trim($gitCheck->output()) === 'git';
if ($sourceExists && $isGitRepo) {
Log::info('[EmulatorBuild] Pulling latest changes');
$commands = [
'cd ' . escapeshellarg((string) $sourcePath) . ' && git fetch origin',
'cd ' . escapeshellarg((string) $sourcePath) . ' && git checkout ' . escapeshellarg($branch),
'cd ' . escapeshellarg((string) $sourcePath) . ' && git pull origin ' . escapeshellarg($branch),
];
} else {
Log::info('[EmulatorBuild] Cloning repository');
$commands = [
'sudo rm -rf ' . escapeshellarg((string) $sourcePath) . ' 2>/dev/null || rm -rf ' . escapeshellarg((string) $sourcePath),
'mkdir -p ' . escapeshellarg(dirname((string) $sourcePath)),
'git clone --branch ' . escapeshellarg($branch) . ' --depth 1 https://github.com/' . escapeshellarg($repo) . '.git ' . escapeshellarg((string) $sourcePath),
'chown -R www-data:www-data ' . escapeshellarg((string) $sourcePath),
];
}
$command = implode(' && ', $commands);
$result = Process::timeout(300)->run($command);
if ($result->failed()) {
$lastError = 'Git clone/pull failed: ' . substr($result->errorOutput(), 0, 300);
Log::warning('[EmulatorBuild] Git operation failed', ['error' => $lastError, 'attempt' => $retry]);
continue;
}
$buildCommands = $this->getBuildCommands($sourcePath);
Log::info('[EmulatorBuild] Running build', ['command' => $buildCommands]);
$buildResult = Process::timeout(600)->run('cd ' . escapeshellarg((string) $sourcePath) . ' && ' . $buildCommands);
$hasSignalError = str_contains($buildResult->errorOutput(), 'signal') || str_contains($buildResult->output(), 'signal');
if ($hasSignalError) {
Log::warning('[EmulatorBuild] Build process received signal, checking if JAR was built anyway');
}
$jarPath = $this->findBuiltJar($sourcePath);
if ($jarPath) {
Log::info('[EmulatorBuild] JAR found despite build status', ['jar' => $jarPath]);
} elseif ($buildResult->failed() && ! $hasSignalError) {
$lastError = 'Build failed: ' . substr($buildResult->errorOutput(), 0, 500);
Log::warning('[EmulatorBuild] Build failed', ['error' => $lastError, 'attempt' => $retry]);
if ($retry < $maxRetries) {
$cleanCommands = [
'cd ' . escapeshellarg((string) $sourcePath) . ' && mvn clean 2>/dev/null || ./gradlew clean 2>/dev/null || true',
];
Process::timeout(60)->run(implode(' && ', $cleanCommands));
continue;
}
continue;
}
if (! $jarPath) {
$lastError = 'Build succeeded but JAR not found. Check build output.';
Log::warning('[EmulatorBuild] JAR not found', ['attempt' => $retry]);
continue;
}
return $this->deployJar($jarPath, $serviceName);
} catch (\Exception $e) {
$lastError = $e->getMessage();
Log::error('[EmulatorBuild] Source build exception', ['error' => $lastError, 'attempt' => $retry]);
}
}
return [
'success' => false,
'error' => 'Build mislukt na ' . ($maxRetries + 1) . ' pogingen. Laatste fout: ' . $lastError,
];
}
public function getBuildCommands(string $sourcePath): string
{
$pomPath = $sourcePath;
$pomCheck = Process::timeout(5)->run("[ -f {$pomPath}/pom.xml ] && echo 'pom'");
if (! $pomCheck->successful() || trim($pomCheck->output()) !== 'pom') {
$pomPath = $sourcePath . '/Emulator';
$pomCheck = Process::timeout(5)->run("[ -f {$pomPath}/pom.xml ] && echo 'pom'");
}
$gradlewPath = $sourcePath . '/gradlew';
$gradlewCheck = Process::timeout(5)->run("[ -f {$gradlewPath} ] && echo 'gradlew'");
$gradlePath = $sourcePath . '/build.gradle';
$gradleCheck = Process::timeout(5)->run("[ -f {$gradlePath} ] && echo 'gradle'");
if ($pomCheck->successful() && trim($pomCheck->output()) === 'pom') {
return "cd {$pomPath} && mvn clean package -DskipTests 2>&1";
}
if ($gradlewCheck->successful() && trim($gradlewCheck->output()) === 'gradlew') {
return "cd {$sourcePath} && chmod +x gradlew && ./gradlew clean build -x test 2>&1";
}
if ($gradleCheck->successful() && trim($gradleCheck->output()) === 'gradle') {
return "cd {$sourcePath} && gradle clean build -x test 2>&1";
}
return "cd {$sourcePath} && ls -la";
}
public function findBuiltJar(string $sourcePath): ?string
{
$patterns = [
$sourcePath . '/target/*.jar',
$sourcePath . '/build/libs/*.jar',
$sourcePath . '/*/*.jar',
$sourcePath . '/*.jar',
$sourcePath . '/Emulator/target/*.jar',
$sourcePath . '/Emulator/build/libs/*.jar',
$sourcePath . '/Emulator/*/*.jar',
];
foreach ($patterns as $pattern) {
$result = Process::timeout(10)->run('ls -t ' . $pattern . ' 2>/dev/null | head -1');
if ($result->successful()) {
$jarPath = trim($result->output());
if ($jarPath !== '' && $jarPath !== '0' && str_contains($jarPath, '.jar')) {
return $jarPath;
}
}
}
return null;
}
private function deployJar(string $jarPath, string $serviceName): array
{
$jarName = basename($jarPath);
$version = $this->extractVersionFromFilename($jarName);
if ($version === '' || $version === '0') {
$version = date('Y.m.d');
}
$deployCommands = [
'mkdir -p ' . escapeshellarg((string) $this->jarPath),
'chown -R www-data:www-data ' . escapeshellarg((string) $this->jarPath),
'if ls ' . escapeshellarg((string) $this->jarPath) . '/*.jar 1>/dev/null 2>&1; then mkdir -p ' . escapeshellarg((string) $this->jarPath) . '/backup && mv ' . escapeshellarg((string) $this->jarPath) . '/*.jar ' . escapeshellarg((string) $this->jarPath) . '/backup/; fi',
'cp -f ' . escapeshellarg($jarPath) . ' ' . escapeshellarg((string) $this->jarPath) . '/' . escapeshellarg($jarName),
'chown www-data:www-data ' . escapeshellarg((string) $this->jarPath) . '/' . escapeshellarg($jarName),
'chmod 755 ' . escapeshellarg((string) $this->jarPath) . '/' . escapeshellarg($jarName),
'ls -la ' . escapeshellarg((string) $this->jarPath) . '/',
'systemctl restart ' . escapeshellarg((string) $serviceName) . ' 2>&1 || service ' . escapeshellarg((string) $serviceName) . ' restart 2>&1 || true',
];
$deployCommand = implode(' && ', $deployCommands);
Log::info('[EmulatorBuild] Deploying jar', [
'source' => $jarPath,
'destination' => "{$this->jarPath}/{$jarName}",
]);
$deployResult = Process::timeout(60)->run($deployCommand);
if (! $deployResult->successful()) {
return [
'success' => false,
'error' => 'Deploy failed: ' . substr($deployResult->errorOutput(), 0, 300),
];
}
$sourceService = new EmulatorSourceService;
$sourceInfo = $sourceService->checkForUpdates();
$installedDate = $sourceInfo['latest_timestamp'] ?? time();
$this->settings->set('emulator_version', $version);
$this->settings->set('emulator_jar_installed_date', (string) $installedDate);
$this->settings->set('emulator_source_commit', $sourceInfo['latest_sha'] ?? 'unknown');
$this->settings->set('emulator_source_date', (string) $installedDate);
$currentBranch = $this->sourceBranch ?: $this->githubBranch ?: 'main';
$this->settings->set('emulator_installed_branch', $currentBranch);
$sqlService = new EmulatorSqlService;
$sqlService->runUpdates();
Log::info('[EmulatorBuild] Source build successful');
return [
'success' => true,
'version' => $version,
'jar' => $jarName,
'built' => true,
'message' => "✅ Emulator vanaf source gebouwd!\n📦 {$jarName}\n🔄 Service herstart",
];
}
private function ensureJavaInstalled(): void
{
$javaCheck = Process::timeout(5)->run('java -version 2>&1');
if (! $javaCheck->successful()) {
Log::warning('[EmulatorBuild] Java not found, attempting to install');
Process::timeout(180)->run('apt-get update && apt-get install -y default-jdk 2>&1');
}
$mavenCheck = Process::timeout(5)->run('which mvn');
if (! $mavenCheck->successful()) {
Log::warning('[EmulatorBuild] Maven not found, attempting to install');
Process::timeout(180)->run('apt-get install -y maven 2>&1');
}
}
}
+197
View File
@@ -0,0 +1,197 @@
<?php
declare(strict_types=1);
namespace App\Services\Emulator;
use App\Services\SettingsService;
use Illuminate\Support\Facades\Process;
trait EmulatorConfiguration
{
private ?SettingsService $settings;
private ?string $githubUrl;
private ?string $jarDirectUrl;
private ?string $githubRepo;
private ?string $githubBranch;
private ?string $jarPath;
private ?string $emulatorService;
private ?string $emulatorSourcePath;
private ?string $sourceRepo;
private ?string $sourceBranch;
protected function loadConfiguration(): void
{
$this->settings = app(SettingsService::class);
$this->settings->clearInstanceCache();
$this->githubUrl = $this->settings->getOrDefault('emulator_github_url', '');
$this->jarDirectUrl = $this->settings->getOrDefault('emulator_jar_direct_url', '');
$basePath = $this->detectBasePath();
$this->jarPath = $this->resolveEmulatorPath($this->settings->getOrDefault('emulator_jar_path', $basePath . '/Emulator'));
$this->emulatorService = $this->settings->getOrDefault('emulator_service_name', $this->detectEmulatorService());
$this->emulatorSourcePath = $this->resolveEmulatorPath($this->settings->getOrDefault('emulator_source_path', $basePath . '/emulator-source'));
$this->sourceRepo = $this->settings->getOrDefault('emulator_source_repo', '');
$this->parseGitHubUrl($this->githubUrl);
$this->parseSourceRepo($this->sourceRepo);
$this->ensureGitSafeDirectories();
$settingBranch = $this->settings->getOrDefault('emulator_github_branch', null);
if ($settingBranch) {
$this->githubBranch = strtolower((string) $settingBranch);
$this->sourceBranch = strtolower((string) $settingBranch);
}
}
private function detectBasePath(): string
{
$possiblePaths = [
base_path(),
'/var/www/atomcms',
'/var/www/html',
'/var/www',
dirname(base_path()),
];
foreach ($possiblePaths as $path) {
if (is_dir($path)) {
return $path;
}
}
return '/var/www';
}
private function resolveEmulatorPath(string $path): string
{
if (str_starts_with($path, '/')) {
return $path;
}
return $this->detectBasePath() . '/' . ltrim($path, '/');
}
private function detectEmulatorService(): string
{
$possibleServices = ['emulator', 'arcturus', 'morningstar', 'habbo', 'hotel', 'game'];
foreach ($possibleServices as $service) {
$result = Process::timeout(5)->run("systemctl list-unit-files {$service}.service 2>/dev/null | grep -q '{$service}.service' && echo 'found'");
if ($result->successful() && trim($result->output()) === 'found') {
return $service;
}
$result = Process::timeout(5)->run("systemctl list-units --type=service --all 2>/dev/null | grep -q '{$service}.service' && echo 'found'");
if ($result->successful() && trim($result->output()) === 'found') {
return $service;
}
}
return 'emulator';
}
private function ensureGitSafeDirectories(): void
{
$directories = [
base_path(),
'/var/www/atomcms',
'/var/www',
$this->emulatorSourcePath,
$this->jarPath,
dirname((string) $this->emulatorSourcePath),
dirname((string) $this->jarPath),
];
foreach (array_unique($directories) as $dir) {
if (! empty($dir) && $this->isDirAccessible($dir)) {
Process::timeout(5)->run('git config --global --add safe.directory ' . escapeshellarg($dir) . ' 2>/dev/null || true');
}
}
Process::timeout(5)->run("git config --global --add safe.directory '*' 2>/dev/null || true");
}
private function isDirAccessible(string $path): bool
{
if ($path === '' || $path === '0') {
return false;
}
$result = Process::timeout(5)->run('test -d ' . escapeshellarg($path) . ' && echo "yes" || echo "no"');
return trim($result->output()) === 'yes';
}
private function parseGitHubUrl(string $url): void
{
$parsed = $this->parseGithubRepoUrl($url);
$this->githubRepo = $parsed['repo'];
$this->githubBranch = $parsed['branch'];
}
private function parseSourceRepo(string $url): void
{
$parsed = $this->parseGithubRepoUrl($url);
$this->sourceRepo = $parsed['repo'];
$this->sourceBranch = $parsed['branch'];
}
private function parseGithubRepoUrl(string $url): array
{
if ($url === '' || $url === '0') {
return ['repo' => null, 'branch' => 'main'];
}
if (preg_match('/github\.com\/([^\/]+)\/([^\/\?#]+)/', $url, $matches)) {
$repo = $matches[1] . '/' . $matches[2];
$branch = 'main';
if (preg_match('/\/tree\/([^\/]+)/', $url, $branchMatch)) {
$branch = $branchMatch[1];
}
return ['repo' => $repo, 'branch' => $branch];
}
return ['repo' => null, 'branch' => 'main'];
}
protected function extractVersionFromFilename(string $filename): string
{
$filename = basename($filename, '.jar');
if (preg_match('/[\d]+\.[\d]+\.[\d]+/', $filename, $matches)) {
return $matches[0];
}
if (preg_match('/v?([\d]+)/', $filename, $matches)) {
return $matches[1];
}
return '';
}
protected function commandDirExists(string $path): bool
{
$result = Process::timeout(5)->run('[ -d ' . escapeshellarg($path) . ' ] && echo "1" || echo "0"');
return trim($result->output()) === '1';
}
protected function commandFileExists(string $path): bool
{
$result = Process::timeout(5)->run('ls ' . $path . ' 2>/dev/null | head -1');
return $result->successful() && trim($result->output()) !== '';
}
}
@@ -0,0 +1,510 @@
<?php
declare(strict_types=1);
namespace App\Services\Emulator;
use App\Services\Emulator\EmulatorSourceService;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Str;
class EmulatorJarService
{
use EmulatorConfiguration;
public function __construct()
{
$this->loadConfiguration();
}
public function isConfigured(): bool
{
return ! in_array($this->githubUrl, [null, '', '0'], true) || ! in_array($this->jarDirectUrl, [null, '', '0'], true);
}
public function checkForUpdates(): array
{
if (! $this->isConfigured()) {
return [
'update_available' => false,
'error' => 'Configureer een GitHub URL of directe .jar URL',
];
}
$sourceService = new EmulatorSourceService;
$sourceInfo = $sourceService->checkForUpdates();
$hasSourceUpdates = $sourceInfo && $sourceInfo['has_update'];
if (! in_array($this->jarDirectUrl, [null, '', '0'], true)) {
return $this->checkDirectUrlUpdates($sourceInfo, $hasSourceUpdates);
}
return $this->checkGitHubFolderUpdates($sourceInfo, $hasSourceUpdates);
}
public function performUpdate(array $check): array
{
$jarUrl = $check['jar_url'];
$jarName = $check['jar_name'];
$version = $check['latest_version'];
$serviceName = $this->emulatorService;
$tempDir = '/tmp/emulator-update-' . Str::random(8);
$tempJar = $tempDir . '/' . $jarName;
$updateScript = $this->buildUpdateScript($jarUrl, $jarName, $tempDir, $tempJar, $serviceName);
$scriptPath = '/tmp/emulator_update_' . uniqid() . '.sh';
file_put_contents($scriptPath, $updateScript);
chmod($scriptPath, 0755);
Log::info('[EmulatorJar] Starting update', [
'version' => $version,
'jar' => $jarName,
'url' => $jarUrl,
]);
try {
$result = Process::timeout(600)->run('bash ' . $scriptPath . ' 2>&1');
@unlink($scriptPath);
if ($result->exitCode() !== 0) {
Log::error('[EmulatorJar] Update failed', [
'output' => $result->output(),
'error' => $result->errorOutput(),
]);
return [
'success' => false,
'error' => 'Update mislukt: ' . substr($result->output(), 0, 300),
];
}
$this->storeUpdateInfo($version, $check);
Log::info('[EmulatorJar] Update successful');
return [
'success' => true,
'version' => $version,
'jar' => $jarName,
'message' => "✅ Emulator geüpdatet naar v{$version}!\n📦 {$jarName}\n🔄 Service herstart",
];
} catch (\Exception $e) {
Log::error('[EmulatorJar] Exception', ['error' => $e->getMessage()]);
return [
'success' => false,
'error' => $e->getMessage(),
];
}
}
public function findLatestJar(): ?array
{
if (! $this->githubRepo) {
return null;
}
$branch = $this->githubBranch ?: 'main';
$commonNames = ['arcturus.jar', 'Arcturus.jar', 'emulator.jar', 'habbo.jar', 'hotel.jar'];
foreach ($commonNames as $name) {
try {
$apiUrl = "https://api.github.com/repos/{$this->githubRepo}/contents/Latest_Compiled_Version/{$name}?ref={$branch}";
$response = Http::timeout(10)
->withHeaders([
'Accept' => 'application/vnd.github.v3+json',
'User-Agent' => 'AtomCMS-Emulator-Updater',
])
->get($apiUrl);
if (! $response->successful()) {
continue;
}
$data = json_decode($response->body(), true);
if (! isset($data['sha']) || ! isset($data['download_url'])) {
continue;
}
$commitDate = null;
$commitSha = $data['sha'];
try {
$commitsResponse = Http::timeout(10)
->withHeaders([
'Accept' => 'application/vnd.github.v3+json',
'User-Agent' => 'AtomCMS-Emulator-Updater',
])
->get("https://api.github.com/repos/{$this->githubRepo}/commits", [
'path' => "Latest_Compiled_Version/{$name}",
'sha' => $branch,
'per_page' => 1,
]);
if ($commitsResponse->successful()) {
$commits = $commitsResponse->json();
if (! empty($commits) && isset($commits[0]['commit']['committer']['date'])) {
$commitDate = strtotime($commits[0]['commit']['committer']['date']);
$commitSha = $commits[0]['sha'] ?? $data['sha'];
}
}
} catch (\Exception) {
Log::debug('[EmulatorJar] Could not fetch commit date for ' . $name);
}
$installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null);
$installedJarCommit = $this->settings->getOrDefault('emulator_jar_commit', null);
$isUpdate = false;
if ($installedJarCommit !== null && $commitSha !== null) {
$isUpdate = $installedJarCommit !== $commitSha;
} elseif ($installedDate !== null && $commitDate) {
$isUpdate = (int) $installedDate < $commitDate;
} elseif ($installedDate === null && $commitDate) {
$isUpdate = true;
}
$version = $this->extractVersionFromFilename($name);
if ($version === '' || $version === '0') {
$version = $commitDate ? date('Y.m.d', $commitDate) : date('Y.m.d');
}
return [
'name' => $name,
'url' => $data['download_url'],
'version' => $version,
'commit' => $commitSha,
'commit_date' => $commitDate,
'is_update' => $isUpdate,
'installed_date' => $installedDate,
];
} catch (\Exception $e) {
Log::warning('[EmulatorJar] Error checking JAR file ' . $name, ['error' => $e->getMessage()]);
}
}
return null;
}
private function checkDirectUrlUpdates(?array $sourceInfo, bool $hasSourceUpdates): array
{
$jarInfo = $this->validateDirectUrl($this->jarDirectUrl);
if ($jarInfo) {
if (! empty($jarInfo['version'])) {
$currentVersion = $this->settings->getOrDefault('emulator_version', '0.0.0');
if ($jarInfo['version'] !== $currentVersion) {
$this->settings->set('emulator_version', $jarInfo['version']);
}
}
$hasJarUpdates = $jarInfo['is_update'] ?? false;
if ($hasSourceUpdates && ! $hasJarUpdates) {
return [
'update_available' => true,
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
'latest_version' => $sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : $jarInfo['version'],
'release_name' => 'Source Update',
'jar_url' => null,
'jar_name' => null,
'jar_size' => 'Onbekend',
'type' => 'source_build',
'source_info' => $sourceInfo,
'message' => 'Nieuwe commits beschikbaar - build vanaf source nodig',
];
}
return [
'update_available' => $hasJarUpdates || $hasSourceUpdates,
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
'latest_version' => $jarInfo['version'],
'release_name' => $hasSourceUpdates ? 'Source Update' : 'Direct URL',
'jar_url' => $this->jarDirectUrl,
'jar_name' => $jarInfo['name'],
'jar_size' => 'Onbekend',
'type' => $hasSourceUpdates ? 'source_build' : 'direct_url',
'commit' => $jarInfo['commit_sha'] ?? null,
'source_info' => $sourceInfo,
'has_source_updates' => $hasSourceUpdates,
];
}
if ($hasSourceUpdates) {
return [
'update_available' => true,
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
'latest_version' => $sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : 'Onbekend',
'release_name' => 'Source Update',
'jar_url' => null,
'jar_name' => null,
'jar_size' => 'Onbekend',
'type' => 'source_build',
'source_info' => $sourceInfo,
'message' => 'Nieuwe commits beschikbaar - build vanaf source nodig',
];
}
return [
'update_available' => false,
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
'error' => 'Directe .jar URL is niet bereikbaar',
];
}
private function checkGitHubFolderUpdates(?array $sourceInfo, bool $hasSourceUpdates): array
{
$jarInfo = $this->findLatestJar();
if (! $jarInfo) {
if ($hasSourceUpdates) {
return [
'update_available' => true,
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
'latest_version' => $sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : 'Onbekend',
'release_name' => 'Source Update',
'jar_url' => null,
'jar_name' => null,
'jar_size' => 'Onbekend',
'type' => 'source_build',
'source_info' => $sourceInfo,
'message' => 'Nieuwe commits beschikbaar - build vanaf source nodig',
];
}
return [
'update_available' => false,
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
'latest_version' => 'Onbekend',
'error' => 'Kon geen .jar bestand vinden',
'type' => 'not_found',
'source_available' => $sourceInfo !== null,
];
}
$version = $jarInfo['version'];
$hasJarUpdates = $jarInfo['is_update'] ?? false;
if ($hasSourceUpdates && ! $hasJarUpdates) {
return [
'update_available' => true,
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
'latest_version' => $sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : $version,
'release_name' => 'Source Update',
'jar_url' => null,
'jar_name' => null,
'jar_size' => 'Onbekend',
'type' => 'source_build',
'source_info' => $sourceInfo,
'message' => 'Nieuwe commits beschikbaar - build vanaf source nodig',
];
}
return [
'update_available' => $hasJarUpdates || $hasSourceUpdates,
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
'latest_version' => $hasSourceUpdates ? ($sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : $version) : $version,
'release_name' => $hasSourceUpdates ? 'Source Update' : 'Latest from GitHub',
'jar_url' => $jarInfo['url'],
'jar_name' => $jarInfo['name'],
'jar_size' => 'Onbekend',
'type' => $hasSourceUpdates ? 'source_build' : 'github_folder',
'commit' => $jarInfo['commit'] ?? null,
'commit_date' => $jarInfo['commit_date'] ?? null,
'source_info' => $sourceInfo,
'has_source_updates' => $hasSourceUpdates,
];
}
private function validateDirectUrl(string $url): ?array
{
if ($url === '' || $url === '0' || ! str_ends_with(strtolower($url), '.jar')) {
return null;
}
try {
$jarName = basename(parse_url($url, PHP_URL_PATH));
$version = $this->extractVersionFromFilename($jarName);
$storedVersion = $this->settings->getOrDefault('emulator_version', '0.0.0');
$lastModified = null;
$commitSha = null;
$gitHubInfoAvailable = false;
if (preg_match('/github\.com\/([^\/]+)\/([^\/]+)\/raw\/refs\/heads\/([^\/]+)\/(.+)/', $url, $matches)) {
$owner = $matches[1];
$repo = $matches[2];
$branch = $matches[3];
$path = $matches[4];
try {
$apiResponse = Http::timeout(10)
->withHeaders([
'Accept' => 'application/vnd.github.v3+json',
'User-Agent' => 'AtomCMS-Emulator-Updater',
])
->get("https://api.github.com/repos/{$owner}/{$repo}/contents/{$path}", [
'ref' => $branch,
]);
if ($apiResponse->successful()) {
$data = $apiResponse->json();
if (isset($data['sha']) && ! isset($data['message'])) {
$gitHubInfoAvailable = true;
$commitSha = $data['sha'];
if (isset($data['commit']['committer']['date'])) {
$lastModified = strtotime($data['commit']['committer']['date']);
}
}
}
} catch (\Exception) {
Log::debug('[EmulatorJar] Could not fetch GitHub commit info for direct URL');
}
}
if ($lastModified === null) {
$response = Http::timeout(10)->head($url);
if ($response->successful()) {
$modifiedSince = $response->header('Last-Modified');
if ($modifiedSince) {
$lastModified = strtotime($modifiedSince);
if ($lastModified === false) {
$lastModified = null;
}
}
}
}
$isUpdate = false;
$installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null);
$storedData = $this->settings->getOrDefault('emulator_direct_url_info_' . md5($url));
if ($storedData !== null && is_string($storedData)) {
$storedDataArray = json_decode($storedData, true);
if (is_array($storedDataArray)) {
if ($gitHubInfoAvailable && $commitSha !== null && isset($storedDataArray['commit_sha'])) {
$isUpdate = $commitSha !== $storedDataArray['commit_sha'];
} elseif ($lastModified !== null && $installedDate !== null) {
$isUpdate = (int) $installedDate < $lastModified;
} elseif ($lastModified !== null && isset($storedDataArray['last_modified'])) {
$isUpdate = $lastModified > $storedDataArray['last_modified'];
} elseif (! in_array($version, ['', '0', $storedVersion], true)) {
$isUpdate = version_compare($version, $storedVersion) > 0;
}
}
} else {
$isUpdate = true;
}
$infoToStore = [
'last_checked' => time(),
'version' => $version,
];
if ($lastModified) {
$infoToStore['last_modified'] = $lastModified;
}
if ($commitSha) {
$infoToStore['commit_sha'] = $commitSha;
}
$this->settings->set('emulator_direct_url_info_' . md5($url), json_encode($infoToStore));
return [
'name' => $jarName,
'version' => $version,
'last_modified' => $lastModified,
'commit_sha' => $commitSha,
'is_update' => $isUpdate,
'gitHub_rate_limited' => ! $gitHubInfoAvailable && preg_match('/github\.com/', $url),
];
} catch (\Exception $e) {
Log::warning('[EmulatorJar] Direct URL not reachable', ['error' => $e->getMessage()]);
}
return null;
}
private function buildUpdateScript(string $jarUrl, string $jarName, string $tempDir, string $tempJar, string $serviceName): string
{
return <<<BASH
#!/bin/bash
set -e
JAR_URL='{$jarUrl}'
JAR_NAME='{$jarName}'
JAR_PATH='{$this->jarPath}'
SERVICE='{$serviceName}'
TEMP_DIR='{$tempDir}'
TEMP_JAR='{$tempJar}'
mkdir -p "\$TEMP_DIR" "\$JAR_PATH"
if ls "\$JAR_PATH"/*.jar 1>/dev/null 2>&1; then
mkdir -p "\$JAR_PATH/backup"
mv "\$JAR_PATH"/*.jar "\$JAR_PATH/backup/" 2>/dev/null || true
fi
download_success=false
for attempt in 1 2 3; do
if curl -L --max-time 300 --retry 3 --retry-delay 5 -o "\$TEMP_JAR" "\$JAR_URL" 2>&1; then
FILE_TYPE=\$(file -b "\$TEMP_JAR" 2>/dev/null)
JAR_SIZE=\$(stat -c%s "\$TEMP_JAR" 2>/dev/null || echo 0)
if echo "\$FILE_TYPE" | grep -qi "zip\|jar\|archive" && [ "\$JAR_SIZE" -gt 1000 ]; then
download_success=true
break
else
rm -f "\$TEMP_JAR" 2>/dev/null || true
sleep 3
fi
else
sleep 5
fi
done
if [ "\$download_success" = false ]; then
if ls "\$JAR_PATH/backup"/*.jar 1>/dev/null 2>&1; then
mv "\$JAR_PATH/backup"/*.jar "\$JAR_PATH/" 2>/dev/null || true
fi
rm -rf "\$TEMP_DIR"
exit 1
fi
mv "\$TEMP_JAR" "\$JAR_PATH/"
chown -R www-data:www-data "\$JAR_PATH"
chmod 755 "\$JAR_PATH/\$JAR_NAME"
systemctl restart "\$SERVICE" 2>&1 || service "\$SERVICE" restart 2>&1 || true
rm -rf "\$TEMP_DIR" 2>/dev/null || true
BASH;
}
private function storeUpdateInfo(string $version, array $check): void
{
setting('emulator_version', $version);
$commitDate = $check['commit_date'] ?? time();
$this->settings->set('emulator_jar_installed_date', (string) $commitDate);
$this->settings->set('emulator_jar_commit', $check['commit'] ?? null);
$sourceSha = $check['source_info']['latest_sha'] ?? $check['commit'] ?? null;
$sourceDate = $check['source_info']['latest_timestamp'] ?? ($check['commit_date'] ?? time());
if ($sourceSha) {
$this->settings->set('emulator_source_commit', $sourceSha);
}
if ($sourceDate) {
$this->settings->set('emulator_source_date', (string) $sourceDate);
}
$currentBranch = $this->sourceBranch ?: $this->githubBranch ?: 'main';
$this->settings->set('emulator_installed_branch', $currentBranch);
}
}
+273
View File
@@ -0,0 +1,273 @@
<?php
declare(strict_types=1);
namespace App\Services\Emulator;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Str;
class EmulatorSourceService
{
use EmulatorConfiguration;
public function __construct()
{
$this->loadConfiguration();
}
public function checkForUpdates(): ?array
{
$repo = $this->sourceRepo ?: $this->githubRepo;
$branch = $this->sourceBranch ?: $this->githubBranch ?: 'main';
if (! $repo) {
return null;
}
$localCheck = $this->checkLocalSourceUpdates();
if ($localCheck !== null) {
return $localCheck;
}
return $this->checkRemoteSourceUpdates($repo, $branch);
}
public function isSourceBuildAvailable(): bool
{
$hasRepo = ! in_array($this->sourceRepo, [null, '', '0'], true) || ! in_array($this->githubRepo, [null, '', '0'], true);
$hasPath = ! in_array($this->emulatorSourcePath, [null, '', '0'], true);
if (! $hasRepo || ! $hasPath) {
return false;
}
try {
$result = Process::timeout(5)->run("[ -d {$this->emulatorSourcePath} ] && echo 'exists' || echo 'not_exists'");
return trim($result->output()) === 'exists';
} catch (\Exception) {
return false;
}
}
private function checkLocalSourceUpdates(): ?array
{
$sourcePath = $this->emulatorSourcePath;
$existsCheck = Process::timeout(5)->run("[ -d {$sourcePath} ] && echo 'exists'");
if (! $existsCheck->successful() || trim($existsCheck->output()) !== 'exists') {
return $this->checkSourceByCloning();
}
try {
$gitCheck = Process::timeout(5)->run("cd {$sourcePath} && git rev-parse --is-inside-work-tree 2>/dev/null");
if (! $gitCheck->successful()) {
return $this->checkSourceByCloning();
}
$currentCommitResult = Process::timeout(5)->run("cd {$sourcePath} && git rev-parse HEAD");
if (! $currentCommitResult->successful()) {
return null;
}
$currentSha = trim($currentCommitResult->output());
$fetchResult = Process::timeout(30)->run("cd {$sourcePath} && git fetch origin 2>&1");
if ($fetchResult->failed()) {
Log::debug('[EmulatorSource] Git fetch failed, trying local check only');
}
$branch = $this->sourceBranch ?: $this->githubBranch ?: 'main';
$latestResult = Process::timeout(5)->run("cd {$sourcePath} && git rev-parse origin/{$branch} 2>/dev/null || git rev-parse HEAD");
if (! $latestResult->successful()) {
return null;
}
$latestSha = trim($latestResult->output());
$dateResult = Process::timeout(5)->run("cd {$sourcePath} && git log -1 --format='%ci' {$latestSha}");
$latestDate = null;
$latestTimestamp = time();
if ($dateResult->successful()) {
$latestDate = trim($dateResult->output(), "'");
if ($latestDate !== '' && $latestDate !== '0') {
$latestTimestamp = strtotime($latestDate);
}
}
$this->persistSourceCommitInfo($latestSha, $latestTimestamp);
$msgResult = Process::timeout(5)->run("cd {$sourcePath} && git log -1 --format='%s' {$latestSha}");
$latestMessage = $msgResult->successful() ? trim($msgResult->output()) : '';
$authorResult = Process::timeout(5)->run("cd {$sourcePath} && git log -1 --format='%an' {$latestSha}");
$latestAuthor = $authorResult->successful() ? trim($authorResult->output()) : '';
$storedSha = $this->settings->getOrDefault('emulator_source_commit', null);
$storedDate = $this->settings->getOrDefault('emulator_source_date', null);
$isUpdate = $storedSha !== null && $storedSha !== $latestSha;
if ($storedSha === null) {
$isUpdate = true;
}
return [
'has_update' => $isUpdate,
'latest_sha' => $latestSha,
'latest_date' => $latestDate,
'latest_timestamp' => $latestTimestamp,
'latest_message' => $latestMessage,
'latest_author' => $latestAuthor,
'stored_sha' => $storedSha,
'stored_date' => $storedDate,
'source' => 'local',
];
} catch (\Exception $e) {
Log::debug('[EmulatorSource] Local source check failed: ' . $e->getMessage());
return null;
}
}
private function checkSourceByCloning(): ?array
{
$repo = $this->sourceRepo ?: $this->githubRepo;
$branch = $this->sourceBranch ?: $this->githubBranch ?: 'main';
if (! $repo) {
return null;
}
$repo = preg_replace('#/tree/[^/]+/.*$#', '', $repo);
$repo = str_replace(['https://github.com/', 'http://github.com/'], '', $repo);
$repo = rtrim($repo, '/');
try {
$tempDir = '/tmp/emulator-source-check-' . Str::random(8);
$cloneResult = Process::timeout(120)->run(
"git clone --branch {$branch} --depth 1 https://github.com/{$repo}.git {$tempDir} 2>&1",
);
if ($cloneResult->failed()) {
return null;
}
$shaResult = Process::timeout(5)->run("cd {$tempDir} && git rev-parse HEAD");
$latestSha = $shaResult->successful() ? trim($shaResult->output()) : '';
$dateResult = Process::timeout(5)->run("cd {$tempDir} && git log -1 --format='%ci'");
$latestDate = null;
$latestTimestamp = time();
if ($dateResult->successful()) {
$latestDate = trim($dateResult->output());
if ($latestDate !== '' && $latestDate !== '0') {
$latestTimestamp = strtotime($latestDate);
}
}
$this->persistSourceCommitInfo($latestSha, $latestTimestamp);
$msgResult = Process::timeout(5)->run("cd {$tempDir} && git log -1 --format='%s'");
$latestMessage = $msgResult->successful() ? trim($msgResult->output()) : '';
$authorResult = Process::timeout(5)->run("cd {$tempDir} && git log -1 --format='%an'");
$latestAuthor = $authorResult->successful() ? trim($authorResult->output()) : '';
Process::timeout(10)->run("rm -rf {$tempDir}");
$storedSha = $this->settings->getOrDefault('emulator_source_commit', null);
$storedDate = $this->settings->getOrDefault('emulator_source_date', null);
$isUpdate = $storedSha !== null && $storedSha !== $latestSha;
if ($storedSha === null) {
$isUpdate = true;
}
return [
'has_update' => $isUpdate,
'latest_sha' => $latestSha,
'latest_date' => $latestDate,
'latest_timestamp' => $latestTimestamp,
'latest_message' => $latestMessage,
'latest_author' => $latestAuthor,
'stored_sha' => $storedSha,
'stored_date' => $storedDate,
'source' => 'cloned',
];
} catch (\Exception $e) {
Log::debug('[EmulatorSource] Source clone check failed: ' . $e->getMessage());
return null;
}
}
private function checkRemoteSourceUpdates(string $repo, string $branch): ?array
{
try {
$response = Http::timeout(10)
->withHeaders([
'Accept' => 'application/vnd.github.v3+json',
'User-Agent' => 'AtomCMS-Emulator-Updater',
])
->get("https://api.github.com/repos/{$repo}/commits", [
'sha' => $branch,
'per_page' => 1,
]);
if (! $response->successful()) {
return null;
}
$commits = $response->json();
if (empty($commits) || ! isset($commits[0]['sha'])) {
return null;
}
$latestCommit = $commits[0];
$latestSha = $latestCommit['sha'];
$latestDate = $latestCommit['commit']['committer']['date'] ?? null;
$latestTimestamp = $latestDate ? strtotime((string) $latestDate) : time();
$this->persistSourceCommitInfo($latestSha, $latestTimestamp);
$installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null);
$storedSha = $this->settings->getOrDefault('emulator_source_commit', null);
$storedDate = $this->settings->getOrDefault('emulator_source_date', null);
if ($storedSha !== null && $storedSha === $latestSha) {
$isUpdate = false;
} elseif ($storedSha !== null && $storedSha !== $latestSha) {
$isUpdate = true;
} elseif ($installedDate !== null) {
$installedTimestamp = is_numeric($installedDate) ? (int) $installedDate : strtotime((string) $installedDate);
$isUpdate = $installedTimestamp < $latestTimestamp;
} else {
$isUpdate = false;
}
return [
'has_update' => $isUpdate,
'latest_sha' => $latestSha,
'latest_date' => $latestDate,
'latest_timestamp' => $latestTimestamp,
'latest_message' => $latestCommit['commit']['message'] ?? '',
'latest_author' => $latestCommit['commit']['author']['name'] ?? '',
'stored_sha' => $storedSha,
'stored_date' => $storedDate,
];
} catch (\Exception $e) {
Log::warning('[EmulatorSource] Could not check source updates via GitHub API', ['error' => $e->getMessage()]);
return null;
}
}
private function persistSourceCommitInfo(string $sha, int $timestamp): void
{
$this->settings->set('emulator_source_commit', $sha);
$this->settings->set('emulator_source_date', (string) $timestamp);
}
}
+510
View File
@@ -0,0 +1,510 @@
<?php
declare(strict_types=1);
namespace App\Services\Emulator;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
class EmulatorSqlService
{
use EmulatorConfiguration;
private const string SQL_TABLE = 'emulator_sql_updates';
public function __construct()
{
$this->loadConfiguration();
}
public function isConfigured(): bool
{
return ! in_array($this->githubUrl, [null, '', '0'], true) || ! in_array($this->jarDirectUrl, [null, '', '0'], true);
}
public function checkForUpdates(bool $recentOnly = true): array
{
if (! $this->githubRepo) {
return [
'has_updates' => false,
'error' => 'Geen GitHub repo geconfigureerd',
];
}
$this->ensureSqlTableExists();
$result = $this->fetchSqlFilesFromGitHub($recentOnly);
if (isset($result['error'])) {
return [
'has_updates' => false,
'error' => $result['error'],
];
}
$sqlFiles = $result;
if ($sqlFiles === []) {
return [
'has_updates' => false,
'message' => $recentOnly
? 'Geen SQL updates van de afgelopen week gevonden'
: 'Geen SQL updates gevonden',
];
}
$appliedHashes = $this->getAppliedSqlHashes();
$newSqlFiles = [];
$alreadyApplied = [];
foreach ($sqlFiles as $file) {
$hash = $file['sha'] ?? md5((string) $file['name']);
if (in_array($hash, $appliedHashes)) {
$alreadyApplied[] = $file['name'];
continue;
}
$newSqlFiles[] = $file;
}
if ($newSqlFiles === []) {
return [
'has_updates' => false,
'message' => 'Alle SQL updates zijn al toegepast (' . count($alreadyApplied) . ' stuks)',
'applied_count' => count($alreadyApplied),
];
}
usort($newSqlFiles, fn ($a, $b) => strcmp((string) $a['name'], (string) $b['name']));
return [
'has_updates' => true,
'count' => count($newSqlFiles),
'files' => $newSqlFiles,
'message' => count($newSqlFiles) . ' nieuwe SQL update(s) van deze week',
'applied_count' => count($alreadyApplied),
];
}
public function runUpdates(): array
{
$sqlCheck = $this->checkForUpdates(false);
if (! ($sqlCheck['has_updates'] ?? false)) {
return ['success' => true, 'sql_updated' => false, 'message' => 'Geen nieuwe SQL updates'];
}
$results = [
'success' => true,
'sql_updated' => true,
'files_run' => [],
'errors' => [],
];
foreach ($sqlCheck['files'] as $file) {
$sqlResult = $this->downloadAndRunSql($file);
if ($sqlResult['success']) {
$results['files_run'][] = $file['name'];
$this->markSqlAsApplied($file);
} else {
$results['errors'][] = $file['name'] . ': ' . $sqlResult['error'];
}
}
if (count($results['errors']) > 0) {
$results['message'] = count($results['files_run']) . ' SQL updates succesvol, ' . count($results['errors']) . ' met fouten';
} else {
$results['message'] = count($results['files_run']) . ' SQL updates succesvol uitgevoerd!';
}
return $results;
}
public function getAppliedUpdates(): array
{
$this->ensureSqlTableExists();
return DB::table(self::SQL_TABLE)
->orderBy('applied_at', 'desc')
->get()
->toArray();
}
public function diagnose(): array
{
$diagnosis = [
'table_exists' => false,
'applied_count' => 0,
'pending_count' => 0,
'error' => null,
];
try {
$diagnosis['table_exists'] = Schema::hasTable(self::SQL_TABLE);
if ($diagnosis['table_exists']) {
$diagnosis['applied_count'] = DB::table(self::SQL_TABLE)->count();
}
if ($this->githubRepo && $diagnosis['table_exists']) {
$sqlCheck = $this->checkForUpdates(false);
if (isset($sqlCheck['count'])) {
$diagnosis['pending_count'] = $sqlCheck['count'];
}
}
} catch (\Exception $e) {
$diagnosis['error'] = $e->getMessage();
}
return $diagnosis;
}
public function repair(): array
{
$actions = [];
$errors = [];
try {
$this->ensureSqlTableExists();
$actions[] = 'SQL update tabel gecontroleerd';
$appliedCount = DB::table(self::SQL_TABLE)->count();
$actions[] = "SQL updates tabel OK ({$appliedCount} records)";
if ($this->githubRepo) {
$sqlCheck = $this->checkForUpdates(false);
if (isset($sqlCheck['error'])) {
$actions[] = 'SQL update check: ' . $sqlCheck['error'];
} elseif (! ($sqlCheck['has_updates'] ?? false)) {
$actions[] = 'SQL updates: ' . ($sqlCheck['message'] ?? 'Allemaal up-to-date');
} else {
$count = $sqlCheck['count'] ?? 0;
$actions[] = "{$count} nieuwe SQL updates beschikbaar";
if ($this->isConfigured()) {
$actions[] = 'SQL updates worden toegepast...';
$runResult = $this->runUpdates();
if ($runResult['success'] ?? false) {
$filesRun = count($runResult['files_run'] ?? []);
$actions[] = "{$filesRun} SQL updates toegepast";
} else {
$errors[] = 'SQL updates: ' . ($runResult['error'] ?? 'Onbekende fout');
}
}
}
}
} catch (\Exception $e) {
$errors[] = 'SQL repair: ' . $e->getMessage();
Log::error('[EmulatorSql] Repair failed', ['error' => $e->getMessage()]);
}
return [
'success' => $errors === [],
'actions' => $actions,
'errors' => $errors,
];
}
private function ensureSqlTableExists(): void
{
if (Schema::hasTable(self::SQL_TABLE)) {
return;
}
Schema::create(self::SQL_TABLE, function ($table) {
$table->id();
$table->string('file_name');
$table->string('file_hash')->unique();
$table->timestamp('applied_at');
$table->text('sql_content')->nullable();
});
}
private function getAppliedSqlHashes(): array
{
$this->ensureSqlTableExists();
return DB::table(self::SQL_TABLE)
->pluck('file_hash')
->toArray();
}
private function markSqlAsApplied(array $file): void
{
$this->ensureSqlTableExists();
$hash = $file['sha'] ?? md5((string) $file['name']);
DB::table(self::SQL_TABLE)->updateOrInsert(
['file_hash' => $hash],
[
'file_name' => $file['name'],
'applied_at' => now(),
],
);
}
private function fetchSqlFilesFromGitHub(bool $recentOnly = false): array
{
if (! $this->githubRepo) {
return [];
}
$branch = $this->githubBranch ?: 'main';
$folderNames = [
'Database%20Updates',
'Database Updates',
'database_updates',
'database/updates',
'sql/updates',
'sql',
'updates',
];
$knownSqlFiles = [
'07012026_UpdateDatabase_to_4-0-1.sql',
'09012026_UpdateDatabase_to_4-0-2.sql',
'12012026_Battle Banzai.sql',
'12012026_Breeding Fixes.sql',
'12012026_ChatBubbles.sql',
'16032026_updateall_command.sql',
'17032026_allow_underpass.sql',
'19032026_hotel_timezone.sql',
'21022026_user_prefixes.sql',
'Default_Camera.sql',
'UpdateDatabase_Allow_diagonale.sql',
'UpdateDatabase_BOT.sql',
'UpdateDatabase_Banners.sql',
'UpdateDatabase_DanceCMD.sql',
'UpdateDatabase_Happiness.sql',
'UpdateDatabase_Websocket.sql',
'UpdateDatabase_unignorable.sql',
];
try {
if ($recentOnly) {
return $this->fetchRecentSqlFiles($branch);
}
$sqlFiles = [];
foreach ($knownSqlFiles as $filename) {
foreach ($folderNames as $folderName) {
$encodedFilename = str_replace(' ', '%20', $filename);
$url = "https://raw.githubusercontent.com/{$this->githubRepo}/{$branch}/{$folderName}/{$encodedFilename}";
$response = Http::timeout(10)->get($url);
if ($response->successful()) {
$sqlFiles[] = [
'name' => $filename,
'url' => $url,
'sha' => md5($response->body()),
];
break;
}
}
}
if ($sqlFiles !== []) {
usort($sqlFiles, fn ($a, $b) => strcmp($a['name'], $b['name']));
return $sqlFiles;
}
return [];
} catch (\Exception $e) {
Log::warning('[EmulatorSql] Could not fetch SQL files', ['error' => $e->getMessage()]);
return [];
}
}
private function fetchRecentSqlFiles(string $branch): array
{
$weekAgo = now()->subDays(7)->toIso8601String();
$githubToken = setting('github_token', '');
$headers = [
'Accept' => 'application/vnd.github+json',
'User-Agent' => 'AtomCMS-EmulatorUpdate/1.0',
];
if (! empty($githubToken)) {
$headers['Authorization'] = 'Bearer ' . $githubToken;
}
$folderNames = ['Database Updates', 'database_updates', 'sql', 'updates'];
try {
foreach ($folderNames as $folderName) {
$response = Http::withHeaders($headers)->timeout(30)->get("https://api.github.com/repos/{$this->githubRepo}/commits", [
'path' => $folderName,
'sha' => $branch,
'since' => $weekAgo,
'per_page' => 100,
]);
if ($response->successful()) {
$commits = $response->json();
if (! is_array($commits) || $commits === []) {
continue;
}
$sqlFiles = [];
foreach ($commits as $commit) {
$sha = $commit['sha'] ?? null;
$commitDate = $commit['commit']['committer']['date'] ?? null;
if (! $sha) {
continue;
}
$commitResponse = Http::withHeaders($headers)->timeout(30)->get("https://api.github.com/repos/{$this->githubRepo}/commits/{$sha}");
if (! $commitResponse->successful()) {
continue;
}
$commitData = $commitResponse->json();
$files = $commitData['files'] ?? [];
foreach ($files as $file) {
$filename = $file['filename'] ?? '';
if (! str_ends_with(strtolower((string) $filename), '.sql')) {
continue;
}
$name = basename((string) $filename);
$sqlFiles[] = [
'name' => $name,
'url' => $file['raw_url'] ?? "https://raw.githubusercontent.com/{$this->githubRepo}/{$sha}/{$filename}",
'sha' => $sha,
'date' => $commitDate,
];
}
}
if ($sqlFiles !== []) {
usort($sqlFiles, fn ($a, $b) => strcmp($a['name'], $b['name']));
return $sqlFiles;
}
}
}
return [];
} catch (\Exception $e) {
Log::warning('[EmulatorSql] Could not fetch recent SQL files', ['error' => $e->getMessage()]);
return [];
}
}
private function downloadAndRunSql(array $file): array
{
try {
$response = Http::timeout(60)->get($file['url']);
if (! $response->successful()) {
return ['success' => false, 'error' => 'Download mislukt'];
}
$sql = $this->cleanSql($response->body());
if (in_array(trim($sql), ['', '0'], true)) {
return ['success' => true, 'message' => 'Lege SQL file overgeslagen'];
}
$host = setting('emulator_database_host', config('database.connections.emulator.host', '127.0.0.1'));
$port = setting('emulator_database_port', config('database.connections.emulator.port', '3306'));
$name = setting('emulator_database_name', config('database.connections.emulator.database', ''));
$username = setting('emulator_database_username', config('database.connections.emulator.username', ''));
$password = setting('emulator_database_password', config('database.connections.emulator.password', ''));
if (empty($name) || empty($username)) {
return ['success' => false, 'error' => 'Emulator database niet geconfigureerd'];
}
$pdo = new \PDO(
"mysql:host={$host};port={$port};dbname={$name};charset=utf8mb4",
$username,
$password,
[\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION],
);
$statements = $this->splitSqlStatements($sql);
$successCount = 0;
$skipCount = 0;
foreach ($statements as $statement) {
$statement = trim((string) $statement);
if ($statement === '' || $statement === '0') {
continue;
}
try {
$pdo->exec($statement);
$successCount++;
} catch (\PDOException $e) {
if ($this->isDuplicateError($e)) {
$skipCount++;
continue;
}
Log::warning('[SQL Update] Statement error: ' . $e->getMessage());
}
}
Log::info('[SQL Update] Successfully ran: ' . $file['name'] . " ({$successCount} statements, {$skipCount} skipped)");
return ['success' => true, 'message' => "SQL uitgevoerd ({$successCount} statements, {$skipCount} duplicate)"];
} catch (\Exception $e) {
Log::error('[SQL Update] Failed: ' . $file['name'], ['error' => $e->getMessage()]);
return ['success' => false, 'error' => $e->getMessage()];
}
}
private function cleanSql(string $sql): string
{
$sql = preg_replace('/--.*$/m', '', $sql);
$sql = preg_replace('/#.*$/m', '', (string) $sql);
$sql = preg_replace('/SET FOREIGN_KEY_CHECKS.*?;/i', '', (string) $sql);
return preg_replace('/SET @@SESSION.SQL_MODE.*?;/i', '', (string) $sql);
}
private function splitSqlStatements(string $sql): array
{
$parts = explode(';', $sql);
return array_filter($parts, fn ($part) => trim((string) $part) !== '');
}
private function isDuplicateError(\PDOException $e): bool
{
$message = strtolower($e->getMessage());
$patterns = [
'duplicate entry',
"table '",
'already exists',
"can't create",
'duplicate key',
'duplicate index',
'error 1061',
'error 1062',
'error 1826',
];
return array_any($patterns, fn ($pattern) => str_contains($message, (string) $pattern));
}
}
+144
View File
@@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
namespace App\Services\Emulator;
use App\Services\RconService;
use Illuminate\Support\Facades\Process;
class EmulatorStatusService
{
use EmulatorConfiguration;
public function __construct()
{
$this->loadConfiguration();
}
public function getStatus(): array
{
$rconService = new RconService;
return [
'is_connected' => $rconService->isConnected(),
'service_running' => $this->isServiceRunning(),
'jar_exists' => $this->commandFileExists($this->jarPath . '/*.jar'),
'jar_files' => $this->getJarFiles(),
'source_exists' => $this->commandDirExists($this->emulatorSourcePath),
'emulator_db_connected' => $this->testEmulatorDbConnection(),
'jar_path' => $this->jarPath,
'source_path' => $this->emulatorSourcePath,
'service_name' => $this->emulatorService,
];
}
public function isServiceRunning(): bool
{
try {
$result = Process::timeout(5)->run('systemctl is-active ' . escapeshellarg((string) $this->emulatorService) . ' 2>/dev/null');
return trim($result->output()) === 'active';
} catch (\Exception) {
return false;
}
}
public function testEmulatorDbConnection(): bool
{
try {
$host = $this->settings->getOrDefault('emulator_database_host', config('database.connections.mysql.host', '127.0.0.1'));
$port = $this->settings->getOrDefault('emulator_database_port', config('database.connections.mysql.port', '3306'));
$name = $this->settings->getOrDefault('emulator_database_name', '');
$username = $this->settings->getOrDefault('emulator_database_username', '');
$password = $this->settings->getOrDefault('emulator_database_password', '');
if (empty($name) || empty($username)) {
return false;
}
$result = Process::timeout(5)->run(
'mysql -h ' . escapeshellarg((string) $host) .
' -P ' . escapeshellarg((string) $port) .
' -u ' . escapeshellarg((string) $username) .
' -p' . escapeshellarg((string) $password) .
' -e "SELECT 1" ' . escapeshellarg((string) $name) . ' 2>/dev/null | head -1',
);
return $result->successful();
} catch (\Exception) {
return false;
}
}
public function getJarFiles(): array
{
$result = Process::timeout(5)->run('ls -1 ' . escapeshellarg((string) $this->jarPath) . '/*.jar 2>/dev/null | head -5');
if ($result->successful()) {
$files = array_filter(explode("\n", trim($result->output())));
return array_map(basename(...), $files);
}
return [];
}
public function getInstalledVersion(): string
{
return setting('emulator_version', 'Onbekend');
}
public function getInstalledJar(): ?string
{
try {
$result = Process::timeout(5)->run("ls -1 {$this->jarPath}/*.jar 2>/dev/null | head -1");
if ($result->successful()) {
$jarPath = trim($result->output());
if ($jarPath !== '' && $jarPath !== '0' && str_contains($jarPath, '.jar')) {
return basename($jarPath);
}
}
} catch (\Exception) {
}
return setting('emulator_version', 'Onbekend') !== 'Onbekend'
? 'Emulator v' . setting('emulator_version')
: null;
}
public function getInstalledJarInfo(): array
{
$files = [];
try {
$result = Process::timeout(5)->run("ls -1lh {$this->jarPath}/*.jar 2>/dev/null");
if ($result->successful()) {
$lines = array_filter(explode("\n", trim($result->output())));
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line === '0') {
continue;
}
$parts = preg_split('/\s+/', $line);
$filename = end($parts);
if (str_contains((string) $filename, '.jar')) {
$size = $parts[4] ?? '?';
$files[] = [
'name' => basename((string) $filename),
'size' => $size,
];
}
}
}
} catch (\Exception) {
}
return $files;
}
public function isConfigured(): bool
{
return ! in_array($this->githubUrl, [null, '', '0'], true) || ! in_array($this->jarDirectUrl, [null, '', '0'], true);
}
}
File diff suppressed because it is too large Load Diff