修复:
- 检查更新可能失败 - 关闭代理时代理不会正确退出,表现为重启设备后代理设置异常 - 按关闭按钮关闭窗口时程序不会正确退出 新增功能: - 多节点切换支持 - 更好的 PAC 规则 - 更多的配置项分离加载
This commit is contained in:
+61
-13
@@ -313,8 +313,9 @@ def proxy_off():
|
|||||||
"""关闭系统代理并停止 TLS 转发器 / PAC 本地服务。"""
|
"""关闭系统代理并停止 TLS 转发器 / PAC 本地服务。"""
|
||||||
log("正在关闭系统代理...")
|
log("正在关闭系统代理...")
|
||||||
_run(["reg", "add", PROXY_REG, "/v", "ProxyEnable", "/t", "REG_DWORD", "/d", "0", "/f"])
|
_run(["reg", "add", PROXY_REG, "/v", "ProxyEnable", "/t", "REG_DWORD", "/d", "0", "/f"])
|
||||||
# 清除 ProxyServer 和 AutoConfigURL,确保 PAC / 固定代理两种模式都能正确关闭
|
# 清除 ProxyServer / ProxyOverride / AutoConfigURL,确保两种模式都清理干净
|
||||||
_run(["reg", "delete", PROXY_REG, "/v", "ProxyServer", "/f"], warn=False)
|
_run(["reg", "delete", PROXY_REG, "/v", "ProxyServer", "/f"], warn=False)
|
||||||
|
_run(["reg", "delete", PROXY_REG, "/v", "ProxyOverride", "/f"], warn=False)
|
||||||
_run(["reg", "delete", PROXY_REG, "/v", "AutoConfigURL", "/f"], warn=False)
|
_run(["reg", "delete", PROXY_REG, "/v", "AutoConfigURL", "/f"], warn=False)
|
||||||
log("系统代理已关闭")
|
log("系统代理已关闭")
|
||||||
_notify_proxy_change()
|
_notify_proxy_change()
|
||||||
@@ -375,10 +376,11 @@ def _traffic_snapshot():
|
|||||||
with _traffic_lock:
|
with _traffic_lock:
|
||||||
return _traffic["up"], _traffic["down"]
|
return _traffic["up"], _traffic["down"]
|
||||||
|
|
||||||
def _tls_start(remote_host, remote_port=443, use_tls=True, bypass_lan=False, smart=False):
|
def _tls_start(remote_host, remote_port=443, use_tls=True, bypass_lan=False, smart=False, quiet=False):
|
||||||
"""启动本地认证转发器:127.0.0.1:<local_port> -> remote_host:remote_port(可选 TLS 包裹)。
|
"""启动本地认证转发器:127.0.0.1:<local_port> -> remote_host:remote_port(可选 TLS 包裹)。
|
||||||
|
|
||||||
smart=True 时启用 IP 智能分流(域名表/非中国 IP 走隧道,中国 IP 直连)。
|
smart=True 时启用 IP 智能分流(域名表/非中国 IP 走隧道,中国 IP 直连)。
|
||||||
|
quiet=True 时不输出启动日志(临时转发器用)。
|
||||||
"""
|
"""
|
||||||
global _tls_forwarder
|
global _tls_forwarder
|
||||||
_tls_stop()
|
_tls_stop()
|
||||||
@@ -386,21 +388,24 @@ def _tls_start(remote_host, remote_port=443, use_tls=True, bypass_lan=False, sma
|
|||||||
# 服务器证书为 Let's Encrypt 正式证书,默认上下文完整校验
|
# 服务器证书为 Let's Encrypt 正式证书,默认上下文完整校验
|
||||||
ctx = ssl.create_default_context()
|
ctx = ssl.create_default_context()
|
||||||
|
|
||||||
svr = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
svr.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
||||||
# Windows 上 Hyper-V/WSL 的端口保留区间会动态变动,bind 可能瞬时被拒
|
# Windows 上 Hyper-V/WSL 的端口保留区间会动态变动,bind 可能瞬时被拒
|
||||||
# (WinError 10013/10048);重试 3 次,间隔 1 秒
|
# (WinError 10013/10048);重试 3 次,间隔 1 秒,每次用全新 socket
|
||||||
last_err = None
|
last_err = None
|
||||||
|
svr = None
|
||||||
for attempt in range(3):
|
for attempt in range(3):
|
||||||
|
svr = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
try:
|
try:
|
||||||
|
svr.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
svr.bind(("127.0.0.1", _TLS_LOCAL_PORT))
|
svr.bind(("127.0.0.1", _TLS_LOCAL_PORT))
|
||||||
break
|
break
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
last_err = e
|
last_err = e
|
||||||
|
svr.close()
|
||||||
|
svr = None
|
||||||
if attempt < 2:
|
if attempt < 2:
|
||||||
log(f"端口 {_TLS_LOCAL_PORT} 绑定失败: {e},1 秒后重试 ({attempt + 1}/3)")
|
log(f"端口 {_TLS_LOCAL_PORT} 绑定失败: {e},1 秒后重试 ({attempt + 1}/3)")
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
else:
|
if svr is None:
|
||||||
raise last_err
|
raise last_err
|
||||||
svr.listen(32)
|
svr.listen(32)
|
||||||
svr.settimeout(1)
|
svr.settimeout(1)
|
||||||
@@ -420,6 +425,7 @@ def _tls_start(remote_host, remote_port=443, use_tls=True, bypass_lan=False, sma
|
|||||||
|
|
||||||
t = threading.Thread(target=_run, daemon=True)
|
t = threading.Thread(target=_run, daemon=True)
|
||||||
t.start()
|
t.start()
|
||||||
|
if not quiet:
|
||||||
log(f"本地转发器已启动 ({proto} 127.0.0.1:{_TLS_LOCAL_PORT} -> {remote_host}:{remote_port})")
|
log(f"本地转发器已启动 ({proto} 127.0.0.1:{_TLS_LOCAL_PORT} -> {remote_host}:{remote_port})")
|
||||||
|
|
||||||
_HTTP_METHODS = (b"CONNECT ", b"GET ", b"POST ", b"PUT ", b"DELETE ",
|
_HTTP_METHODS = (b"CONNECT ", b"GET ", b"POST ", b"PUT ", b"DELETE ",
|
||||||
@@ -811,6 +817,19 @@ def _tls_stop():
|
|||||||
pass
|
pass
|
||||||
_tls_forwarder = None
|
_tls_forwarder = None
|
||||||
|
|
||||||
|
def _ensure_forwarder():
|
||||||
|
"""确保本地转发器在运行;未运行时启动临时全隧道转发器(静默),返回是否由本次启动。"""
|
||||||
|
if _tls_forwarder:
|
||||||
|
return False
|
||||||
|
_tls_start(_endpoint_domain(), _endpoint_port(), use_tls=True,
|
||||||
|
bypass_lan=False, smart=False, quiet=True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _release_forwarder(started_here):
|
||||||
|
"""停止由 _ensure_forwarder 启动的临时转发器;非本次启动则保持不动。"""
|
||||||
|
if started_here:
|
||||||
|
_tls_stop()
|
||||||
|
|
||||||
def is_proxy_on():
|
def is_proxy_on():
|
||||||
"""系统代理是否生效:固定代理已开启,或 AutoConfigURL 指向本地 PAC 服务。"""
|
"""系统代理是否生效:固定代理已开启,或 AutoConfigURL 指向本地 PAC 服务。"""
|
||||||
r = _run(["reg", "query", PROXY_REG, "/v", "ProxyEnable"], warn=False)
|
r = _run(["reg", "query", PROXY_REG, "/v", "ProxyEnable"], warn=False)
|
||||||
@@ -1113,8 +1132,10 @@ class MainWindow(QWidget):
|
|||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
"""关闭窗口时自动清理代理和隧道。"""
|
"""关闭窗口时自动清理代理和隧道。"""
|
||||||
|
try:
|
||||||
self.quit_app()
|
self.quit_app()
|
||||||
event.accept()
|
finally:
|
||||||
|
event.accept() # 即使清理异常窗口也必须关闭
|
||||||
|
|
||||||
# ── 开关操作 ──
|
# ── 开关操作 ──
|
||||||
def _set_toggle_button_style(self, is_start=True):
|
def _set_toggle_button_style(self, is_start=True):
|
||||||
@@ -1375,10 +1396,20 @@ class MainWindow(QWidget):
|
|||||||
return
|
return
|
||||||
url = f"https://{domain}/latest_version.json"
|
url = f"https://{domain}/latest_version.json"
|
||||||
log("正在检查更新...")
|
log("正在检查更新...")
|
||||||
|
# 显式走本地转发器隧道,不依赖系统代理状态:默认 opener 读注册表代理设置
|
||||||
|
# (异常退出残留 127.0.0.1 代理 -> 连接被拒),不识别 PAC 的 AutoConfigURL,
|
||||||
|
# 直连服务器还可能被墙干扰 TLS 握手(SSL UNEXPECTED_EOF)
|
||||||
|
started_here = _ensure_forwarder()
|
||||||
|
try:
|
||||||
|
opener = urllib.request.build_opener(urllib.request.ProxyHandler(
|
||||||
|
{"http": f"{PROXY_HOST}:{PROXY_PORT}",
|
||||||
|
"https": f"{PROXY_HOST}:{PROXY_PORT}"}))
|
||||||
req = urllib.request.Request(url)
|
req = urllib.request.Request(url)
|
||||||
auth = _proxy_auth_basic()
|
auth = _proxy_auth_basic()
|
||||||
req.add_header("Authorization", f"Basic {auth}")
|
req.add_header("Authorization", f"Basic {auth}")
|
||||||
data = urllib.request.urlopen(req, timeout=10).read().decode("utf-8-sig")
|
data = opener.open(req, timeout=10).read().decode("utf-8-sig")
|
||||||
|
finally:
|
||||||
|
_release_forwarder(started_here)
|
||||||
info = json.loads(data)
|
info = json.loads(data)
|
||||||
log("客户端认证成功")
|
log("客户端认证成功")
|
||||||
self._latest_version = info.get("latest_version", "")
|
self._latest_version = info.get("latest_version", "")
|
||||||
@@ -1421,12 +1452,20 @@ class MainWindow(QWidget):
|
|||||||
log(f"正在下载新版本: {url}")
|
log(f"正在下载新版本: {url}")
|
||||||
log(f"保存到: {save_path}")
|
log(f"保存到: {save_path}")
|
||||||
|
|
||||||
|
# 同更新检查:显式走本地转发器隧道,不依赖系统代理状态
|
||||||
|
started_here = _ensure_forwarder()
|
||||||
|
try:
|
||||||
|
opener = urllib.request.build_opener(urllib.request.ProxyHandler(
|
||||||
|
{"http": f"{PROXY_HOST}:{PROXY_PORT}",
|
||||||
|
"https": f"{PROXY_HOST}:{PROXY_PORT}"}))
|
||||||
req = urllib.request.Request(url)
|
req = urllib.request.Request(url)
|
||||||
auth = _proxy_auth_basic()
|
auth = _proxy_auth_basic()
|
||||||
req.add_header("Authorization", f"Basic {auth}")
|
req.add_header("Authorization", f"Basic {auth}")
|
||||||
|
|
||||||
with urllib.request.urlopen(req, timeout=120) as resp:
|
with opener.open(req, timeout=120) as resp:
|
||||||
data = resp.read()
|
data = resp.read()
|
||||||
|
finally:
|
||||||
|
_release_forwarder(started_here)
|
||||||
|
|
||||||
if len(data) < 1024:
|
if len(data) < 1024:
|
||||||
self._update_download_done.emit(False, "下载文件过小,可能不是有效的 EXE")
|
self._update_download_done.emit(False, "下载文件过小,可能不是有效的 EXE")
|
||||||
@@ -1450,12 +1489,15 @@ class MainWindow(QWidget):
|
|||||||
QMessageBox.warning(self, "下载失败", f"无法下载新版本:\n{message}")
|
QMessageBox.warning(self, "下载失败", f"无法下载新版本:\n{message}")
|
||||||
|
|
||||||
def quit_app(self):
|
def quit_app(self):
|
||||||
"""只清理自己设置的系统代理:用户或其他软件设置的代理不受影响。"
|
"""只清理自己设置的系统代理:用户或其他软件设置的代理不受影响。
|
||||||
|
|
||||||
本程序异常退出残留的代理设置由下次启动时 _cleanup_stale_proxy() 处理。
|
本程序异常退出残留的代理设置由下次启动时 _cleanup_stale_proxy() 处理。
|
||||||
"""
|
"""
|
||||||
if self._proxy_set_by_us:
|
if self._proxy_set_by_us:
|
||||||
|
try:
|
||||||
proxy_off()
|
proxy_off()
|
||||||
|
except Exception:
|
||||||
|
pass # 清理失败也不能阻止退出,残留交给下次启动清理
|
||||||
QApplication.quit()
|
QApplication.quit()
|
||||||
|
|
||||||
# ── 程序入口 ──────────────────────────────────────────
|
# ── 程序入口 ──────────────────────────────────────────
|
||||||
@@ -1491,9 +1533,12 @@ def _acquire_single_instance():
|
|||||||
"""通过命名互斥体保证单实例运行;已有实例时返回 False。"""
|
"""通过命名互斥体保证单实例运行;已有实例时返回 False。"""
|
||||||
global _SINGLE_INSTANCE_MUTEX
|
global _SINGLE_INSTANCE_MUTEX
|
||||||
try:
|
try:
|
||||||
kernel32 = ctypes.windll.kernel32
|
# 必须用 use_last_error 捕获真实错误码:直接调 GetLastError 可能被
|
||||||
|
# ctypes 内部调用重置(曾因此漏检已运行实例,导致双开互抢端口)
|
||||||
|
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
||||||
|
kernel32.CreateMutexW.restype = ctypes.c_void_p
|
||||||
_SINGLE_INSTANCE_MUTEX = kernel32.CreateMutexW(None, True, r"Local\RRPP_SingleInstance")
|
_SINGLE_INSTANCE_MUTEX = kernel32.CreateMutexW(None, True, r"Local\RRPP_SingleInstance")
|
||||||
return kernel32.GetLastError() != 183 # ERROR_ALREADY_EXISTS
|
return ctypes.get_last_error() != 183 # ERROR_ALREADY_EXISTS
|
||||||
except Exception:
|
except Exception:
|
||||||
return True # 互斥体创建失败时放行,避免把用户锁在门外
|
return True # 互斥体创建失败时放行,避免把用户锁在门外
|
||||||
|
|
||||||
@@ -1543,7 +1588,10 @@ def main():
|
|||||||
# 延迟重设一次图标兜底,避免首次启动任务栏图标丢失
|
# 延迟重设一次图标兜底,避免首次启动任务栏图标丢失
|
||||||
QTimer.singleShot(2000, lambda: window.setWindowIcon(icon))
|
QTimer.singleShot(2000, lambda: window.setWindowIcon(icon))
|
||||||
|
|
||||||
sys.exit(app.exec_())
|
# 退出必须彻底:此前点 X 后出现过进程残留(解释器收尾被残留线程/信号
|
||||||
|
# 竞态挂住),os._exit 直接终止进程——所有清理已在 quit_app 里完成
|
||||||
|
ret = app.exec_()
|
||||||
|
os._exit(ret)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
Reference in New Issue
Block a user