v3.5.1:自更新抓取回退系统 curl(修根证书缺失环境)
This commit is contained in:
+4
-4
@@ -4,7 +4,7 @@
|
||||
"LICENSE.md": "6b839540e0fd4ab75530ec31fc5c3cbc3c2d5de5fe85068ef1c0901e54be9315",
|
||||
"README.md": "1e11e5d19856fbd9bb15068c74e184617a10b2e0b18c7e42d9b617cd0d8f534a",
|
||||
"SKILL.md": "eaff2bc3cc1221417caaf40cf86741c7fe2e43f7538fd9efc0f436c13c030a0b",
|
||||
"VERSION": "76120c1f2c4311c2154c22a6b58e00e411e0ebe76afae0daf608d9196c61f114",
|
||||
"VERSION": "eb41f86c3d67299b46490cde10158a521148fdc03641aa14a93fa2797becd78c",
|
||||
"data/.public_edition.json": "e8692c45d512c78903fc89a924d0e6d4dc9b95a78ca6b359bb7a5ef5c946ffee",
|
||||
"data/cities.json": "00f17905c8843218162e25238a78aeb4c4a3bb5424b611cc30ed9e436db31854",
|
||||
"memory/README.md": "b3cf2c58a8418a8c1a54243c2e09fbdb42f31dcd48ba8c841b74f1234c7fe063",
|
||||
@@ -585,7 +585,7 @@
|
||||
"scripts/render/html.py": "9896ffc7c1f29cdce0d2ce9c21ce78ec6035b77851249466776945de918de49d",
|
||||
"scripts/render/markdown.py": "4b2bcc3fc6581728d74bbaa357bf1db2e8aec1d3ec54c2c27ca645b6c5179977",
|
||||
"scripts/render/templates/paipan.html": "9cdeed44834ef0de7adf0d775486b8ece7c81cf228107295e51bc31847a81688",
|
||||
"scripts/self_update.py": "95dc0302ca2b78d71b2893ebaced7e233dbc00944a5e3c5b06081b5c9a749f8a",
|
||||
"scripts/self_update.py": "b1c9ad0ef90e20ac75ac6b8e857a4a5bd31181c67a58c7c1d46b45f468fd9f26",
|
||||
"scripts/share_export.py": "21396143ad5cc2d244c7cc060a3ece25702594465689a9f12438dfe92de3296f",
|
||||
"scripts/share_push.py": "42683c27a93f12208c20b0a5cd2feb7cd81f9be3efb76b46d4789ad4d2aab4ca",
|
||||
"share/README.md": "42ebed92285752af93c7c7af5eb4b6e84ed836408d0cedb57e1b32c796746b26",
|
||||
@@ -600,7 +600,7 @@
|
||||
"tests/test_license_gate.py": "ee44226b75620b3965ba52cc2a0cc5404fbff37822fd96a6faf2aed89d07d47e",
|
||||
"tests/test_luck.py": "821f98bc19ab1e920353de58c6ffacac5d757c887c690a2d596f51c1485b21ea",
|
||||
"tests/test_memory.py": "d3decfdc8d7c5bbfc4af5e3aed93bb3c53ab09071b682a0f4808614ac2cd3c6f",
|
||||
"tests/test_self_update.py": "455461ba121814c8cdbe98bb2d31cd2dac630ef111eafb23118583c42f65a71c",
|
||||
"tests/test_self_update.py": "183c2fb82d2d20285941b6c75a1890c4417d400c3c7e7d5f8a9680b4fa7bf76c",
|
||||
"tests/test_share.py": "399f20da22689d00e71eb78c76d60507060c29a7d68865e6b0e9f591d19be871",
|
||||
"tests/test_shensha.py": "b8a20d264e18c79f1fe546281381b0d929543af44ec68dc890ef8b64efc625b0",
|
||||
"tests/test_solar_time.py": "f072e5ea82fd0cb331c8a4fdc6dce08148cd9ac81ef9fe4506196fa68a42d1e4",
|
||||
@@ -1268,5 +1268,5 @@
|
||||
"vendor/tzdata/zoneinfo/zonenow.tab": "7f9d1c11445af7a17862c20a9081226d31a09a1730e270ff59a34c8d2b4c7f7f",
|
||||
"vendor/tzdata/zones": "28d8f3eb25839095fa8a209997bb20e822ebfbebe21e60edc483a3d5e20dc881"
|
||||
},
|
||||
"version": "3.5.0"
|
||||
"version": "3.5.1"
|
||||
}
|
||||
|
||||
+23
-5
@@ -12,12 +12,17 @@
|
||||
代理环境变量由 urllib 默认读取、自动生效。测试或自建场景可用环境变量
|
||||
ZIPEC_VERSION_URLS / ZIPEC_ZIPBALL_URLS(逗号分隔)整体覆盖远端地址,
|
||||
支持 file:// 形式。
|
||||
|
||||
抓取通道:urllib 优先;macOS 框架版 Python 常见的根证书缺失
|
||||
(SSL CERTIFICATE_VERIFY_FAILED)时自动回退到系统 curl(curl 自带
|
||||
系统信任库且自带代理环境变量支持)。
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
@@ -62,15 +67,28 @@ def _zipball_urls() -> list:
|
||||
return [GITHUB_ZIPBALL_URL, GITEA_ZIPBALL_URL]
|
||||
|
||||
|
||||
def _fetch_bytes(url: str, timeout: int) -> bytes:
|
||||
"""urllib 优先;SSL 证书缺失等失败时回退系统 curl(macOS 框架版 Python 常见)。"""
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=timeout) as r:
|
||||
return r.read()
|
||||
except Exception:
|
||||
curl = shutil.which("curl")
|
||||
if not curl:
|
||||
raise
|
||||
r = subprocess.run([curl, "-fsSL", "-m", str(timeout), url],
|
||||
capture_output=True)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(f"curl 抓取失败({r.returncode}):{url}")
|
||||
return r.stdout
|
||||
|
||||
|
||||
def _fetch_text(url: str, timeout: int = NETWORK_TIMEOUT) -> str:
|
||||
with urllib.request.urlopen(url, timeout=timeout) as r:
|
||||
return r.read().decode("utf-8")
|
||||
return _fetch_bytes(url, timeout).decode("utf-8")
|
||||
|
||||
|
||||
def _download(url: str, dest: Path, timeout: int = NETWORK_TIMEOUT) -> None:
|
||||
with urllib.request.urlopen(url, timeout=timeout) as r:
|
||||
with open(dest, "wb") as fh:
|
||||
shutil.copyfileobj(r, fh)
|
||||
dest.write_bytes(_fetch_bytes(url, timeout))
|
||||
|
||||
|
||||
def parse_version(text: str) -> tuple:
|
||||
|
||||
@@ -245,3 +245,36 @@ class TestCastChartIntegration:
|
||||
cache_path.unlink(missing_ok=True)
|
||||
else:
|
||||
cache_path.write_bytes(old_bytes)
|
||||
|
||||
|
||||
class TestFetchFallback:
|
||||
def test_curl_fallback_when_urllib_ssl_fails(self, tmp_path, monkeypatch):
|
||||
"""urllib 证书校验失败时回退系统 curl(macOS 框架版 Python 场景)。"""
|
||||
import urllib.request
|
||||
|
||||
def _boom(*a, **k):
|
||||
raise urllib.request.URLError("CERTIFICATE_VERIFY_FAILED")
|
||||
|
||||
monkeypatch.setattr(urllib.request, "urlopen", _boom)
|
||||
|
||||
fake_curl = tmp_path / "curl"
|
||||
fake_curl.write_text("#!/bin/sh\ncat \"$3\" | sed 's|^file://||' | xargs cat\n",
|
||||
encoding="utf-8")
|
||||
src = tmp_path / "VERSION"
|
||||
src.write_text("9.9.9\n", encoding="utf-8")
|
||||
fake_curl.write_text(f'#!/bin/sh\ncat "{src}"\n', encoding="utf-8")
|
||||
fake_curl.chmod(0o755)
|
||||
monkeypatch.setattr(su.shutil, "which", lambda name: str(fake_curl))
|
||||
|
||||
assert su._fetch_text("https://example.invalid/VERSION") == "9.9.9\n"
|
||||
|
||||
def test_no_curl_no_urllib_raises(self, monkeypatch):
|
||||
import urllib.request
|
||||
|
||||
def _boom(*a, **k):
|
||||
raise urllib.request.URLError("SSL")
|
||||
|
||||
monkeypatch.setattr(urllib.request, "urlopen", _boom)
|
||||
monkeypatch.setattr(su.shutil, "which", lambda name: None)
|
||||
with pytest.raises(Exception):
|
||||
su._fetch_text("https://example.invalid/VERSION")
|
||||
|
||||
Reference in New Issue
Block a user