合并远程变更: adbutils 改造 + 无线 ADB 支持 + TUI 优化
合并内容: - 远程: adbutils 库替换 subprocess、无线 ADB 连接、TUI 闪烁修复 - 本地: 审计日志、命令执行后验证重试、安装脚本 - 合并后保留双方特性: adbutils + 审计日志 + 无线 ADB + 命令验证
This commit is contained in:
+319
-124
@@ -16,14 +16,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
|
||||
|
||||
# ── 常量 ────────────────────────────────────────────────────────
|
||||
|
||||
# 注意: .resolve().parent 确保即使通过 symlink 调用也能正确找到真实目录
|
||||
@@ -39,7 +39,9 @@ DEFAULT_CONFIG = {
|
||||
"check_interval_seconds": 30,
|
||||
"auto_sleep_delay_minutes": 5,
|
||||
"adb_device_serial": "",
|
||||
"adb_path": "adb",
|
||||
"adb_wireless_host": "",
|
||||
"adb_wireless_port": 5555,
|
||||
"adb_wireless_auto_connect": False,
|
||||
}
|
||||
|
||||
# ── 日志 ────────────────────────────────────────────────────────
|
||||
@@ -256,74 +258,187 @@ class TimeChecker:
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# AdbManager — ADB 操作封装
|
||||
# AdbManager — ADB 操作封装(使用 adbutils 库)
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
class AdbManager:
|
||||
"""封装所有 ADB CLI 操作,所有异常都被捕获,不会导致进程退出。"""
|
||||
"""封装所有 ADB 操作,使用 adbutils 库实现持久连接。"""
|
||||
|
||||
def __init__(self, config_getter, audit_logger: AuditLogger | None = None):
|
||||
self._config_getter = config_getter
|
||||
self._audit = audit_logger
|
||||
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._current_serial: str | None = None # 自动检测到的设备串号
|
||||
self._wireless_addr: str = "" # 当前无线连接地址 (IP:PORT)
|
||||
self._wireless_retries: int = 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", "") or self._current_serial or ""
|
||||
|
||||
cmd = [adb_path]
|
||||
# "devices" / "--version" 命令不加 -s
|
||||
if serial and args and args[0] not in ("devices", "--version"):
|
||||
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
|
||||
|
||||
# ── 无线连接管理 ──
|
||||
|
||||
@staticmethod
|
||||
def is_wireless_serial(serial: str) -> bool:
|
||||
"""判断 serial 是否为无线格式(IP:PORT)。"""
|
||||
parts = serial.split(":")
|
||||
if len(parts) != 2:
|
||||
return False
|
||||
try:
|
||||
port = int(parts[1])
|
||||
ip_parts = parts[0].split(".")
|
||||
if len(ip_parts) == 4 and all(0 <= int(p) <= 255 for p in ip_parts):
|
||||
return True
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
return False
|
||||
|
||||
def connect_wireless(self, host: str, port: int = 5555, timeout: float = 5.0) -> tuple[bool, str]:
|
||||
"""连接无线 ADB 设备。返回 (success, message)。"""
|
||||
addr = f"{host}:{port}"
|
||||
try:
|
||||
client = self._ensure_client()
|
||||
result = client.connect(addr, timeout=timeout)
|
||||
if "connected" in result.lower():
|
||||
self._wireless_addr = addr
|
||||
self._wireless_retries = 0
|
||||
log.info("无线 ADB 已连接: %s", addr)
|
||||
return True, result
|
||||
return False, result
|
||||
except Exception as e:
|
||||
self._last_error = str(e)
|
||||
log.error("无线连接失败: %s", e)
|
||||
return False, str(e)
|
||||
|
||||
def disconnect_wireless(self, addr: str = "") -> bool:
|
||||
"""断开无线 ADB 连接。"""
|
||||
target = addr or self._wireless_addr
|
||||
if not target:
|
||||
return False
|
||||
try:
|
||||
client = self._ensure_client()
|
||||
client.disconnect(target)
|
||||
log.info("无线 ADB 已断开: %s", target)
|
||||
if target == self._wireless_addr:
|
||||
self._wireless_addr = ""
|
||||
return True
|
||||
except Exception as e:
|
||||
self._last_error = str(e)
|
||||
log.error("断开无线连接失败: %s", e)
|
||||
return False
|
||||
|
||||
def get_connection_type(self) -> str:
|
||||
"""返回当前连接类型:'usb' / 'wireless' / 'none'。"""
|
||||
if self._device is None:
|
||||
return "none"
|
||||
if self.is_wireless_serial(self._device.serial):
|
||||
return "wireless"
|
||||
return "usb"
|
||||
|
||||
def ensure_wireless_connection(self) -> bool:
|
||||
"""确保无线连接存活,失败时尝试重连。返回是否成功。"""
|
||||
config = self._config_getter()
|
||||
wireless_host = config.get("adb_wireless_host", "")
|
||||
wireless_port = config.get("adb_wireless_port", 5555)
|
||||
|
||||
if not wireless_host:
|
||||
return False
|
||||
|
||||
addr = f"{wireless_host}:{wireless_port}"
|
||||
|
||||
# 检查是否已有连接
|
||||
if self._device and self._device.serial == addr:
|
||||
try:
|
||||
self._device.getprop("ro.build.version.sdk")
|
||||
return True
|
||||
except Exception:
|
||||
# 连接已断开,尝试重连
|
||||
pass
|
||||
|
||||
# 尝试重连(最多 3 次)
|
||||
max_retries = 3
|
||||
for i in range(max_retries):
|
||||
self._wireless_retries += 1
|
||||
log.info("尝试重连无线设备 (%d/%d): %s", i + 1, max_retries, addr)
|
||||
success, msg = self.connect_wireless(wireless_host, wireless_port, timeout=3.0)
|
||||
if success:
|
||||
# 重新获取设备
|
||||
try:
|
||||
client = self._ensure_client()
|
||||
self._device = client.device(addr)
|
||||
self._device.getprop("ro.build.version.sdk")
|
||||
self._ready = True
|
||||
self._last_error = ""
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(2)
|
||||
|
||||
self._last_error = f"无线重连失败: {addr}"
|
||||
return False
|
||||
|
||||
# ── 设备管理 ──
|
||||
|
||||
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", "connection_type": "usb"/"wireless"}]。"""
|
||||
try:
|
||||
client = self._ensure_client()
|
||||
devices = client.device_list()
|
||||
result = []
|
||||
for d in devices:
|
||||
conn_type = "wireless" if self.is_wireless_serial(d.serial) else "usb"
|
||||
result.append({
|
||||
"serial": d.serial,
|
||||
"status": "device",
|
||||
"connection_type": conn_type,
|
||||
})
|
||||
return result
|
||||
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。"""
|
||||
for d in self.list_devices():
|
||||
if d["status"] == "device":
|
||||
return d["serial"]
|
||||
"""返回当前已连接设备的串号,无则返回 None。"""
|
||||
if self._device is not None:
|
||||
return self._device.serial
|
||||
return None
|
||||
|
||||
def is_ready(self) -> bool:
|
||||
@@ -334,53 +449,86 @@ class AdbManager:
|
||||
self._last_adb_check = now
|
||||
|
||||
config = self._config_getter()
|
||||
target = config.get("adb_device_serial", "")
|
||||
target_serial = config.get("adb_device_serial", "")
|
||||
wireless_host = config.get("adb_wireless_host", "")
|
||||
|
||||
devices = self.list_devices()
|
||||
for d in devices:
|
||||
if d["status"] == "device":
|
||||
if not target or d["serial"] == target:
|
||||
self._ready = True
|
||||
self._current_serial = d["serial"]
|
||||
self._last_error = f"已就绪: {d['serial']}"
|
||||
try:
|
||||
client = self._ensure_client()
|
||||
devices = client.device_list()
|
||||
|
||||
# USB 连接优先
|
||||
for d in devices:
|
||||
if not self.is_wireless_serial(d.serial):
|
||||
if not target_serial or d.serial == target_serial:
|
||||
d.getprop("ro.build.version.sdk")
|
||||
self._device = d
|
||||
self._current_serial = d.serial
|
||||
self._ready = True
|
||||
self._last_error = ""
|
||||
return True
|
||||
|
||||
# 尝试无线连接
|
||||
if wireless_host:
|
||||
wireless_port = config.get("adb_wireless_port", 5555)
|
||||
wireless_addr = f"{wireless_host}:{wireless_port}"
|
||||
|
||||
# 检查是否已在设备列表中
|
||||
for d in devices:
|
||||
if d.serial == wireless_addr:
|
||||
d.getprop("ro.build.version.sdk")
|
||||
self._device = d
|
||||
self._ready = True
|
||||
self._last_error = ""
|
||||
self._wireless_addr = wireless_addr
|
||||
return True
|
||||
|
||||
# 不在列表中,尝试连接
|
||||
if self.ensure_wireless_connection():
|
||||
return True
|
||||
|
||||
# 诊断
|
||||
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 设备"
|
||||
if not devices and not wireless_host:
|
||||
self._last_error = "未检测到 ADB 设备,请连接平板"
|
||||
elif not devices:
|
||||
self._last_error = f"无线设备未连接: {wireless_host}:{config.get('adb_wireless_port', 5555)}"
|
||||
else:
|
||||
self._last_error = f"未找到指定设备: {target_serial}"
|
||||
except Exception as e:
|
||||
self._last_error = str(e)
|
||||
|
||||
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:
|
||||
@@ -394,69 +542,82 @@ class AdbManager:
|
||||
def screen_off(self) -> bool:
|
||||
"""发送息屏命令,等待 2 秒后验证,失败时重试一次。"""
|
||||
log.info("执行息屏操作")
|
||||
if self._audit:
|
||||
self._audit.log("息屏", "发出息屏指令")
|
||||
rc, _, err = self._exec(["shell", "input", "keyevent", "26"])
|
||||
if rc != 0:
|
||||
log.error("息屏命令失败: %s", err)
|
||||
try:
|
||||
device = self._ensure_device()
|
||||
if device is None:
|
||||
log.error("息屏失败: 无可用设备")
|
||||
return False
|
||||
if self._audit:
|
||||
self._audit.log("息屏失败", err)
|
||||
return False
|
||||
self._audit.log("息屏", "发出息屏指令")
|
||||
device.shell("input keyevent 26")
|
||||
|
||||
# 等待并验证
|
||||
time.sleep(2)
|
||||
is_on, _ = self.get_screen_state()
|
||||
if is_on:
|
||||
log.warning("息屏命令已发出但屏幕未熄灭,重试一次")
|
||||
rc, _, err = self._exec(["shell", "input", "keyevent", "26"])
|
||||
# 等待并验证
|
||||
time.sleep(2)
|
||||
is_on, _ = self.get_screen_state()
|
||||
if is_on:
|
||||
log.error("息屏重试后仍未熄灭")
|
||||
if self._audit:
|
||||
self._audit.log("息屏异常", "屏幕状态未变化")
|
||||
# 不更新 _screen_on_since
|
||||
return False
|
||||
log.warning("息屏命令已发出但屏幕未熄灭,重试一次")
|
||||
device.shell("input keyevent 26")
|
||||
time.sleep(2)
|
||||
is_on, _ = self.get_screen_state()
|
||||
if is_on:
|
||||
log.error("息屏重试后仍未熄灭")
|
||||
self._last_error = "息屏验证失败"
|
||||
if self._audit:
|
||||
self._audit.log("息屏异常", "屏幕状态未变化")
|
||||
return False
|
||||
|
||||
self._screen_on_since = None
|
||||
log.info("息屏成功")
|
||||
if self._audit:
|
||||
self._audit.log("息屏成功", "屏幕已熄灭")
|
||||
return True
|
||||
self._screen_on_since = None
|
||||
log.info("息屏成功")
|
||||
if self._audit:
|
||||
self._audit.log("息屏成功", "屏幕已熄灭")
|
||||
return True
|
||||
except Exception as e:
|
||||
self._last_error = str(e)
|
||||
log.error("息屏失败: %s", e)
|
||||
if self._audit:
|
||||
self._audit.log("息屏失败", str(e))
|
||||
return False
|
||||
|
||||
def screen_on(self) -> bool:
|
||||
"""发送亮屏命令,等待 2 秒后验证,失败时重试一次。"""
|
||||
log.info("执行亮屏操作")
|
||||
if self._audit:
|
||||
self._audit.log("亮屏", "发出亮屏指令")
|
||||
rc, _, err = self._exec(["shell", "input", "keyevent", "26"])
|
||||
if rc != 0:
|
||||
log.error("亮屏命令失败: %s", err)
|
||||
try:
|
||||
device = self._ensure_device()
|
||||
if device is None:
|
||||
log.error("亮屏失败: 无可用设备")
|
||||
return False
|
||||
if self._audit:
|
||||
self._audit.log("亮屏失败", err)
|
||||
return False
|
||||
self._audit.log("亮屏", "发出亮屏指令")
|
||||
device.shell("input keyevent 26")
|
||||
|
||||
# 等待并验证
|
||||
time.sleep(2)
|
||||
is_on, _ = self.get_screen_state()
|
||||
if not is_on:
|
||||
log.warning("亮屏命令已发出但屏幕未点亮,重试一次")
|
||||
rc, _, err = self._exec(["shell", "input", "keyevent", "26"])
|
||||
# 等待并验证
|
||||
time.sleep(2)
|
||||
is_on, _ = self.get_screen_state()
|
||||
if not is_on:
|
||||
log.error("亮屏重试后仍未点亮")
|
||||
if self._audit:
|
||||
self._audit.log("亮屏异常", "屏幕状态未变化")
|
||||
return False
|
||||
log.warning("亮屏命令已发出但屏幕未点亮,重试一次")
|
||||
device.shell("input keyevent 26")
|
||||
time.sleep(2)
|
||||
is_on, _ = self.get_screen_state()
|
||||
if not is_on:
|
||||
log.error("亮屏重试后仍未点亮")
|
||||
self._last_error = "亮屏验证失败"
|
||||
if self._audit:
|
||||
self._audit.log("亮屏异常", "屏幕状态未变化")
|
||||
return False
|
||||
|
||||
self._screen_on_since = time.time()
|
||||
# 解锁屏幕
|
||||
self._exec(["shell", "input", "keyevent", "82"])
|
||||
log.info("亮屏成功")
|
||||
if self._audit:
|
||||
self._audit.log("亮屏成功", "屏幕已点亮")
|
||||
return True
|
||||
self._screen_on_since = time.time()
|
||||
# 解锁屏幕
|
||||
device.shell("input keyevent 82")
|
||||
log.info("亮屏成功")
|
||||
if self._audit:
|
||||
self._audit.log("亮屏成功", "屏幕已点亮")
|
||||
return True
|
||||
except Exception as e:
|
||||
self._last_error = str(e)
|
||||
log.error("亮屏失败: %s", e)
|
||||
if self._audit:
|
||||
self._audit.log("亮屏失败", str(e))
|
||||
return False
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
@@ -576,6 +737,9 @@ class PadSleepApp:
|
||||
status["check_interval"] = cfg.get("check_interval_seconds", 30)
|
||||
status["config_path"] = str(self.config_mgr.path)
|
||||
status["log_path"] = str(LOG_PATH)
|
||||
status["connection_type"] = self.adb.get_connection_type()
|
||||
status["wireless_host"] = cfg.get("adb_wireless_host", "")
|
||||
status["wireless_port"] = cfg.get("adb_wireless_port", 5555)
|
||||
return {"ok": True, "data": status}
|
||||
|
||||
if cmd == "get_config":
|
||||
@@ -621,6 +785,25 @@ class PadSleepApp:
|
||||
n = request.get("lines", 50)
|
||||
return {"ok": True, "data": {"log": self.audit.read_tail(n)}}
|
||||
|
||||
if cmd == "connect_wireless":
|
||||
host = request.get("host", "")
|
||||
port = request.get("port", 5555)
|
||||
if not host:
|
||||
return {"ok": False, "error": "请提供 IP 地址"}
|
||||
success, msg = self.adb.connect_wireless(host, int(port))
|
||||
if success:
|
||||
# 更新配置
|
||||
self.config_mgr.update({
|
||||
"adb_wireless_host": host,
|
||||
"adb_wireless_port": int(port),
|
||||
})
|
||||
return {"ok": success, "data": {"message": msg}}
|
||||
|
||||
if cmd == "disconnect_wireless":
|
||||
addr = request.get("addr", "")
|
||||
ok = self.adb.disconnect_wireless(addr)
|
||||
return {"ok": ok}
|
||||
|
||||
return {"ok": False, "error": f"未知命令: {cmd}"}
|
||||
|
||||
# ── 主循环 ──
|
||||
@@ -643,6 +826,18 @@ class PadSleepApp:
|
||||
else:
|
||||
log.info("ADB 可执行文件已就绪")
|
||||
|
||||
# 自动连接无线设备
|
||||
config = self.config_mgr.get()
|
||||
if config.get("adb_wireless_auto_connect") and config.get("adb_wireless_host"):
|
||||
host = config["adb_wireless_host"]
|
||||
port = config.get("adb_wireless_port", 5555)
|
||||
log.info("尝试自动连接无线设备: %s:%d", host, port)
|
||||
success, msg = self.adb.connect_wireless(host, port, timeout=5.0)
|
||||
if success:
|
||||
log.info("无线设备已连接: %s", msg)
|
||||
else:
|
||||
log.warning("无线设备连接失败: %s", msg)
|
||||
|
||||
# 启动 IPC
|
||||
self.ipc.start()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user