From 637915dc546f0d0788fe7aa7ba83427e08cdd6f1 Mon Sep 17 00:00:00 2001 From: Kahiy314 <1761724289@qq.com> Date: Wed, 8 Apr 2026 10:40:28 +0800 Subject: [PATCH] Improve tunnel startup failure handling The original tunnel bootstrap only waited for a line matching "--rsd HOST PORT". When the underlying pymobiledevice3 command exited early or failed before printing that line, the child process kept reading until the parent hit the 20-second timeout. Different root causes were therefore collapsed into the same generic tunnel failure, which made one-off environment issues hard to diagnose later. This commit keeps the upstream-facing behavior intact, including the existing 20-second timeout and raw subprocess output, but makes the failure path more explicit and easier to reason about. Changes: - run lockdown start-tunnel in script mode so successful output is easier to parse reliably - stop the child read loop when the subprocess exits without emitting an RSD address - forward recent subprocess output back to the parent so the real failure reason is visible - return early from main.py when tunnel startup fails, instead of continuing with an invalid process/address state - terminate the tunnel worker process defensively on failure to avoid leaving it running in the background Reason: - improve debugging when tunnel startup fails for transient causes such as permissions, trust state, or device readiness - avoid cascading follow-up errors after a failed tunnel bootstrap - keep the change narrow enough to stay suitable for a future upstream PR if needed --- init/tunnel.py | 82 ++++++++++++++++++++++++++++++++++++++++---------- main.py | 15 ++++++--- 2 files changed, 77 insertions(+), 20 deletions(-) diff --git a/init/tunnel.py b/init/tunnel.py index f5a6022..5d914ed 100644 --- a/init/tunnel.py +++ b/init/tunnel.py @@ -3,37 +3,72 @@ import logging import subprocess import multiprocessing +import queue as queue_module logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") +TUNNEL_START_TIMEOUT = 20 +RSD_PATTERNS = ( + re.compile(r"--rsd (\S+) (\d+)"), + re.compile(r"^(\S+)\s+(\d+)$"), +) + + +def parse_rsd_output(output): + for pattern in RSD_PATTERNS: + match = pattern.search(output) + if match: + return match.group(1), int(match.group(2)) + return None, None + + +def build_tunnel_error(return_code, output_lines): + if output_lines: + recent_output = " | ".join(output_lines[-5:]) + return f"start-tunnel exited before emitting an RSD address (exit code {return_code}). Recent output: {recent_output}" + return f"start-tunnel exited before emitting an RSD address (exit code {return_code})" + def start_tunnel(queue): - command = [sys.executable, '-m', 'pymobiledevice3', 'lockdown', 'start-tunnel'] + command = [sys.executable, '-m', 'pymobiledevice3', 'lockdown', 'start-tunnel', '--script-mode'] process = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - text=True + text=True, + bufsize=1, + errors='replace' ) logging.info("Tunnel started") - rsd_pattern = re.compile(r"--rsd (\S+) (\d+)") - address, port = None, None + if process.stdout is None: + queue.put(("error", "start-tunnel did not provide a readable output stream")) + return + + output_lines = [] while True: - output = process.stdout.readline().strip() - if output: - logging.info(output) + output = process.stdout.readline() + if output == "" and process.poll() is not None: + break - match = rsd_pattern.search(output) - if match: - address, port = match.group(1), int(match.group(2)) - queue.put((address, port)) + line = output.strip() + if not line: + continue + + output_lines.append(line) + logging.info(line) + + address, port = parse_rsd_output(line) + if address is not None: + queue.put(("ok", address, port)) logging.info(f"RSD Address: {address}, RSD Port: {port}") - break + process.wait() + return - process.wait() + return_code = process.wait() + queue.put(("error", build_tunnel_error(return_code, output_lines))) def tunnel(): queue = multiprocessing.Queue() @@ -41,12 +76,21 @@ def tunnel(): process.start() try: - result = queue.get(timeout=20) + result = queue.get(timeout=TUNNEL_START_TIMEOUT) if result is None: - raise RuntimeError("❌ 无法建立隧道连接") - address, port = result + raise RuntimeError("failed to establish tunnel connection") + status, *payload = result + if status != "ok": + raise RuntimeError(payload[0] if payload else "未知错误") + + address, port = payload return process, address, port + except queue_module.Empty: + logging.error( + f"❌ 隧道建立失败: timed out after {TUNNEL_START_TIMEOUT}s waiting for an RSD address. " + "Ensure the terminal is running as administrator, the device is unlocked and trusted, and run pymobiledevice3 mounter auto-mount first" + ) except Exception as e: logging.error(f"❌ 隧道建立失败: {e}") process.terminate() @@ -54,4 +98,10 @@ def tunnel(): if process.is_alive(): process.kill() + if process.is_alive(): + process.terminate() + process.join(timeout=2) + if process.is_alive(): + process.kill() + return None, None, None diff --git a/main.py b/main.py index 86bff17..69b688f 100644 --- a/main.py +++ b/main.py @@ -41,10 +41,15 @@ async def main(): init.init() logger.info("init done") + process = None logger.info("trying to start tunnel") original_sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN) process, address, port = tunnel.tunnel() signal.signal(signal.SIGINT, original_sigint_handler) + + if process is None or address is None or port is None: + return + try: logger.debug(f"tunnel address: {address}, port: {port}") @@ -66,10 +71,12 @@ async def main(): except KeyboardInterrupt: logger.debug("get KeyboardInterrupt (outer)") finally: - logger.debug(f"Is process alive? {process.is_alive()}") - logger.debug("terminating tunnel process") - process.terminate() - logger.info("tunnel process terminated") + if process is not None: + logger.debug(f"Is process alive? {process.is_alive()}") + if process.is_alive(): + logger.debug("terminating tunnel process") + process.terminate() + logger.info("tunnel process terminated") print("Bye")