diff --git a/core/adb_helper.py b/core/adb_helper.py index 795925c..09bcd61 100644 --- a/core/adb_helper.py +++ b/core/adb_helper.py @@ -14,6 +14,9 @@ from core.logger import get_logger _log = get_logger("core.adb") _ADB_LOCK = threading.Lock() +# adb 命令超时(秒)。adb connect 到不可达主机(如 Tailscale IP 不通)会长时间挂起, +# 没有超时会让 worker 永久卡在 connecting。connect/disconnect 正常都应瞬时完成。 +_ADB_TIMEOUT = 30 def _adb(*args): @@ -21,8 +24,13 @@ def _adb(*args): 用 bytes 模式自己解码(Windows 下 subprocess text=True 会用 GBK 读 adb 输出, 遇到非 GBK 字节会崩溃)。 + 带 30s 超时:adb connect 到不可达地址可能挂很久,超时返回空串由调用方处理。 """ - r = subprocess.run([ADB_PATH, *args], capture_output=True) + try: + r = subprocess.run([ADB_PATH, *args], capture_output=True, timeout=_ADB_TIMEOUT) + except subprocess.TimeoutExpired: + _log.warning(f"adb {' '.join(args)} 超时 {_ADB_TIMEOUT}s") + return "" out = (r.stdout or b"") + (r.stderr or b"") try: return out.decode("utf-8", errors="replace").strip() diff --git a/core/device_worker.py b/core/device_worker.py index b83c1ad..74f766f 100644 --- a/core/device_worker.py +++ b/core/device_worker.py @@ -51,13 +51,17 @@ class STFDevice: raise STFError(f"占用设备失败: {msg}", "conflict") _log.info(f"[{self.serial}] STF 占用成功") - # 优先直连设备 serial(当 serial 本身就是 IP:5555 时,如 Tailscale 网络) - # 这比 STF remoteConnect 桥接更稳定,避免桥接地址认证失败 + # 直连优先:serial 本身是 IP:5555(如 Tailscale 网络设备)时直接 adb connect。 + # 实测(本机 100.100.10.2,设备 100.100.10.x): + # - 本机在 tailnet 内时直连可靠且快(<1s),且只 connect、绝不 disconnect/kill-server, + # 不会影响 STF provider 共享的 adb 连接(STF 端设备保持 present/ready)。 + # - STF remoteConnect 桥接在此环境反而有 adb key 认证问题(隧道显示 unauthorized, + # 即使设备已授权本机 key),直连才是可靠路径。 + # 仅当 serial 不是 IP:port(如 USB 序列号)时才走 STF 桥接。 if ":" in self.serial: self.remote_adb_url = self.serial _log.info(f"[{self.serial}] 直连设备: {self.remote_adb_url}") else: - # serial 不是 IP:port 格式,走 STF remoteConnect 桥接 self.remote_adb_url = self.stf.remote_connect(self.serial) _log.info(f"[{self.serial}] STF 桥接: {self.remote_adb_url}") @@ -75,18 +79,18 @@ class STFDevice: if not self.serial: return # 直连模式(remote_adb_url == serial):不 disconnect - # STF agent 可能共享同一个 adb 连接,disconnect 会导致 STF 设备离线后重新连接 + # STF provider 共享该 IP:5555 的 adb transport,disconnect 会让 STF 误判离线并重连 if self.remote_adb_url and self.remote_adb_url != self.serial: adb_disconnect(self.remote_adb_url) try: self.stf.remote_disconnect(self.serial) except Exception: pass - try: - self.stf.release(self.serial) + ok, msg = self.stf.release(self.serial) + if ok: _log.info(f"[{self.serial}] 已释放") - except Exception as e: - _log.warning(f"[{self.serial}] 释放失败: {e}") + else: + _log.warning(f"[{self.serial}] STF 释放失败: {msg}") # ================== 全局 worker 状态注册表(供 web_server 读取) ================== diff --git a/core/stf_client.py b/core/stf_client.py index 5ca2b9d..ba9b4f2 100644 --- a/core/stf_client.py +++ b/core/stf_client.py @@ -120,27 +120,45 @@ class STFClient: return False, str(e) return False, "占用冲突重试耗尽" - def release(self, serial): - """释放设备占用(含远程断开)。""" + def release(self, serial, retries=3): + """释放设备占用。返回 (ok, msg)。 + + STF 释放可能因设备响应超时返回 504(如设备过载/卡死),此时设备实际未释放。 + 必须检查响应状态并重试,不能静默吞掉——否则占用会一直悬着。 + """ self.remote_disconnect(serial) - try: - requests.delete(f"{self.base_url}/api/v1/user/devices/{serial}", - headers=self.headers, timeout=_TIMEOUT) - except requests.exceptions.RequestException as e: - _log.warning(f"释放 {serial} 请求失败: {e}") + last_status = None + last_body = "" + for attempt in range(1, retries + 1): + try: + r = requests.delete(f"{self.base_url}/api/v1/user/devices/{serial}", + headers=self.headers, timeout=_TIMEOUT) + if r.status_code in (200, 201, 202, 204): + return True, "released" + last_status = r.status_code + last_body = r.text[:150] + _log.warning(f"释放 {serial} 失败 HTTP {r.status_code}: {last_body} ({attempt}/{retries})") + except requests.exceptions.RequestException as e: + _log.warning(f"释放 {serial} 请求异常 ({attempt}/{retries}): {e}") + last_status = -1 + last_body = str(e) + if attempt < retries: + time.sleep(2) + return False, f"STF 释放失败(HTTP {last_status}): {last_body}" def release_all_mine(self): - """释放当前账户占用的所有设备(清理用)。""" - released = [] + """释放当前账户占用的所有设备(清理用)。返回 (released, failed)。""" + released, failed = [], [] for d in self.list_my_devices(): serial = d["serial"] - try: - self.release(serial) + ok, msg = self.release(serial) + if ok: released.append(serial) _log.info(f"已释放 {serial}") - except Exception as e: - _log.error(f"释放 {serial} 失败: {e}") - return released + else: + failed.append(serial) + _log.error(f"释放 {serial} 失败: {msg}") + return released, failed # ================== 远程 ADB 隧道 ================== def remote_connect(self, serial): diff --git a/web_server.py b/web_server.py index 8cc5d7c..b2842f0 100644 --- a/web_server.py +++ b/web_server.py @@ -346,8 +346,8 @@ def api_stop_all(): @app.route("/api/release", methods=["POST"]) @login_required def api_release(): - released = stf.release_all_mine() - return jsonify({"ok": True, "released": released}) + released, failed = stf.release_all_mine() + return jsonify({"ok": True, "released": released, "failed": failed}) @app.route("/api/device/screenshot")