fix: adb 命令超时 + STF 释放失败检查,杜绝设备卡死/占用悬空
- adb 所有命令加 30s 超时:connect 到不可达地址不再无限挂起(worker 不再卡 connecting) - STF release 检查响应状态并重试:504 等失败不再被静默吞掉,释放失败会真实上报 - release_all_mine 返回 (released, failed);web /api/release 透出失败列表
This commit is contained in:
+9
-1
@@ -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()
|
||||
|
||||
+12
-8
@@ -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 读取) ==================
|
||||
|
||||
+32
-14
@@ -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):
|
||||
|
||||
+2
-2
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user