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->databaseHost = config('database.connections.mysql.host'); $this->databasePort = config('database.connections.mysql.port', '3306'); $this->databaseName = config('database.connections.mysql.database'); $this->databaseUsername = config('database.connections.mysql.username'); $this->databasePassword = config('database.connections.mysql.password'); $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 { $this->detectBasePath(); $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"); $this->ensureSudoAccess(); } 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 ensureSudoAccess(): void { $sudoersFile = '/etc/sudoers.d/atomcms-emulator'; $webUser = 'www-data'; $content = "# Auto-generated by AtomCMS - Do not edit\n"; $content .= "{$webUser} ALL=(ALL) NOPASSWD: /bin/systemctl start *, /bin/systemctl stop *, /bin/systemctl restart *, /usr/bin/systemctl start *, /usr/bin/systemctl stop *, /usr/bin/systemctl restart *\n"; // Check if file exists using command line (avoids open_basedir) $checkResult = Process::timeout(5)->run("test -f {$sudoersFile} && echo 'exists' || echo 'missing'"); $fileExists = trim($checkResult->output()) === 'exists'; if (! $fileExists) { Process::timeout(5)->run("echo '{$content}' | sudo tee {$sudoersFile} > /dev/null 2>&1"); Process::timeout(5)->run("sudo chmod 0440 {$sudoersFile} 2>/dev/null"); } } private function ensureJavaInstalled(): array { $actions = []; $errors = []; $javaCheck = Process::timeout(5)->run('java -version 2>&1'); if (! $javaCheck->successful()) { $errors[] = 'Java niet geïnstalleerd'; Log::warning('[EmulatorUpdate] Java not found, attempting to install'); $installResult = Process::timeout(180)->run('apt-get update && apt-get install -y default-jdk 2>&1'); if ($installResult->successful()) { $actions[] = 'Java (laatste versie) geïnstalleerd'; } else { $errors[] = 'Kon Java niet installeren'; } } else { $javaVersion = explode("\n", $javaCheck->errorOutput())[0] ?? 'Java gevonden'; $actions[] = trim($javaVersion); } $mavenCheck = Process::timeout(5)->run('which mvn'); if (! $mavenCheck->successful()) { Log::warning('[EmulatorUpdate] Maven not found, attempting to install'); $installResult = Process::timeout(180)->run('apt-get install -y maven 2>&1'); if ($installResult->successful()) { $actions[] = 'Maven geïnstalleerd'; } } else { $actions[] = 'Maven gevonden'; } $gradleCheck = Process::timeout(5)->run('which gradle'); if (! $gradleCheck->successful()) { Log::warning('[EmulatorUpdate] Gradle not found, will use wrapper if available'); } else { $actions[] = 'Gradle gevonden'; } return [ 'actions' => $actions, 'errors' => $errors, ]; } public function isConfigured(): bool { return ! in_array($this->githubUrl, [null, '', '0'], true) || ! in_array($this->jarDirectUrl, [null, '', '0'], true); } public function getStatus(): array { $rconService = new RconService; // Check basic status $isConnected = $rconService->isConnected(); // Check for updates $updateCheck = $this->checkForUpdates(); // Check JAR file $jarExists = $this->commandFileExists($this->jarPath . '/*.jar'); $jarFiles = $this->getJarFiles(); // Check source $sourceExists = $this->commandDirExists($this->emulatorSourcePath); // Check service $serviceRunning = $this->isServiceRunning(); // Check emulator database $emulatorDbConnected = $this->testEmulatorDbConnection(); // Get commits $sourceInfo = $this->checkForSourceUpdates(); return [ 'is_connected' => $isConnected, 'service_running' => $serviceRunning, 'jar_exists' => $jarExists, 'jar_files' => $jarFiles, 'source_exists' => $sourceExists, 'source_info' => $sourceInfo, 'update_available' => $updateCheck['update_available'] ?? false, 'current_version' => $updateCheck['current_version'] ?? setting('emulator_version', 'N/A'), 'latest_version' => $updateCheck['latest_version'] ?? 'N/A', 'update_type' => $updateCheck['type'] ?? 'unknown', 'has_source_updates' => $sourceInfo['has_update'] ?? false, 'latest_sha' => $sourceInfo['latest_sha'] ?? null, 'latest_message' => $sourceInfo['latest_message'] ?? null, 'latest_author' => $sourceInfo['latest_author'] ?? null, 'latest_date' => $sourceInfo['latest_date'] ?? null, 'stored_sha' => $sourceInfo['stored_sha'] ?? null, 'stored_date' => $sourceInfo['stored_date'] ?? null, 'jar_path' => $this->jarPath, 'source_path' => $this->emulatorSourcePath, 'service_name' => $this->emulatorService, 'emulator_db_connected' => $emulatorDbConnected, ]; } private function testEmulatorDbConnection(): bool { try { $result = Process::timeout(5)->run('mysql -h ' . escapeshellarg((string) $this->databaseHost) . ' -P ' . escapeshellarg((string) $this->databasePort) . ' -u ' . escapeshellarg((string) $this->databaseUsername) . ' -p' . escapeshellarg((string) $this->databasePassword) . ' -e "SELECT 1" ' . escapeshellarg((string) $this->databaseName) . ' 2>/dev/null | head -1'); return $result->successful(); } catch (\Exception) { return false; } } private 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; } } private 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 checkForUpdates(): array { if (! $this->isConfigured()) { return [ 'update_available' => false, 'error' => 'Configureer een GitHub URL of directe .jar URL', ]; } // Check for source updates FIRST (independent of JAR method) $sourceInfo = $this->checkForSourceUpdates(); $hasSourceUpdates = $sourceInfo && $sourceInfo['has_update']; if (! in_array($this->jarDirectUrl, [null, '', '0'], true)) { $jarInfo = $this->validateDirectUrl($this->jarDirectUrl); if ($jarInfo) { // Update the stored version if we're checking a direct URL if (! empty($jarInfo['version'])) { $currentVersion = $this->settings->getOrDefault('emulator_version', '0.0.0'); if ($jarInfo['version'] !== $currentVersion) { $this->settings->set('emulator_version', $jarInfo['version']); } } // Check if source has updates even if JAR doesn't $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, ]; } // URL not reachable, but check source updates 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', ]; } $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']; $commitDate = $jarInfo['commit_date'] ?? null; $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' => $commitDate, 'source_info' => $sourceInfo, 'has_source_updates' => $hasSourceUpdates, ]; } public function checkForSqlUpdates(bool $recentOnly = true): array { if (! $this->githubRepo) { return [ 'has_updates' => false, 'error' => 'Geen GitHub repo geconfigureerd', ]; } $this->ensureSqlTableExists(); $result = $this->fetchSqlFilesFromGitHub($recentOnly); // Check if result is an error response 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), ]; } private function ensureSqlTableExists(): void { $tableName = self::SQL_TABLE; if (Schema::hasTable($tableName)) { return; } Schema::create($tableName, 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 getAppliedSqlFiles(): array { $this->ensureSqlTableExists(); return DB::table(self::SQL_TABLE) ->orderBy('applied_at', 'desc') ->get() ->toArray(); } public function runSqlUpdates(): array { $sqlCheck = $this->checkForSqlUpdates(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 (isset($results['errors']) && $results['errors'] !== []) { $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!'; } if (isset($results['files_run']) && $results['files_run'] !== [] && $this->restartEmulator()) { $results['message'] .= ' Emulator herstart.'; } return $results; } public function restartEmulator(): bool { $serviceName = $this->emulatorService; try { Log::info('[EmulatorUpdate] Restarting emulator service: ' . $serviceName); $commands = [ "sudo systemctl restart {$serviceName} 2>&1", "sudo service {$serviceName} restart 2>&1", "sudo /etc/init.d/{$serviceName} restart 2>&1", "sudo systemctl stop {$serviceName} && sudo systemctl start {$serviceName} 2>&1", ]; foreach ($commands as $cmd) { $output = shell_exec($cmd); if (! str_contains($output ?? '', 'error') && ! str_contains($output ?? '', 'failed')) { Log::info('[EmulatorUpdate] Emulator restarted successfully'); return true; } } Log::warning('[EmulatorUpdate] All restart methods failed, trying pgrep'); $pidCheck = shell_exec("pgrep -f '{$serviceName}'"); if (! in_array(trim($pidCheck ?? ''), ['', '0'], true)) { $pids = explode("\n", trim($pidCheck)); foreach ($pids as $pid) { shell_exec("kill -9 {$pid} 2>&1"); } sleep(2); } $startCommands = [ "sudo systemctl start {$serviceName} 2>&1", "sudo service {$serviceName} start 2>&1", ]; foreach ($startCommands as $cmd) { $output = shell_exec($cmd); if (! str_contains($output ?? '', 'error') && ! str_contains($output ?? '', 'failed')) { Log::info('[EmulatorUpdate] Emulator started successfully'); return true; } } return false; } catch (\Exception $e) { Log::error('[EmulatorUpdate] Failed to restart emulator', ['error' => $e->getMessage()]); return false; } } 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(), ], ); } public function getAppliedSqlUpdates(): array { $this->ensureSqlTableExists(); return $this->getAppliedSqlFiles(); } public function updateEmulator(): array { if (! $this->isConfigured()) { return ['success' => false, 'error' => 'Geen GitHub URL geconfigureerd']; } $check = $this->checkForUpdates(); if (! ($check['update_available'] ?? false)) { if ($check['type'] === 'not_found' && ($check['source_available'] ?? false)) { return $this->buildFromSource(); } return ['success' => false, 'error' => 'Emulator is al up-to-date']; } // Build from source if there are source updates (regardless of type) $hasSourceUpdates = ($check['has_source_updates'] ?? false) || ($check['type'] ?? '') === 'source_build'; if ($hasSourceUpdates && $this->isSourceBuildAvailable()) { return $this->buildFromSource(); } if ($check['type'] === 'source_build') { return $this->buildFromSource(); } if (! ($check['jar_url'] ?? null)) { return ['success' => false, 'error' => 'Geen .jar gevonden']; } $result = $this->performUpdate($check); if ($result['success']) { $this->runSqlUpdates(); if ($this->restartEmulator()) { $result['restarted'] = true; $result['message'] = ($result['message'] ?? '') . ' | 🔄 Emulator herstart'; } } return $result; } private function downloadAndRunSql(array $file): array { try { $response = Http::timeout(60)->get($file['url']); if (! $response->successful()) { return ['success' => false, 'error' => 'Download mislukt']; } $sql = $response->body(); $sql = $this->cleanSql($sql); if (in_array(trim($sql), ['', '0'], true)) { return ['success' => true, 'message' => 'Lege SQL file overgeslagen']; } // Get database credentials from settings or .env $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. Vul de database gegevens in bij de instellingen.']; } $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)); } private function fetchSqlFilesFromGitHub(bool $recentOnly = false): array { if (! $this->githubRepo) { return []; } $branch = $this->githubBranch ?: 'main'; // Try multiple possible folder names (with URL encoding) $folderNames = [ 'Database%20Updates', 'Database Updates', 'database_updates', 'database/updates', 'sql/updates', 'sql', 'updates', ]; // Known SQL files in Arcturus-Morningstar-Extended repository // We check raw GitHub content which has no rate limit $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 = []; // Check each known file exists via raw GitHub (no rate limit) 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; // Found in this folder, move to next file } } } if ($sqlFiles !== []) { usort($sqlFiles, fn ($a, $b) => strcmp($a['name'], $b['name'])); return $sqlFiles; } return []; } catch (\Exception $e) { Log::warning('[EmulatorUpdate] Could not fetch SQL files', ['error' => $e->getMessage()]); return []; } } private function fetchRecentSqlFiles(string $branch): array { $weekAgo = now()->subDays(7)->toIso8601String(); // Get GitHub token from settings if available $githubToken = setting('github_token', ''); $headers = [ 'Accept' => 'application/vnd.github+json', 'User-Agent' => 'AtomCMS-EmulatorUpdate/1.0', ]; if (! empty($githubToken)) { $headers['Authorization'] = 'Bearer ' . $githubToken; } // Try multiple folder names $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'])); Log::info('[EmulatorUpdate] Found recent SQL files in folder: ' . $folderName, ['count' => count($sqlFiles)]); return $sqlFiles; } } } return []; } catch (\Exception $e) { Log::warning('[EmulatorUpdate] Could not fetch recent SQL files', ['error' => $e->getMessage()]); return []; } } 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; // Build update script with comprehensive fallbacks $updateScript = <</dev/null | tail -1 | awk '{print \$4}') if [ "\$FREE_MB" -lt 500 ] 2>/dev/null; then echo "WARNING: Low disk space (\${FREE_MB}MB), cleaning temp..." rm -rf /tmp/emulator-update-* /tmp/nitro_* /tmp/nitro-switch-* 2>/dev/null || true fi mkdir -p "\$TEMP_DIR" "\$JAR_PATH" # Backup existing JAR 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 echo "Existing JAR backed up" fi # Download with retries download_success=false for attempt in 1 2 3; do echo "Download attempt \$attempt/3..." if curl -L --max-time 300 --retry 3 --retry-delay 5 -o "\$TEMP_JAR" "\$JAR_URL" 2>&1; then # Validate it's actually a JAR FILE_TYPE=\$(file -b "\$TEMP_JAR" 2>/dev/null) JAR_SIZE=\$(stat -c%s "\$TEMP_JAR" 2>/dev/null || echo 0) echo "Downloaded: \$JAR_SIZE bytes, type: \$FILE_TYPE" if echo "\$FILE_TYPE" | grep -qi "zip\|jar\|archive" && [ "\$JAR_SIZE" -gt 1000 ]; then download_success=true break else echo "Invalid file (not a JAR), retrying..." rm -f "\$TEMP_JAR" 2>/dev/null || true sleep 3 fi else echo "Curl failed, retrying in 5s..." sleep 5 fi done if [ "\$download_success" = false ]; then echo "ERROR: Download failed after 3 attempts" # Restore backup if ls "\$JAR_PATH/backup"/*.jar 1>/dev/null 2>&1; then mv "\$JAR_PATH/backup"/*.jar "\$JAR_PATH/" 2>/dev/null || true echo "Backup restored" fi rm -rf "\$TEMP_DIR" exit 1 fi # Move JAR to target mv "$TEMP_JAR" "$JAR_PATH/" chown -R www-data:www-data "$JAR_PATH" chmod 755 "$JAR_PATH/$JAR_NAME" echo "JAR installed: $JAR_PATH/$JAR_NAME" # Restart service (try all methods) service_restarted=false for method in "systemctl restart \"\$SERVICE\"" "service \"\$SERVICE\" restart" "/etc/init.d/\$SERVICE restart" "kill -HUP \$(pgrep -f \"\$SERVICE\")" ; do echo "Trying: \$method" eval "\$method" 2>/dev/null && { service_restarted=true; break; } || true done if [ "\$service_restarted" = false ]; then echo "WARNING: Service restart may have failed (check manually)" >&2 exit 1 fi # Cleanup rm -rf "\$TEMP_DIR" 2>/dev/null || true echo "" echo "=== Update complete ===" BASH; $scriptPath = '/tmp/emulator_update_' . uniqid() . '.sh'; file_put_contents($scriptPath, $updateScript); chmod($scriptPath, 0755); Log::info('[EmulatorUpdate] Starting update', [ 'version' => $version, 'jar' => $jarName, 'url' => $jarUrl, ]); try { $result = Process::timeout(600)->run('bash ' . $scriptPath . ' 2>&1'); @unlink($scriptPath); Log::info('[EmulatorUpdate] Update exitCode', ['exitCode' => $result->exitCode()]); if ($result->exitCode() !== 0) { Log::error('[EmulatorUpdate] Update failed', [ 'output' => $result->output(), 'error' => $result->errorOutput(), ]); return [ 'success' => false, 'error' => 'Update mislukt: ' . substr($result->output(), 0, 300), ]; } setting('emulator_version', $version); // Store the commit date as installed date $commitDate = $check['commit_date'] ?? time(); $this->settings->set('emulator_jar_installed_date', (string) $commitDate); $this->settings->set('emulator_jar_commit', $check['commit'] ?? null); // Also track source commit info for update detection $sourceSha = $check['source_info']['latest_sha'] ?? $check['commit'] ?? null; $sourceDate = $check['source_info']['latest_timestamp'] ?? $commitDate ?? null; if ($sourceSha) { $this->settings->set('emulator_source_commit', $sourceSha); } if ($sourceDate) { $this->settings->set('emulator_source_date', (string) $sourceDate); } // Track which branch the update came from $currentBranch = $this->sourceBranch ?: $this->githubBranch ?: 'main'; $this->settings->set('emulator_installed_branch', $currentBranch); Log::info('[EmulatorUpdate] 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('[EmulatorUpdate] Exception', ['error' => $e->getMessage()]); return [ 'success' => false, 'error' => $e->getMessage(), ]; } } 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'); // Try to get commit info from GitHub API for GitHub raw URLs $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(); // Check if we got valid data (not a rate limit error) if (isset($data['sha']) && ! isset($data['message'])) { $gitHubInfoAvailable = true; $commitSha = $data['sha']; // Try to get commit date if (isset($data['commit']['committer']['date'])) { $lastModified = strtotime($data['commit']['committer']['date']); } } } } catch (\Exception) { Log::debug('[EmulatorUpdate] Could not fetch GitHub commit info for direct URL'); } } // Fallback: try HEAD request if ($lastModified === null) { $response = Http::timeout(10)->head($url); if ($response->successful()) { $modifiedSince = $response->header('Last-Modified'); if ($modifiedSince) { $lastModified = strtotime($modifiedSince); // Verify strtotime succeeded if ($lastModified === false) { $lastModified = null; } } } } // Check if we have stored info and if this is actually newer $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)) { // Compare by SHA (most reliable when GitHub API available) if ($gitHubInfoAvailable && $commitSha !== null && isset($storedDataArray['commit_sha'])) { $isUpdate = $commitSha !== $storedDataArray['commit_sha']; } // Fallback to date comparison elseif ($lastModified !== null && $installedDate !== null) { $isUpdate = (int) $installedDate < $lastModified; } elseif ($lastModified !== null && isset($storedDataArray['last_modified'])) { $isUpdate = $lastModified > $storedDataArray['last_modified']; } // Fallback to version comparison from filename elseif (! in_array($version, ['', '0', $storedVersion], true)) { $isUpdate = version_compare($version, $storedVersion) > 0; } } } else { // First time check $isUpdate = true; } // Always update our stored info $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)); Log::info('[EmulatorUpdate] Direct URL check: jar=' . $jarName . ', version=' . $version . ', stored=' . $storedVersion . ', sha=' . ($commitSha ?? 'N/A') . ', github=' . ($gitHubInfoAvailable ? 'yes' : 'no') . ', isUpdate=' . ($isUpdate ? 'true' : 'false')); 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('[EmulatorUpdate] Direct URL not reachable', ['error' => $e->getMessage()]); } return null; } private 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 { // First get the file info for download URL $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; } // Get the commit date by fetching the last commit that touched this file $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 $e) { Log::debug('[EmulatorUpdate] Could not fetch commit date for ' . $name); } // Get the installed date (set when actually installed) $installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null); // Also get the last checked date (for debugging) $lastCheckedDate = $this->settings->getOrDefault("emulator_jar_{$name}_date", null); // Update the last checked date if ($commitDate) { $this->settings->set("emulator_jar_{$name}_date", (string) $commitDate); } // Extract version from filename or use commit date $version = $this->extractVersionFromFilename($name); if ($version === '' || $version === '0') { $version = $commitDate ? date('Y.m.d', $commitDate) : date('Y.m.d'); } // Get the installed JAR commit SHA for comparison $installedJarCommit = $this->settings->getOrDefault('emulator_jar_commit', null); // Check if this is actually an update $isUpdate = false; $installedVersion = $this->settings->getOrDefault('emulator_version', '0.0.0'); // First, try to compare by SHA (most reliable) if ($installedJarCommit !== null && $commitSha !== null) { $isUpdate = $installedJarCommit !== $commitSha; } // If SHA comparison isn't possible, fall back to date comparison elseif ($installedDate !== null && $commitDate) { // Compare installed date with latest commit date $isUpdate = (int) $installedDate < $commitDate; } elseif ($installedDate === null && $commitDate) { // Never installed - this is a first install $isUpdate = true; } Log::info('[EmulatorUpdate] Checking update for ' . $name . ': installed=' . ($installedDate ?? 'null') . ', latest=' . ($commitDate ?? 'null') . ', isUpdate=' . ($isUpdate ? 'true' : 'false')); 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('[EmulatorUpdate] Error checking JAR file {$name}', ['error' => $e->getMessage()]); } } return null; } public function checkForSourceUpdates(): ?array { $repo = $this->sourceRepo ?: $this->githubRepo; $branch = $this->sourceBranch ?: $this->githubBranch ?: 'main'; if (! $repo) { return null; } // Try local git check first if source is already cloned $localCheck = $this->checkLocalSourceUpdates(); if ($localCheck !== null) { return $localCheck; } // Fallback to GitHub API 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); // Compare SHAs first - if they're the same, no update if ($storedSha !== null && $storedSha === $latestSha) { $isUpdate = false; } elseif ($storedSha !== null && $storedSha !== $latestSha) { $isUpdate = true; } elseif ($installedDate !== null) { // Fallback to date comparison $installedTimestamp = is_numeric($installedDate) ? (int) $installedDate : strtotime((string) $installedDate); $isUpdate = $installedTimestamp < $latestTimestamp; } else { // Never installed - this is a first install $isUpdate = false; } Log::info('[EmulatorUpdate] Source update check (GitHub API): repo=' . $repo . ', installed=' . ($installedDate ?? 'null') . ', latest=' . $latestTimestamp . ' (' . $latestDate . '), isUpdate=' . ($isUpdate ? 'true' : '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('[EmulatorUpdate] Could not check source updates via GitHub API', ['error' => $e->getMessage()]); return null; } } private function checkLocalSourceUpdates(): ?array { $sourcePath = $this->emulatorSourcePath; // Use command line to check if directory exists (bypasses open_basedir) $existsCheck = Process::timeout(5)->run("[ -d {$sourcePath} ] && echo 'exists'"); if (! $existsCheck->successful() || trim($existsCheck->output()) !== 'exists') { // Try to clone the repo first return $this->checkSourceByCloning(); } try { // Check if it's a git repo $gitCheck = Process::timeout(5)->run("cd {$sourcePath} && git rev-parse --is-inside-work-tree 2>/dev/null"); if (! $gitCheck->successful()) { // Not a git repo - try to clone fresh Log::info('[EmulatorUpdate] Source path exists but not a git repo, will clone fresh'); return $this->checkSourceByCloning(); } // Get current commit $currentCommitResult = Process::timeout(5)->run("cd {$sourcePath} && git rev-parse HEAD"); if (! $currentCommitResult->successful()) { return null; } $currentSha = trim($currentCommitResult->output()); // Get latest commit from remote $fetchResult = Process::timeout(30)->run("cd {$sourcePath} && git fetch origin 2>&1"); if ($fetchResult->failed()) { Log::debug('[EmulatorUpdate] Git fetch failed, trying local check only'); } // Get latest commit on current branch $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()); // Get commit date $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 = ''; if ($msgResult->successful()) { $latestMessage = trim($msgResult->output()); } // Get author $authorResult = Process::timeout(5)->run("cd {$sourcePath} && git log -1 --format='%an' {$latestSha}"); $latestAuthor = ''; if ($authorResult->successful()) { $latestAuthor = trim($authorResult->output()); } $storedSha = $this->settings->getOrDefault('emulator_source_commit', null); $storedDate = $this->settings->getOrDefault('emulator_source_date', null); $installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null); // Check if there's an update - compare stored SHA with latest SHA $isUpdate = false; if ($storedSha !== null && $storedSha !== $latestSha) { $isUpdate = true; } elseif ($storedSha === null) { // First time - consider it an update $isUpdate = true; } Log::info('[EmulatorUpdate] Source update check (local git): stored=' . $storedSha . ', latest=' . $latestSha . ', isUpdate=' . ($isUpdate ? 'true' : 'false')); 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('[EmulatorUpdate] 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) { Log::debug('[EmulatorUpdate] No repo configured for source check'); return null; } // Clean up repo URL if it contains tree/branch info $repo = preg_replace('#/tree/[^/]+/.*$#', '', $repo); $repo = str_replace(['https://github.com/', 'http://github.com/'], '', $repo); $repo = rtrim($repo, '/'); Log::info('[EmulatorUpdate] Checking source updates by cloning', ['repo' => $repo, 'branch' => $branch]); try { $tempDir = '/tmp/emulator-source-check-' . Str::random(8); // Clone with depth 1 $cloneResult = Process::timeout(120)->run( "git clone --branch {$branch} --depth 1 https://github.com/{$repo}.git {$tempDir} 2>&1", ); if ($cloneResult->failed()) { Log::debug('[EmulatorUpdate] Could not clone source for checking: ' . $cloneResult->errorOutput()); return null; } // Get latest commit info $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()) : ''; // Cleanup Process::timeout(10)->run("rm -rf {$tempDir}"); $storedSha = $this->settings->getOrDefault('emulator_source_commit', null); $storedDate = $this->settings->getOrDefault('emulator_source_date', null); $installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null); // Check if there's an update - compare stored SHA with latest SHA $isUpdate = false; if ($storedSha !== null && $storedSha !== $latestSha) { $isUpdate = true; } elseif ($storedSha === null) { // First time - consider it an update $isUpdate = true; } Log::info('[EmulatorUpdate] Source update check (cloned): stored=' . $storedSha . ', latest=' . $latestSha . ', isUpdate=' . ($isUpdate ? 'true' : 'false')); 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('[EmulatorUpdate] Source clone check failed: ' . $e->getMessage()); return null; } } 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('[EmulatorUpdate] Starting source build', [ 'repo' => $repo, 'branch' => $branch, 'path' => $sourcePath, ]); $this->ensureJavaInstalled(); // Pre-flight: Ensure directories exist and fix permissions 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))); // Auto-retry logic $maxRetries = 2; $lastError = ''; for ($retry = 0; $retry <= $maxRetries; $retry++) { // Always fix permissions before any operation (handles root-owned files from previous builds) 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('[EmulatorUpdate] Retry attempt', ['attempt' => $retry]); // Force re-clone on retry Process::timeout(30)->run('rm -rf ' . escapeshellarg((string) $sourcePath)); } try { // Use command line to check if directory exists and is a git repo (bypasses open_basedir) $existsCheck = Process::timeout(5)->run('[ -d ' . escapeshellarg((string) $sourcePath) . " ] && echo 'exists'"); $sourceExists = $existsCheck->successful() && trim($existsCheck->output()) === 'exists'; // Also check if it's a valid git repository $gitCheck = Process::timeout(5)->run('[ -d ' . escapeshellarg((string) $sourcePath) . "/.git ] && echo 'git'"); $isGitRepo = $gitCheck->successful() && trim($gitCheck->output()) === 'git'; if ($sourceExists && $isGitRepo) { Log::info('[EmulatorUpdate] 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('[EmulatorUpdate] 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('[EmulatorUpdate] Git operation failed', ['error' => $lastError, 'attempt' => $retry]); continue; // Retry } $buildCommands = $this->getBuildCommands($sourcePath); Log::info('[EmulatorUpdate] Running build', ['command' => $buildCommands]); $buildResult = Process::timeout(600)->run('cd ' . escapeshellarg((string) $sourcePath) . ' && ' . $this->getBuildCommands($sourcePath)); $hasSignalError = str_contains($buildResult->errorOutput(), 'signal') || str_contains($buildResult->output(), 'signal'); // Check for signal errors - still try to find JAR if ($hasSignalError) { Log::warning('[EmulatorUpdate] Build process received signal, checking if JAR was built anyway'); } // Even if build "failed", check if JAR exists (signal errors may be misleading) $jarPath = $this->findBuiltJar($sourcePath); if ($jarPath) { Log::info('[EmulatorUpdate] JAR found despite build status', ['jar' => $jarPath]); } elseif ($buildResult->failed() && ! $hasSignalError) { $lastError = 'Build failed: ' . substr($buildResult->errorOutput(), 0, 500); Log::warning('[EmulatorUpdate] Build failed', ['error' => $lastError, 'attempt' => $retry]); // Try to clean and rebuild 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; // Retry } continue; } if (! $jarPath) { $lastError = 'Build succeeded but JAR not found. Check build output.'; Log::warning('[EmulatorUpdate] JAR not found', ['attempt' => $retry]); continue; } $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) . '/', '# Restart service (try all methods) service_restarted=false for method in "systemctl restart ' . escapeshellarg((string) $serviceName) . '" "service ' . escapeshellarg((string) $serviceName) . ' restart" "/etc/init.d/' . escapeshellarg((string) $serviceName) . ' restart" "kill -HUP \$(pgrep -f ' . escapeshellarg((string) $serviceName) . ')" ; do echo "Trying: $method" eval "$method" 2>/dev/null && { service_restarted=true; break; } || true done if [ "$service_restarted" = false ]; then echo "WARNING: Service restart may have failed (check manually)" >&2 exit 1 fi', ]; $deployCommand = implode(' && ', $deployCommands); Log::info('[EmulatorUpdate] Deploying jar', [ 'source' => $jarPath, 'destination' => "{$this->jarPath}/{$jarName}", ]); $deployResult = Process::timeout(60)->run($deployCommand); Log::info('[EmulatorUpdate] Deploy result', [ 'output' => $deployResult->output(), 'error' => $deployResult->errorOutput(), 'successful' => $deployResult->successful(), ]); if (! $deployResult->successful()) { $lastError = 'Deploy failed: ' . substr($deployResult->errorOutput(), 0, 300); Log::warning('[EmulatorUpdate] Deploy failed', ['error' => $lastError, 'attempt' => $retry]); if ($retry < $maxRetries) { continue; // Retry } // All retries exhausted return [ 'success' => false, 'error' => 'Build mislukt na ' . ($maxRetries + 1) . ' pogingen. Laatste fout: ' . $lastError, ]; } $sourceInfo = $this->checkForSourceUpdates(); $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); $this->runSqlUpdates(); Log::info('[EmulatorUpdate] Source build successful'); return [ 'success' => true, 'version' => $version, 'jar' => $jarName, 'built' => true, 'message' => "✅ Emulator vanaf source gebouwd!\n📦 {$jarName}\n🔄 Service herstart", ]; } catch (\Exception $e) { $lastError = $e->getMessage(); Log::error('[EmulatorUpdate] Source build exception', ['error' => $lastError, 'attempt' => $retry]); } } // All retries exhausted return [ 'success' => false, 'error' => 'Build mislukt na ' . ($maxRetries + 1) . ' pogingen. Laatste fout: ' . $lastError, ]; } private function getBuildCommands(string $sourcePath): string { // Check for pom.xml in both sourcePath and sourcePath/Emulator $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') { Log::info('[EmulatorUpdate] Building with Maven (pom.xml found in ' . $pomPath . ')'); return "cd {$pomPath} && mvn clean package -DskipTests 2>&1"; } if ($gradlewCheck->successful() && trim($gradlewCheck->output()) === 'gradlew') { Log::info('[EmulatorUpdate] Building with Gradle (gradlew found)'); return "cd {$sourcePath} && chmod +x gradlew && ./gradlew clean build -x test 2>&1"; } if ($gradleCheck->successful() && trim($gradleCheck->output()) === 'gradle') { Log::info('[EmulatorUpdate] Building with Gradle (build.gradle found)'); return "cd {$sourcePath} && gradle clean build -x test 2>&1"; } Log::warning('[EmulatorUpdate] No build system found in source directory'); return "cd {$sourcePath} && ls -la"; } private function findBuiltJar(string $sourcePath): ?string { $patterns = [ $sourcePath . '/target/*.jar', $sourcePath . '/build/libs/*.jar', $sourcePath . '/*/*.jar', $sourcePath . '/*.jar', // Also check in Emulator subdirectory $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; } 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; } // Check via command line to bypass open_basedir restriction 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 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 ''; } 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']; } 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, ]; } } } if ($files === []) { $result = Process::timeout(5)->run("ls {$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 (str_contains($line, '.jar')) { $files[] = [ 'name' => basename($line), 'size' => '?', ]; } } } } } catch (\Exception) { } return $files; } public function getLastSqlUpdate(): ?string { return setting('emulator_last_sql_update'); } public function getBackupList(): 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 restoreBackup(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()]; } } public function debugStatus(): array { return Cache::remember('emulator_debug_status', 120, function () { $this->ensureInstalledDateIsSet(); $installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null); $sourceCommit = $this->settings->getOrDefault('emulator_source_commit', null); $sourceDate = $this->settings->getOrDefault('emulator_source_date', null); $emulatorVersion = $this->settings->getOrDefault('emulator_version', null); $jarFiles = $this->getInstalledJarInfo(); return [ 'github_url' => $this->githubUrl, 'github_repo' => $this->githubRepo, 'github_branch' => $this->githubBranch, 'source_repo' => $this->sourceRepo, 'source_branch' => $this->sourceBranch, 'installed_date' => $installedDate, 'installed_date_formatted' => $installedDate ? date('Y-m-d H:i:s', (int) $installedDate) : null, 'source_commit' => $sourceCommit, 'source_date' => $sourceDate, 'source_date_formatted' => $sourceDate ? date('Y-m-d H:i:s', (int) $sourceDate) : null, 'emulator_version' => $emulatorVersion, 'jar_files' => $jarFiles, 'jar_path' => $this->jarPath, 'installed_branch' => $this->settings->getOrDefault('emulator_installed_branch', null), ]; }); } public function resetInstalledDate(): void { WebsiteSetting::where('key', 'emulator_jar_installed_date')->delete(); WebsiteSetting::where('key', 'emulator_source_commit')->delete(); WebsiteSetting::where('key', 'emulator_source_date')->delete(); } private function commandFileExists(string $path): bool { $result = Process::timeout(5)->run('ls ' . $path . ' 2>/dev/null | head -1'); return $result->successful() && trim($result->output()) !== ''; } private function commandDirExists(string $path): bool { $result = Process::timeout(5)->run('[ -d ' . escapeshellarg($path) . ' ] && echo "1" || echo "0"'); return trim($result->output()) === '1'; } private function persistSourceCommitInfo(string $sha, int $timestamp): void { $this->settings->set('emulator_source_commit', $sha); $this->settings->set('emulator_source_date', (string) $timestamp); } private function ensureInstalledDateIsSet(): void { if ($this->settings->getOrDefault('emulator_jar_installed_date', null) !== null) { return; } if (! $this->commandFileExists($this->jarPath . '/*.jar')) { return; } $installedVersion = $this->settings->getOrDefault('emulator_version', null); if (! $installedVersion) { return; } $jarInfo = $this->findLatestJar(); if ($jarInfo && ($jarInfo['commit_date'] ?? null)) { $this->settings->set('emulator_jar_installed_date', (string) $jarInfo['commit_date']); return; } $sourceInfo = $this->checkForSourceUpdates(); if ($sourceInfo && ($sourceInfo['latest_timestamp'] ?? null)) { $this->settings->set('emulator_jar_installed_date', (string) $sourceInfo['latest_timestamp']); } } public function clearAllLogs(): array { $cleared = []; $paths = [ storage_path('logs') => 'Laravel Logs', storage_path('logs/emulator.log') => 'Emulator Log', '/tmp/emulator-update-*' => 'Emulator Update Temp', '/tmp/nitro-switch-*' => 'Nitro Switch Logs', '/tmp/nitro_*' => 'Nitro Temp', '/var/www/Emulator/logs' => 'Emulator Folder Logs', ]; foreach ($paths as $path => $label) { try { if (str_contains($path, '*')) { Process::timeout(10)->run("rm -f {$path} 2>/dev/null || true"); $cleared[] = $label; } elseif (is_dir($path)) { Process::timeout(10)->run("find {$path} -name '*.log' -mtime +1 -delete 2>/dev/null || true"); $cleared[] = $label; } elseif (is_file($path)) { @unlink($path); $cleared[] = $label; } } catch (\Exception) { } } // Also clear Laravel log try { $laravelLog = storage_path('logs/laravel.log'); if (is_file($laravelLog)) { file_put_contents($laravelLog, ''); $cleared[] = 'laravel.log'; } } catch (\Exception) { } // Clear tmp files older than 1 day Process::timeout(10)->run("find /tmp -name 'emulator_*' -mtime +1 -delete 2>/dev/null || true"); Process::timeout(10)->run("find /tmp -name 'nitro_*' -mtime +1 -delete 2>/dev/null || true"); Process::timeout(10)->run("find /tmp -name 'deploy_*' -mtime +1 -delete 2>/dev/null || true"); return [ 'success' => true, 'cleared' => $cleared, 'message' => count($cleared) . ' log locaties geleegd', ]; } public function repairEmulator(): array { $actions = []; $errors = []; Log::info('[EmulatorUpdate] Starting repair process'); try { $status = $this->getStatus(); if (! ($status['jar_exists'] ?? false)) { $actions[] = 'JAR bestand ontbreekt - downloaden...'; $updateResult = $this->updateEmulator(); if (! $updateResult['success']) { $errors[] = 'Kon JAR niet herstellen: ' . ($updateResult['error'] ?? 'Onbekende fout'); } else { $actions[] = 'JAR bestand hersteld'; } } if (! ($status['service_running'] ?? false)) { $actions[] = 'Emulator service niet actief - starten...'; if ($this->restartEmulator()) { $actions[] = 'Emulator service gestart'; } else { $errors[] = 'Kon emulator service niet starten'; } } $emulatorPath = $this->jarPath; $pathExists = $this->commandDirExists(dirname((string) $emulatorPath)); if (! $pathExists) { $actions[] = 'Emulator pad bestaat niet - aanmaken...'; Process::timeout(10)->run('mkdir -p ' . escapeshellarg(dirname((string) $emulatorPath))); Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg(dirname((string) $emulatorPath))); $actions[] = 'Emulator pad aangemaakt'; } if (! ($status['emulator_db_connected'] ?? false)) { $actions[] = 'Emulator database niet bereikbaar - controleer connectie'; $errors[] = 'Emulator database connectie probleem'; } $sqlRepairResult = $this->repairSqlUpdates(); if (! empty($sqlRepairResult['actions'])) { $actions = array_merge($actions, $sqlRepairResult['actions']); } if (! empty($sqlRepairResult['errors'])) { $errors = array_merge($errors, $sqlRepairResult['errors']); } $this->ensureGitSafeDirectories(); $sudoersFile = '/etc/sudoers.d/atomcms-emulator'; $checkResult = Process::timeout(5)->run("test -f {$sudoersFile} && echo 'exists' || echo 'missing'"); if (trim($checkResult->output()) !== 'exists') { $actions[] = 'Sudoers configuratie ontbreekt - toevoegen...'; $this->ensureSudoAccess(); $actions[] = 'Sudoers configuratie toegevoegd'; } if ($errors !== []) { Log::warning('[EmulatorUpdate] Repair completed with errors', ['errors' => $errors]); return [ 'success' => false, 'actions' => $actions, 'errors' => $errors, 'error' => implode('; ', $errors), ]; } Log::info('[EmulatorUpdate] Repair completed successfully', ['actions' => $actions]); return [ 'success' => true, 'actions' => $actions, 'message' => count($actions) . ' acties uitgevoerd', ]; } catch (\Exception $e) { Log::error('[EmulatorUpdate] Repair exception', ['error' => $e->getMessage()]); return [ 'success' => false, 'actions' => $actions, 'error' => $e->getMessage(), ]; } } public function repairSqlUpdates(): 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->checkForSqlUpdates(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->runSqlUpdates(); 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('[EmulatorUpdate] SQL repair failed', ['error' => $e->getMessage()]); } return [ 'success' => $errors === [], 'actions' => $actions, 'errors' => $errors, ]; } public function diagnose(): array { $diagnosis = [ 'timestamp' => now()->toIso8601String(), 'checks' => [], 'issues' => [], 'recommendations' => [], ]; try { $status = $this->getStatus(); $diagnosis['checks']['jar_exists'] = $status['jar_exists'] ?? false; $diagnosis['checks']['jar_files'] = $status['jar_files'] ?? []; $diagnosis['checks']['service_running'] = $status['service_running'] ?? false; $diagnosis['checks']['source_exists'] = $status['source_exists'] ?? false; $diagnosis['checks']['emulator_db_connected'] = $status['emulator_db_connected'] ?? false; $diagnosis['checks']['is_configured'] = $this->isConfigured(); $diagnosis['checks']['update_available'] = $status['update_available'] ?? false; $diagnosis['checks']['jar_path'] = $this->jarPath; $diagnosis['checks']['source_path'] = $this->emulatorSourcePath; $diagnosis['checks']['service_name'] = $this->emulatorService; $sqlDiagnosis = $this->diagnoseSqlUpdates(); $diagnosis['checks']['sql_table_exists'] = $sqlDiagnosis['table_exists'] ?? false; $diagnosis['checks']['sql_updates_applied'] = $sqlDiagnosis['applied_count'] ?? 0; $diagnosis['checks']['sql_pending'] = $sqlDiagnosis['pending_count'] ?? 0; if (! ($status['jar_exists'] ?? false)) { $diagnosis['issues'][] = 'JAR bestand ontbreekt'; $diagnosis['recommendations'][] = 'Voer emulator:update uit om de JAR te downloaden'; } if (! ($status['service_running'] ?? false)) { $diagnosis['issues'][] = 'Emulator service draait niet'; $diagnosis['recommendations'][] = 'Start de service met: sudo systemctl start ' . $this->emulatorService; } if (! ($status['emulator_db_connected'] ?? false)) { $diagnosis['issues'][] = 'Emulator database niet bereikbaar'; $diagnosis['recommendations'][] = 'Controleer de database credentials in de settings'; } if (! ($status['source_exists'] ?? false) && $this->isSourceBuildAvailable()) { $diagnosis['issues'][] = 'Source code niet gevonden'; $diagnosis['recommendations'][] = 'Voer emulator:update --rebuild uit om vanaf source te bouwen'; } if (! ($sqlDiagnosis['table_exists'] ?? false)) { $diagnosis['issues'][] = 'SQL update tabel ontbreekt'; $diagnosis['recommendations'][] = 'Reparatie zal de tabel aanmaken'; } if (($sqlDiagnosis['pending_count'] ?? 0) > 0) { $diagnosis['issues'][] = $sqlDiagnosis['pending_count'] . ' SQL updates pending'; $diagnosis['recommendations'][] = 'Voer reparatie uit om SQL updates toe te passen'; } if (! ($status['update_available'] ?? false) && $this->isConfigured()) { $diagnosis['recommendations'][] = 'Emulator is up-to-date'; } } catch (\Exception $e) { $diagnosis['error'] = $e->getMessage(); } return $diagnosis; } public function diagnoseSqlUpdates(): 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->checkForSqlUpdates(false); if (isset($sqlCheck['count'])) { $diagnosis['pending_count'] = $sqlCheck['count']; } } } catch (\Exception $e) { $diagnosis['error'] = $e->getMessage(); } return $diagnosis; } }