fix: 改善启动失败反馈、Ctrl+C 立退与 Windows 兼容性

- 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
  均可直接粘贴执行,同时用双引号包住路径兼容带空格的用户名
This commit is contained in:
jock
2026-04-20 13:53:45 +08:00
parent 8e073592a7
commit 3bf0b1aaa2
5 changed files with 59 additions and 13 deletions
+29 -6
View File
@@ -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)
+8 -2
View File
@@ -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):
+13 -2
View File
@@ -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 的证书,请手动安装。",
+3 -1
View File
@@ -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
+6 -2
View File
@@ -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