-
Notifications
You must be signed in to change notification settings - Fork 690
fix(codex): harden Windows CLI and HF downloads #1097
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
8475ecc
d0ecdb5
cff6dd9
796f487
af2989f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| $ErrorActionPreference = "Stop" | ||
|
|
||
| . "$PSScriptRoot\prereq-check.ps1" | ||
|
|
||
| function Save-EnvVars { | ||
| param([string[]]$Names) | ||
| $saved = @{} | ||
| foreach ($name in $Names) { | ||
| $saved[$name] = [Environment]::GetEnvironmentVariable($name, "Process") | ||
| } | ||
| return $saved | ||
| } | ||
|
|
||
| function Restore-EnvVars { | ||
| param( | ||
| [hashtable]$Saved, | ||
| [string[]]$Names | ||
| ) | ||
| foreach ($name in $Names) { | ||
| [Environment]::SetEnvironmentVariable($name, $Saved[$name], "Process") | ||
| } | ||
| } | ||
|
|
||
| $names = @( | ||
| "HTTP_PROXY", | ||
| "HTTPS_PROXY", | ||
| "ALL_PROXY", | ||
| "HF_HUB_DISABLE_SYMLINKS", | ||
| "HF_HUB_DISABLE_SYMLINKS_WARNING" | ||
| ) | ||
| $saved = Save-EnvVars -Names $names | ||
| $savedTransportMode = $script:CatCafeHfDownloadTransportMode | ||
|
|
||
| $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("cat-cafe-prereq-test-" + [guid]::NewGuid().ToString("N")) | ||
| New-Item -ItemType Directory -Path $tempDir -Force | Out-Null | ||
| $fakePython = Join-Path $tempDir "python.cmd" | ||
| $capture = Join-Path $tempDir "env.txt" | ||
|
|
||
| try { | ||
| Set-Content -LiteralPath $fakePython -Encoding ASCII -Value @" | ||
| @echo off | ||
| ( | ||
| echo HTTP_PROXY=%HTTP_PROXY% | ||
| echo HTTPS_PROXY=%HTTPS_PROXY% | ||
| echo ALL_PROXY=%ALL_PROXY% | ||
| echo HF_HUB_DISABLE_SYMLINKS=%HF_HUB_DISABLE_SYMLINKS% | ||
| echo HF_HUB_DISABLE_SYMLINKS_WARNING=%HF_HUB_DISABLE_SYMLINKS_WARNING% | ||
| ) > "%CAT_CAFE_TEST_ENV_CAPTURE%" | ||
| exit /b 0 | ||
| "@ | ||
|
|
||
| $env:CAT_CAFE_TEST_ENV_CAPTURE = $capture | ||
| $env:HTTP_PROXY = "http://127.0.0.1:9" | ||
| $env:HTTPS_PROXY = "http://127.0.0.1:9" | ||
| $env:ALL_PROXY = "http://127.0.0.1:9" | ||
| $script:CatCafeHfDownloadTransportMode = "direct" | ||
| Remove-Item Env:HF_HUB_DISABLE_SYMLINKS,Env:HF_HUB_DISABLE_SYMLINKS_WARNING -ErrorAction SilentlyContinue | ||
|
|
||
| Invoke-ModelDownloadWithRetry -VenvPython $fakePython -ModelId "dummy/model" -Loader "snapshot" | ||
|
|
||
| $captured = Get-Content -LiteralPath $capture | ||
| foreach ($line in @("HTTP_PROXY=", "HTTPS_PROXY=", "ALL_PROXY=")) { | ||
| if ($captured -notcontains $line) { | ||
| throw "Expected child process proxy env to be cleared; missing '$line'. Captured: $($captured -join '; ')" | ||
| } | ||
| } | ||
| foreach ($line in @("HF_HUB_DISABLE_SYMLINKS=1", "HF_HUB_DISABLE_SYMLINKS_WARNING=1")) { | ||
| if ($captured -notcontains $line) { | ||
| throw "Expected Windows HuggingFace symlink guard '$line'. Captured: $($captured -join '; ')" | ||
| } | ||
| } | ||
| if ($env:HTTP_PROXY -ne "http://127.0.0.1:9") { | ||
| throw "Expected parent HTTP_PROXY to be restored after child invocation." | ||
| } | ||
|
|
||
| Write-Host "prereq-check.proxy-env.test.ps1: PASS" | ||
| } finally { | ||
| Remove-Item Env:CAT_CAFE_TEST_ENV_CAPTURE -ErrorAction SilentlyContinue | ||
| $script:CatCafeHfDownloadTransportMode = $savedTransportMode | ||
| Restore-EnvVars -Saved $saved -Names $names | ||
| Remove-Item -LiteralPath $tempDir -Recurse -Force -ErrorAction SilentlyContinue | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -155,17 +155,44 @@ function Test-SourceMode { | |
| # "corp-proxy reaches Tsinghua even though it can't reach pypi" case: | ||
| # Sync-SystemProxy no longer gates the candidate on a single pypi probe, | ||
| # so per-source decisions in Assert-Network always see the candidate. | ||
| param([string]$Url, [int]$TimeoutSec = 5, [string]$CandidateProxy = $null) | ||
| param( | ||
| [string]$Url, | ||
| [int]$TimeoutSec = 5, | ||
| [string]$CandidateProxy = $null, | ||
| [ValidateSet("HEAD", "GET")][string]$Method = "HEAD" | ||
| ) | ||
|
|
||
| $probe = { | ||
| param($Proxy) | ||
| $resp = $null | ||
| $stream = $null | ||
| try { | ||
| $req = [System.Net.HttpWebRequest]::Create($Url) | ||
| $req.Proxy = $Proxy | ||
| $req.Method = $Method | ||
| $req.Timeout = $TimeoutSec * 1000 | ||
| $req.ReadWriteTimeout = $TimeoutSec * 1000 | ||
| $req.AllowAutoRedirect = $true | ||
| $req.UserAgent = "cat-cafe-prereq-check" | ||
| $resp = $req.GetResponse() | ||
| if ($Method -eq "GET") { | ||
| $stream = $resp.GetResponseStream() | ||
| $buffer = New-Object byte[] 8192 | ||
| while ($stream -and $stream.Read($buffer, 0, $buffer.Length) -gt 0) {} | ||
| } | ||
| return $true | ||
| } catch { | ||
| return $false | ||
| } finally { | ||
| if ($stream) { $stream.Dispose() } | ||
| if ($resp) { $resp.Close() } | ||
| } | ||
| } | ||
|
|
||
| # 1. Try without any proxy at all. | ||
| try { | ||
| $req = [System.Net.HttpWebRequest]::Create($Url) | ||
| $req.Proxy = $null | ||
| $req.Method = 'HEAD' | ||
| $req.Timeout = $TimeoutSec * 1000 | ||
| $resp = $req.GetResponse() | ||
| $resp.Close() | ||
| if (& $probe $null) { | ||
| return 'direct' | ||
| } catch {} | ||
| } | ||
| # 2. Try via candidate proxy (anonymous -- DON'T let .NET auto-fill the | ||
| # SSPI token, that would mask auth-required corp proxies as reachable). | ||
| $proxyUrl = $CandidateProxy | ||
|
|
@@ -175,18 +202,12 @@ function Test-SourceMode { | |
| if (-not $proxyUrl) { $proxyUrl = $env:HTTP_PROXY } | ||
| } | ||
| if ($proxyUrl) { | ||
| try { | ||
| $webProxy = New-Object System.Net.WebProxy($proxyUrl) | ||
| $webProxy.UseDefaultCredentials = $false | ||
| $webProxy.Credentials = $null | ||
| $req = [System.Net.HttpWebRequest]::Create($Url) | ||
| $req.Proxy = $webProxy | ||
| $req.Method = 'HEAD' | ||
| $req.Timeout = $TimeoutSec * 1000 | ||
| $resp = $req.GetResponse() | ||
| $resp.Close() | ||
| $webProxy = New-Object System.Net.WebProxy($proxyUrl) | ||
| $webProxy.UseDefaultCredentials = $false | ||
| $webProxy.Credentials = $null | ||
| if (& $probe $webProxy) { | ||
| return 'proxy' | ||
| } catch {} | ||
| } | ||
| } | ||
| return 'unreachable' | ||
| } | ||
|
|
@@ -242,6 +263,11 @@ function Invoke-ModelDownloadWithRetry { | |
| [string]$Loader = "snapshot" | ||
| ) | ||
|
|
||
| if ($env:OS -eq "Windows_NT") { | ||
| $env:HF_HUB_DISABLE_SYMLINKS = if ($env:HF_HUB_DISABLE_SYMLINKS) { $env:HF_HUB_DISABLE_SYMLINKS } else { "1" } | ||
| $env:HF_HUB_DISABLE_SYMLINKS_WARNING = if ($env:HF_HUB_DISABLE_SYMLINKS_WARNING) { $env:HF_HUB_DISABLE_SYMLINKS_WARNING } else { "1" } | ||
| } | ||
|
|
||
| $script = switch ($Loader) { | ||
| "snapshot" { @" | ||
| import sys, time, os | ||
|
|
@@ -362,8 +388,34 @@ sys.exit(1) | |
| default { throw "Invoke-ModelDownloadWithRetry: unknown loader '$Loader'" } | ||
| } | ||
|
|
||
| & $VenvPython -c $script $ModelId | ||
| if ($LASTEXITCODE -ne 0) { throw "Failed to download model: $ModelId" } | ||
| $proxyVarNames = if ($env:OS -eq "Windows_NT") { | ||
| @("HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY") | ||
| } else { | ||
| @("HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy", "ALL_PROXY", "all_proxy") | ||
| } | ||
| $savedProxyEnv = @{} | ||
| $clearProxyForDownload = ($script:CatCafeHfDownloadTransportMode -eq "direct") | ||
| $exitCode = $null | ||
| try { | ||
| if ($clearProxyForDownload) { | ||
|
Comment on lines
+401
to
+404
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a user sets Useful? React with 👍 / 👎. |
||
| # HuggingFace artifacts can redirect to CDN/CAS hosts that are | ||
| # not covered by NO_PROXY. When Assert-Network proved artifact | ||
| # downloads work direct, keep this Python child direct-only. | ||
| foreach ($name in $proxyVarNames) { | ||
| $savedProxyEnv[$name] = [Environment]::GetEnvironmentVariable($name, "Process") | ||
| [Environment]::SetEnvironmentVariable($name, $null, "Process") | ||
| } | ||
| } | ||
| & $VenvPython -c $script $ModelId | ||
| $exitCode = $LASTEXITCODE | ||
| } finally { | ||
| if ($clearProxyForDownload) { | ||
| foreach ($name in $proxyVarNames) { | ||
| [Environment]::SetEnvironmentVariable($name, $savedProxyEnv[$name], "Process") | ||
| } | ||
| } | ||
| } | ||
| if ($exitCode -ne 0) { throw "Failed to download model: $ModelId" } | ||
| } | ||
|
|
||
| function Assert-Network { | ||
|
|
@@ -374,6 +426,7 @@ function Assert-Network { | |
| # decisions don't depend on whether env was already set. | ||
| $candidate = Get-SystemProxyCandidate | ||
| $needProxyInjection = $false | ||
| $script:CatCafeHfDownloadTransportMode = $null | ||
|
|
||
| # FIRST: classify the user's PIP_INDEX_URL (if set) the same way we | ||
| # classify public mirrors. Internal corporate mirrors typically | ||
|
|
@@ -455,26 +508,32 @@ function Assert-Network { | |
| Write-Host " Auto-set PIP_INDEX_URL = Tsinghua mirror (primary)" | ||
| } | ||
|
|
||
| # Same two-mode probe for HuggingFace. | ||
| $hfMode = Test-SourceMode -Url "https://huggingface.co" -TimeoutSec 5 -CandidateProxy $candidate | ||
| # Same two-mode probe for HuggingFace. Probe a real model artifact rather | ||
| # than the homepage: some networks allow API/root requests but break TLS on | ||
| # /resolve artifact downloads, which is the path snapshot_download needs. | ||
| $hfProbeUrl = "https://huggingface.co/BAAI/bge-small-zh-v1.5/resolve/main/config.json" | ||
| $hfMode = Test-SourceMode -Url $hfProbeUrl -TimeoutSec 10 -CandidateProxy $candidate -Method "GET" | ||
| if ($hfMode -eq 'direct') { | ||
| Write-Host " HuggingFace connectivity [OK] (direct)" | ||
| Add-NoProxyHost "huggingface.co" | ||
| Write-Host " HuggingFace artifact download [OK] (direct)" | ||
| $script:CatCafeHfDownloadTransportMode = "direct" | ||
| } elseif ($hfMode -eq 'proxy') { | ||
| Write-Host " HuggingFace connectivity [OK] (via proxy: $candidate)" | ||
| Write-Host " HuggingFace artifact download [OK] (via proxy: $candidate)" | ||
| $script:CatCafeHfDownloadTransportMode = "proxy" | ||
| $needProxyInjection = $true | ||
| } else { | ||
| $hfMirrorMode = Test-SourceMode -Url "https://hf-mirror.com" -TimeoutSec 5 -CandidateProxy $candidate | ||
| $hfMirrorProbeUrl = "https://hf-mirror.com/BAAI/bge-small-zh-v1.5/resolve/main/config.json" | ||
| $hfMirrorMode = Test-SourceMode -Url $hfMirrorProbeUrl -TimeoutSec 10 -CandidateProxy $candidate -Method "GET" | ||
| if ($hfMirrorMode -eq 'direct') { | ||
| Write-Host " HuggingFace unreachable, switching to hf-mirror.com (direct)" | ||
| Write-Host " HuggingFace artifact download unreachable, switching to hf-mirror.com (direct)" | ||
| $env:HF_ENDPOINT = "https://hf-mirror.com" | ||
| Add-NoProxyHost "hf-mirror.com" | ||
| $script:CatCafeHfDownloadTransportMode = "direct" | ||
| } elseif ($hfMirrorMode -eq 'proxy') { | ||
| Write-Host " HuggingFace unreachable, switching to hf-mirror.com (via proxy: $candidate)" | ||
| Write-Host " HuggingFace artifact download unreachable, switching to hf-mirror.com (via proxy: $candidate)" | ||
| $env:HF_ENDPOINT = "https://hf-mirror.com" | ||
| $script:CatCafeHfDownloadTransportMode = "proxy" | ||
| $needProxyInjection = $true | ||
| } else { | ||
| Write-ProxyGuidance -Context "huggingface.co and hf-mirror.com are unreachable in both direct and via-proxy modes; model download will definitely fail." | ||
| Write-ProxyGuidance -Context "huggingface.co and hf-mirror.com artifact downloads are unreachable in both direct and via-proxy modes; model download will definitely fail." | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
HTTPS_PROXY/HTTP_PROXYor the registry-derived candidate is malformed,New-Object System.Net.WebProxy($proxyUrl)can throw before the$probecatch block runs; Windows proxy settings commonly use per-scheme strings likehttp=host:port;https=host:port, whichGet-SystemProxyCandidatecurrently turns into a non-URI. Before this refactor the constructor was inside the try/catch and the source was classified as unreachable, but now one bad proxy value aborts the whole prerequisite check instead of falling back/giving guidance.Useful? React with 👍 / 👎.