From 7b79767355d2910394f3ca3603daaa5eff840bfa Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 9 May 2026 14:13:47 +0800 Subject: [PATCH] Add retry mechanism to Git clone and GitHub release fetching methods --- src/Package/Artifact/go_win.php | 4 ++-- src/Package/Artifact/go_xcaddy.php | 4 ++-- src/StaticPHP/Artifact/Downloader/Type/Git.php | 4 ++-- .../Artifact/Downloader/Type/GitHubRelease.php | 12 ++++++------ .../Artifact/Downloader/Type/GitHubTarball.php | 14 +++++++------- src/StaticPHP/Runtime/Shell/DefaultShell.php | 18 ++++++++++++++++-- 6 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/Package/Artifact/go_win.php b/src/Package/Artifact/go_win.php index 44cf61b9..e06e615a 100644 --- a/src/Package/Artifact/go_win.php +++ b/src/Package/Artifact/go_win.php @@ -23,13 +23,13 @@ class go_win $pkgroot = PKG_ROOT_PATH; // get version - [$version] = explode("\n", default_shell()->executeCurl('https://go.dev/VERSION?m=text') ?: ''); + [$version] = explode("\n", default_shell()->executeCurl('https://go.dev/VERSION?m=text', retries: $downloader->getRetry()) ?: ''); if ($version === '') { throw new DownloaderException('Failed to get latest Go version from https://go.dev/VERSION?m=text'); } // find SHA256 hash from download page - $page = default_shell()->executeCurl('https://go.dev/dl/'); + $page = default_shell()->executeCurl('https://go.dev/dl/', retries: $downloader->getRetry()); if ($page === '' || $page === false) { throw new DownloaderException('Failed to get Go download page from https://go.dev/dl/'); } diff --git a/src/Package/Artifact/go_xcaddy.php b/src/Package/Artifact/go_xcaddy.php index ca61bd46..51ccfb87 100644 --- a/src/Package/Artifact/go_xcaddy.php +++ b/src/Package/Artifact/go_xcaddy.php @@ -39,11 +39,11 @@ class go_xcaddy }; // get version and hash - [$version] = explode("\n", default_shell()->executeCurl('https://go.dev/VERSION?m=text') ?: ''); + [$version] = explode("\n", default_shell()->executeCurl('https://go.dev/VERSION?m=text', retries: $downloader->getRetry()) ?: ''); if ($version === '') { throw new DownloaderException('Failed to get latest Go version from https://go.dev/VERSION?m=text'); } - $page = default_shell()->executeCurl('https://go.dev/dl/'); + $page = default_shell()->executeCurl('https://go.dev/dl/', retries: $downloader->getRetry()); if ($page === '' || $page === false) { throw new DownloaderException('Failed to get Go download page from https://go.dev/dl/'); } diff --git a/src/StaticPHP/Artifact/Downloader/Type/Git.php b/src/StaticPHP/Artifact/Downloader/Type/Git.php index d5822e69..ad561c47 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/Git.php +++ b/src/StaticPHP/Artifact/Downloader/Type/Git.php @@ -20,7 +20,7 @@ class Git implements DownloadTypeInterface, CheckUpdateInterface // direct branch clone if (isset($config['rev'])) { - default_shell()->executeGitClone($config['url'], $config['rev'], $path, $shallow, $config['submodules'] ?? null); + default_shell()->executeGitClone($config['url'], $config['rev'], $path, $shallow, $config['submodules'] ?? null, $downloader->getRetry()); $shell = PHP_OS_FAMILY === 'Windows' ? cmd(false) : shell(false); $hash_result = $shell->execWithResult(SPC_GIT_EXEC . ' -C ' . escapeshellarg($path) . ' rev-parse --short HEAD'); $hash = ($hash_result[0] === 0 && !empty($hash_result[1])) ? trim($hash_result[1][0]) : ''; @@ -66,7 +66,7 @@ class Git implements DownloadTypeInterface, CheckUpdateInterface $version = array_key_first($matched_version_branch); $branch = $matched_version_branch[$version]; logger()->info("Matched version {$version} from branch {$branch} for {$name}"); - default_shell()->executeGitClone($config['url'], $branch, $path, $shallow, $config['submodules'] ?? null); + default_shell()->executeGitClone($config['url'], $branch, $path, $shallow, $config['submodules'] ?? null, $downloader->getRetry()); return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); } throw new DownloaderException("No matching branch found for regex {$config['regex']} (checked {$matched_count} branches)."); diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php index 15626089..20db7127 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php @@ -21,13 +21,13 @@ class GitHubRelease implements DownloadTypeInterface, ValidatorInterface, CheckU private ?string $version = null; - public function getGitHubReleases(string $name, string $repo, bool $prefer_stable = true, ?string $query = null): array + public function getGitHubReleases(string $name, string $repo, bool $prefer_stable = true, ?string $query = null, int $retries = 0): array { logger()->debug("Fetching {$name} GitHub releases from {$repo}"); $url = str_replace('{repo}', $repo, self::API_URL); $url .= ($query ?? ''); $headers = $this->getGitHubTokenHeaders(); - $data2 = default_shell()->executeCurl($url, headers: $headers); + $data2 = default_shell()->executeCurl($url, headers: $headers, retries: $retries); $data = json_decode($data2 ?: '', true); if (!is_array($data)) { throw new DownloaderException("Failed to get GitHub release API info for {$repo} from {$url}"); @@ -46,13 +46,13 @@ class GitHubRelease implements DownloadTypeInterface, ValidatorInterface, CheckU * Get the latest GitHub release assets for a given repository. * match_asset is provided, only return the asset that matches the regex. */ - public function getLatestGitHubRelease(string $name, string $repo, bool $prefer_stable, string $match_asset, ?string $query = null): array + public function getLatestGitHubRelease(string $name, string $repo, bool $prefer_stable, string $match_asset, ?string $query = null, int $retries = 0): array { logger()->debug("Fetching {$name} GitHub release from {$repo}"); $url = str_replace('{repo}', $repo, self::API_URL); $url .= ($query ?? ''); $headers = $this->getGitHubTokenHeaders(); - $data2 = default_shell()->executeCurl($url, headers: $headers); + $data2 = default_shell()->executeCurl($url, headers: $headers, retries: $retries); $data = json_decode($data2 ?: '', true); if (!is_array($data)) { throw new DownloaderException("Failed to get GitHub release API info for {$repo} from {$url}"); @@ -84,7 +84,7 @@ class GitHubRelease implements DownloadTypeInterface, ValidatorInterface, CheckU if (!isset($config['match'])) { throw new DownloaderException("GitHubRelease downloader requires 'match' config for {$name}"); } - $rel = $this->getLatestGitHubRelease($name, $config['repo'], $config['prefer-stable'] ?? true, $config['match'], $config['query'] ?? null); + $rel = $this->getLatestGitHubRelease($name, $config['repo'], $config['prefer-stable'] ?? true, $config['match'], $config['query'] ?? null, $downloader->getRetry()); // download file using curl $asset_url = str_replace(['{repo}', '{id}'], [$config['repo'], $rel['id']], self::ASSET_URL); @@ -124,7 +124,7 @@ class GitHubRelease implements DownloadTypeInterface, ValidatorInterface, CheckU if (!isset($config['match'])) { throw new DownloaderException("GitHubRelease downloader requires 'match' config for {$name}"); } - $this->getLatestGitHubRelease($name, $config['repo'], $config['prefer-stable'] ?? true, $config['match'], $config['query'] ?? null); + $this->getLatestGitHubRelease($name, $config['repo'], $config['prefer-stable'] ?? true, $config['match'], $config['query'] ?? null, $downloader->getRetry()); $new_version = $this->version ?? $old_version ?? ''; return new CheckUpdateResult( old: $old_version, diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php index 37ba5ca7..c9c657bc 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php @@ -22,11 +22,11 @@ class GitHubTarball implements DownloadTypeInterface, CheckUpdateInterface * Get the GitHub tarball URL for a given repository and release type. * If match_url is provided, only return the tarball that matches the regex. */ - public function getGitHubTarballInfo(string $name, string $repo, string $rel_type, bool $prefer_stable = true, ?string $match_url = null, ?string $basename = null, ?string $query = null): array + public function getGitHubTarballInfo(string $name, string $repo, string $rel_type, bool $prefer_stable = true, ?string $match_url = null, ?string $basename = null, ?string $query = null, int $retries = 0): array { if ($rel_type === 'releases' && $match_url === null && $query === null && $prefer_stable) { $api_url = str_replace(['{repo}', '{rel_type}'], [$repo, 'releases/latest'], self::API_URL); - $data = default_shell()->executeCurl($api_url, headers: $this->getGitHubTokenHeaders()); + $data = default_shell()->executeCurl($api_url, headers: $this->getGitHubTokenHeaders(), retries: $retries); $data = json_decode($data ?: '', true); if (!is_array($data) || empty($data['tarball_url'])) { throw new DownloaderException("Failed to get GitHub latest release for {$repo} from {$api_url}"); @@ -36,7 +36,7 @@ class GitHubTarball implements DownloadTypeInterface, CheckUpdateInterface } else { $api_url = str_replace(['{repo}', '{rel_type}'], [$repo, $rel_type], self::API_URL); $api_url .= ($query ?? ''); - $data = default_shell()->executeCurl($api_url, headers: $this->getGitHubTokenHeaders()); + $data = default_shell()->executeCurl($api_url, headers: $this->getGitHubTokenHeaders(), retries: $retries); $data = json_decode($data ?: '', true); if (!is_array($data)) { throw new DownloaderException("Failed to get GitHub tarball URL for {$repo} from {$api_url}"); @@ -65,7 +65,7 @@ class GitHubTarball implements DownloadTypeInterface, CheckUpdateInterface } $this->version = $version ?? null; } - $head = default_shell()->executeCurl($rel_url, 'HEAD', headers: $this->getGitHubTokenHeaders()) ?: ''; + $head = default_shell()->executeCurl($rel_url, 'HEAD', headers: $this->getGitHubTokenHeaders(), retries: $retries) ?: ''; preg_match('/^content-disposition:\s+attachment;\s*filename=("?)(?.+\.tar\.gz)\1/im', $head, $matches); if ($matches) { $filename = $matches['filename']; @@ -84,9 +84,9 @@ class GitHubTarball implements DownloadTypeInterface, CheckUpdateInterface 'ghtagtar' => 'tags', default => throw new DownloaderException("Invalid GitHubTarball type for {$name}"), }; - [$url, $filename] = $this->getGitHubTarballInfo($name, $config['repo'], $rel_type, $config['prefer-stable'] ?? true, $config['match'] ?? null, $name, $config['query'] ?? null); + [$url, $filename] = $this->getGitHubTarballInfo($name, $config['repo'], $rel_type, $config['prefer-stable'] ?? true, $config['match'] ?? null, $name, $config['query'] ?? null, $downloader->getRetry()); $path = DOWNLOAD_PATH . "/{$filename}"; - default_shell()->executeCurlDownload($url, $path, headers: $this->getGitHubTokenHeaders()); + default_shell()->executeCurlDownload($url, $path, headers: $this->getGitHubTokenHeaders(), retries: $downloader->getRetry()); return DownloadResult::archive($filename, $config, $config['extract'] ?? null, version: $this->version, downloader: static::class); } @@ -97,7 +97,7 @@ class GitHubTarball implements DownloadTypeInterface, CheckUpdateInterface 'ghtagtar' => 'tags', default => throw new DownloaderException("Invalid GitHubTarball type for {$name}"), }; - $this->getGitHubTarballInfo($name, $config['repo'], $rel_type, $config['prefer-stable'] ?? true, $config['match'] ?? null, $name, $config['query'] ?? null); + $this->getGitHubTarballInfo($name, $config['repo'], $rel_type, $config['prefer-stable'] ?? true, $config['match'] ?? null, $name, $config['query'] ?? null, $downloader->getRetry()); $new_version = $this->version ?? $old_version ?? ''; return new CheckUpdateResult( old: $old_version, diff --git a/src/StaticPHP/Runtime/Shell/DefaultShell.php b/src/StaticPHP/Runtime/Shell/DefaultShell.php index 6c32b155..f20fca33 100644 --- a/src/StaticPHP/Runtime/Shell/DefaultShell.php +++ b/src/StaticPHP/Runtime/Shell/DefaultShell.php @@ -84,7 +84,7 @@ class DefaultShell extends Shell /** * Execute a Git clone command to clone a repository. */ - public function executeGitClone(string $url, string $branch, string $path, bool $shallow = true, ?array $submodules = null): void + public function executeGitClone(string $url, string $branch, string $path, bool $shallow = true, ?array $submodules = null, int $retries = 0): void { $path = FileSystem::convertPath($path); if (file_exists($path)) { @@ -99,7 +99,21 @@ class DefaultShell extends Shell $cmd = clean_spaces("{$git} clone -c http.lowSpeedLimit=1 -c http.lowSpeedTime=3600 --config core.autocrlf=false --branch {$branch_arg} {$shallow_arg} {$submodules_arg} {$url_arg} {$path_arg}"); $this->logCommandInfo($cmd); logger()->debug("[GIT CLONE] {$cmd}"); - $this->passthru($cmd, $this->console_putput); + try { + $this->passthru($cmd, $this->console_putput); + } catch (InterruptException $e) { + throw $e; + } catch (\Throwable $e) { + if ($retries > 0) { + logger()->warning("Git clone failed, retrying... ({$retries} retries left)"); + if (is_dir($path)) { + FileSystem::removeDir($path); + } + $this->executeGitClone($url, $branch, $path, $shallow, $submodules, $retries - 1); + return; + } + throw $e; + } if ($submodules !== null) { $depth_flag = $shallow ? '--depth 1' : ''; foreach ($submodules as $submodule) {