diff --git a/VERSION b/VERSION index 1545d96..d5c0c99 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.5.0 +3.5.1 diff --git a/manifest.json b/manifest.json index eaecac6..e8f1e54 100644 --- a/manifest.json +++ b/manifest.json @@ -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" } diff --git a/scripts/self_update.py b/scripts/self_update.py index 399fc98..8e66ea5 100644 --- a/scripts/self_update.py +++ b/scripts/self_update.py @@ -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: diff --git a/tests/test_self_update.py b/tests/test_self_update.py index 1e175b6..411bcaf 100644 --- a/tests/test_self_update.py +++ b/tests/test_self_update.py @@ -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")