From 3bf0b1aaa23ffcf66d8405a22acaad75aee9352a Mon Sep 17 00:00:00 2001 From: jock <156505552+jooooock@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:53:45 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=94=B9=E5=96=84=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E5=8F=8D=E9=A6=88=E3=80=81Ctrl+C=20=E7=AB=8B?= =?UTF-8?q?=E9=80=80=E4=B8=8E=20Windows=20=E5=85=BC=E5=AE=B9=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - main: Ctrl+C 时直接 SIGKILL 所有 daemon 子进程,跳过 mitmproxy graceful shutdown 与 mitmdump 重启循环里的 time.sleep(3),实现秒退 - main: 启动失败时透传子进程真实错误行(端口/CA 缺失/文件权限等),告别"请切换端口后重试" 一刀切提示;Windows 下等用户按回车再退出,防止 PowerShell/双击启动窗口闪退 - mitm: start() 返回 (proxy_address, error_line);mitmdump 重启循环处理 KeyboardInterrupt - watcher: start() 返回 (ws_address, queue, error_line);将 wss 证书生成失败纳入快速失败识别 - utils: check_system_proxy 修复 KeyError — Windows 下 getproxies() 可能返回仅含 'no' 等 非 http/https key 的字典,旧写法 proxy_obj.keys() < {'http', 'https'} 是真子集判断, 会漏掉这些场景继续访问 proxy_obj['http'] 引发崩溃 - ui/startup: 证书安装命令里 %userprofile% 在 PowerShell 下不会展开导致 certutil 报 ERROR_PATH_NOT_FOUND;改为 Python 侧展开为绝对路径,cmd/PowerShell/bash/zsh 均可直接粘贴执行,同时用双引号包住路径兼容带空格的用户名 --- main.py | 35 +++++++++++++++++++++++++++++------ mitm.py | 10 ++++++++-- ui/startup.py | 15 +++++++++++++-- utils.py | 4 +++- watcher.py | 8 ++++++-- 5 files changed, 59 insertions(+), 13 deletions(-) diff --git a/main.py b/main.py index 60516dc..aa108e8 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,6 @@ import argparse import multiprocessing +import platform import sys import mitm @@ -9,6 +10,22 @@ from ui.console import console from ui.startup import startup_ui_loop +def _fail(title: str, reason: str | None): + """打印真实失败原因并在 Windows 上等待用户确认,避免 PowerShell/双击启动时窗口闪退。""" + console.print(f'[bold red]{title}[/]') + if reason: + console.print(f'原因: {reason}') + else: + console.print('未捕获到具体错误信息,请查看 resources/logs/wxdown.log 或加 -d 重试') + # Windows 下若 stdin 可读,等用户按回车再退出 + if platform.system() == 'Windows': + try: + input('按回车键退出...') + except Exception: + pass + sys.exit(1) + + def main(): # 命令行参数解析 parser = argparse.ArgumentParser(prog='wxdown-service', description='微信公众号下载助手') @@ -20,16 +37,14 @@ def main(): # 启动 mitmproxy 进程 - mitm_proxy_address = mitm.start(args.port, args.debug) + mitm_proxy_address, mitm_err = mitm.start(args.port, args.debug) if mitm_proxy_address is None: - console.print('[bold red]启动 mitmproxy 失败,请切换端口后重试[/]') - sys.exit(1) + _fail('启动 mitmproxy 失败', mitm_err) # 启动文件监控及 ws 服务进程 - ws_address, watcher_output_queue = watcher.start(args.wport) + ws_address, watcher_output_queue, watcher_err = watcher.start(args.wport) if ws_address is None: - console.print('[bold red]启动 watcher 失败,请切换端口后重试[/]') - sys.exit(1) + _fail('启动 watcher 失败', watcher_err) # 启动 UI startup_ui_loop(watcher_output_queue, mitm_proxy_address, ws_address) @@ -42,3 +57,11 @@ if __name__ == '__main__': main() except KeyboardInterrupt: print("Ctrl+C pressed, exiting.") + # 直接 SIGKILL 掉所有 daemon 子进程,跳过 mitmproxy/websockets 的 graceful shutdown + # 以及 mitm.py 里 mitmdump 重启循环中的 time.sleep(3),实现立即退出 + for p in multiprocessing.active_children(): + try: + p.kill() + except Exception: + pass + sys.exit(0) diff --git a/mitm.py b/mitm.py index 1cebe29..d171492 100644 --- a/mitm.py +++ b/mitm.py @@ -24,7 +24,11 @@ def mitmproxy_process(args: list[str], output_queue: multiprocessing.Queue): os.environ.pop(k, None) while True: print(f'Run mitmdump process {args} ({os.getpid()})...', flush=True) - mitmdump(args) + try: + mitmdump(args) + except KeyboardInterrupt: + # mitmdump 通常会吞掉 SIGINT 然后正常 return;这里是为了处理它透传上来的极少数情况 + break logger.info(f'mitmdump process terminated') time.sleep(3) logger.info(f'重启 mitm 进程') @@ -39,6 +43,7 @@ def start(port: str, debug = False): start_time = time.time() proxy_address = None + error_line = None while time.time() - start_time < 10: try: @@ -50,13 +55,14 @@ def start(port: str, debug = False): proxy_address = f"http://127.0.0.1:{port}" break elif "address already in use" in line.lower(): + error_line = line break except queue.Empty: pass threading.Thread(target=log_mitmproxy_output, args=(mitm_output_queue, debug), daemon=True).start() - return proxy_address + return proxy_address, error_line def log_mitmproxy_output(mitm_output_queue: multiprocessing.Queue, debug): diff --git a/ui/startup.py b/ui/startup.py index 8288828..f8c01fd 100644 --- a/ui/startup.py +++ b/ui/startup.py @@ -1,4 +1,5 @@ import multiprocessing +from pathlib import Path from rich.layout import Layout @@ -34,10 +35,20 @@ def startup_ui_loop(watcher_output_queue: multiprocessing.Queue, mitm_proxy_addr # 检查证书是否安装 try: if not cert.is_certificate_installed('mitmproxy'): + # 用 Python 先把 ~ / %userprofile% 展开为绝对路径,避免 + # PowerShell 不认 %userprofile%、导致 certutil 报 + # ERROR_PATH_NOT_FOUND 的经典坑。这条命令在 cmd / PowerShell / + # Windows Terminal / bash / zsh 下都能直接粘贴执行。 + mitm_dir = Path.home() / '.mitmproxy' if platform.system() == 'Windows': - cmd = 'certutil -addstore root %userprofile%\\.mitmproxy\\mitmproxy-ca-cert.cer' + cer_path = mitm_dir / 'mitmproxy-ca-cert.cer' + cmd = f'certutil -addstore root "{cer_path}"' elif platform.system() == 'Darwin': - cmd = 'sudo security add-trusted-cert -d -p ssl -p basic -k /Library/Keychains/System.keychain ~/.mitmproxy/mitmproxy-ca-cert.pem' + pem_path = mitm_dir / 'mitmproxy-ca-cert.pem' + cmd = (f'sudo security add-trusted-cert -d -p ssl -p basic ' + f'-k /Library/Keychains/System.keychain "{pem_path}"') + else: + cmd = '(当前系统暂不支持自动给出安装命令)' layout['status'].update( StatusPanel(is_success=False, reason="系统中未检测到 mitmproxy 的证书,请手动安装。", diff --git a/utils.py b/utils.py index 19751bb..6bc732c 100644 --- a/utils.py +++ b/utils.py @@ -19,7 +19,9 @@ def check_system_proxy(mitm_proxy_address): proxy_obj = urllib.request.getproxies() details = f'将系统代理设置为 [bold green]{mitm_proxy_address.removeprefix('http://')}[/]\n当前系统代理为:\n{proxy_obj}' - if proxy_obj.keys() < {'http', 'https'}: + # 注意:Windows 下 urllib.request.getproxies() 可能返回 {'no': '...'}(ProxyOverride 例外列表) + # 等非 http/https 的 key;所以只能正向判断,不能用 "<"(真子集)判断。 + if 'http' not in proxy_obj or 'https' not in proxy_obj: return False, '检测到系统的网络代理设置不正确(http/https 都需要设置)', details if proxy_obj['http'] != mitm_proxy_address or proxy_obj['https'] != mitm_proxy_address: return False, '检测到系统的网络代理未指向 mitmproxy', details diff --git a/watcher.py b/watcher.py index afe45fd..bd1dc88 100644 --- a/watcher.py +++ b/watcher.py @@ -166,6 +166,7 @@ def start(port: str): start_time = time.time() ws_address = None + error_line = None while time.time() - start_time < 10: try: @@ -175,9 +176,12 @@ def start(port: str): port = match.group(1) ws_address = f"wss://127.0.0.1:{port}" break - elif "address already in use" in line.lower() or "watcher启动失败" in line: + elif ("address already in use" in line.lower() + or "watcher启动失败" in line + or "wss 证书启动失败" in line): + error_line = line break except queue.Empty: pass - return ws_address, watcher_output_queue + return ws_address, watcher_output_queue, error_line