将 ADB 调用从 subprocess 改造为 adbutils 库

- 移除 subprocess.run 调用,改用 adbutils 实现持久连接
- 添加 adbutils>=2.12.0 到 requirements.txt
- 更新 .gitignore 添加 Python 包相关条目
- 保持原有公共接口不变,确保兼容性
This commit is contained in:
2026-07-10 09:11:43 +08:00
parent 4d0db4c390
commit 203d5d15ef
3 changed files with 135 additions and 83 deletions
+108 -82
View File
@@ -14,14 +14,14 @@ import logging
import os
import signal
import socket
import subprocess
import sys
import threading
import time
from datetime import datetime
from pathlib import Path
from socketserver import ThreadingUnixStreamServer, StreamRequestHandler
from adbutils import AdbClient, AdbDevice
# ── 常量 ────────────────────────────────────────────────────────
SCRIPT_DIR = Path(__file__).parent.resolve()
@@ -167,68 +167,72 @@ class TimeChecker:
# ═══════════════════════════════════════════════════════════════════
# AdbManager — ADB 操作封装
# AdbManager — ADB 操作封装(使用 adbutils 库)
# ═══════════════════════════════════════════════════════════════════
class AdbManager:
"""封装所有 ADB CLI 操作,所有异常都被捕获,不会导致进程退出"""
"""封装所有 ADB 操作,使用 adbutils 库实现持久连接"""
def __init__(self, config_getter):
self._config_getter = config_getter
self._client: AdbClient | None = None
self._device: AdbDevice | None = None
self._ready = False
self._last_error = ""
self._screen_on_since: float | None = None
self._last_known_screen_on = False
self._last_adb_check = 0 # 限频用
self._last_adb_check = 0
# ── 底层命令执行 ──
# ── 连接管理 ──
def _exec(self, args: list, timeout: int = 10) -> tuple:
"""执行 ADB 命令,返回 (returncode, stdout, stderr)。永不抛出"""
def _ensure_client(self) -> AdbClient:
"""懒初始化 ADB 客户端连接"""
if self._client is None:
self._client = AdbClient(host="127.0.0.1", port=5037)
return self._client
def _ensure_device(self) -> AdbDevice | None:
"""获取目标设备,支持串号过滤。"""
config = self._config_getter()
adb_path = config.get("adb_path", "adb")
serial = config.get("adb_device_serial", "")
cmd = [adb_path]
if serial:
cmd.extend(["-s", serial])
cmd.extend(args)
target_serial = config.get("adb_device_serial", "")
try:
r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
return r.returncode, r.stdout.strip(), r.stderr.strip()
except FileNotFoundError:
self._ready = False
self._last_error = f"ADB 未找到({adb_path}),请安装: sudo apt install adb"
except subprocess.TimeoutExpired:
self._ready = False
self._last_error = "ADB 命令超时"
client = self._ensure_client()
if target_serial:
self._device = client.device(target_serial)
else:
devices = client.device_list()
self._device = devices[0] if devices else None
return self._device
except Exception as e:
self._ready = False
self._device = None
self._last_error = str(e)
return -1, "", self._last_error
return None
# ── 设备管理 ──
def check_available(self) -> bool:
"""检查 ADB 是否可执行"""
rc, _, _ = self._exec(["--version"], timeout=5)
return rc == 0
"""检查 ADB Server 是否可连接"""
try:
client = self._ensure_client()
client.device_list()
return True
except Exception as e:
self._last_error = f"ADB Server 不可用: {e}"
return False
def list_devices(self) -> list[dict]:
"""返回 [{"serial": "...", "status": "..."}]。"""
rc, out, _ = self._exec(["devices"])
if rc != 0:
"""返回 [{"serial": "...", "status": "device"}]。"""
try:
client = self._ensure_client()
devices = client.device_list()
return [{"serial": d.serial, "status": "device"} for d in devices]
except Exception as e:
self._last_error = str(e)
return []
devices = []
for line in out.split("\n"):
parts = line.strip().split("\t")
if len(parts) >= 2 and not line.startswith("List"):
devices.append({"serial": parts[0], "status": parts[1]})
return devices
def get_ready_device(self) -> str | None:
"""返回第一个状态为 device 的串号,无则返回 None。"""
"""返回第一个已连接设备的串号,无则返回 None。"""
for d in self.list_devices():
if d["status"] == "device":
return d["serial"]
@@ -237,57 +241,67 @@ class AdbManager:
def is_ready(self) -> bool:
"""检查是否有已授权的 ADB 设备可用。"""
now = time.time()
# 限频:每秒最多检测一次
if now - self._last_adb_check < 1 and self._ready:
return True
self._last_adb_check = now
config = self._config_getter()
target = config.get("adb_device_serial", "")
target_serial = config.get("adb_device_serial", "")
devices = self.list_devices()
for d in devices:
if d["status"] == "device" and (not target or d["serial"] == target):
self._ready = True
self._last_error = ""
return True
try:
client = self._ensure_client()
devices = client.device_list()
for d in devices:
if not target_serial or d.serial == target_serial:
# 尝试获取设备属性以验证连接
d.getprop("ro.build.version.sdk")
self._device = d
self._ready = True
self._last_error = ""
return True
if not devices:
self._last_error = "未检测到 ADB 设备,请连接平板"
else:
self._last_error = f"未找到指定设备: {target_serial}"
except Exception as e:
self._last_error = str(e)
# 诊断
unauthorized = [d for d in devices if d["status"] == "unauthorized"]
if unauthorized:
self._last_error = f"设备未授权: {[d['serial'] for d in unauthorized]},请在平板上确认授权"
elif not devices:
self._last_error = "未检测到 ADB 设备,请连接平板"
else:
self._last_error = "没有已就绪的 ADB 设备"
self._ready = False
self._device = None
return False
# ── 屏幕控制 ──
def get_screen_state(self) -> tuple[bool, str]:
"""返回 (is_on, wakefulness),失败时返回上次已知状态。"""
rc, out, _ = self._exec(["shell", "dumpsys", "power"])
if rc != 0:
return self._last_known_screen_on, "unknown"
try:
device = self._ensure_device()
if device is None:
return self._last_known_screen_on, "unknown"
for line in out.split("\n"):
line = line.strip()
if "mWakefulness" in line:
state = line.split("=")[-1].strip()
is_on = state == "Awake"
self._last_known_screen_on = is_on
if is_on and self._screen_on_since is None:
self._screen_on_since = time.time()
elif not is_on:
self._screen_on_since = None
return is_on, state
output = device.shell("dumpsys power")
for line in output.split("\n"):
line = line.strip()
if "mWakefulness" in line:
state = line.split("=")[-1].strip()
is_on = state == "Awake"
self._last_known_screen_on = is_on
if is_on and self._screen_on_since is None:
self._screen_on_since = time.time()
elif not is_on:
self._screen_on_since = None
return is_on, state
# fallback
for line in output.split("\n"):
if "Display Power" in line or "mScreenOn" in line:
is_on = "ON" in line.upper() or "=true" in line.lower()
return is_on, "unknown"
except Exception as e:
self._last_error = str(e)
# fallback
for line in out.split("\n"):
if "Display Power" in line or "mScreenOn" in line:
is_on = "ON" in line.upper() or "=true" in line.lower()
return is_on, "unknown"
return self._last_known_screen_on, "unknown"
def get_screen_on_duration(self) -> float:
@@ -299,23 +313,35 @@ class AdbManager:
def screen_off(self) -> bool:
"""发送息屏命令。"""
log.info("执行息屏操作")
rc, _, err = self._exec(["shell", "input", "keyevent", "26"])
if rc == 0:
try:
device = self._ensure_device()
if device is None:
log.error("息屏失败: 无可用设备")
return False
device.shell("input keyevent 26")
self._screen_on_since = None
return True
log.error("息屏失败: %s", err)
return False
except Exception as e:
self._last_error = str(e)
log.error("息屏失败: %s", e)
return False
def screen_on(self) -> bool:
"""发送亮屏并解锁命令。"""
log.info("执行亮屏操作")
rc, _, err = self._exec(["shell", "input", "keyevent", "26"])
if rc == 0:
try:
device = self._ensure_device()
if device is None:
log.error("亮屏失败: 无可用设备")
return False
device.shell("input keyevent 26")
self._screen_on_since = time.time()
self._exec(["shell", "input", "keyevent", "82"]) # 解锁(MENU
device.shell("input keyevent 82") # 解锁(MENU
return True
log.error("亮屏失败: %s", err)
return False
except Exception as e:
self._last_error = str(e)
log.error("亮屏失败: %s", e)
return False
# ═══════════════════════════════════════════════════════════════════