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); } }