autotrain/nextgen/autotrain_next/device.py
2026-07-24 10:37:32 +08:00

93 lines
3.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""唯一设备选择与原子 ADB 操作。绝不允许无 serial 执行手机指令。"""
from __future__ import annotations
import re
import subprocess
from dataclasses import dataclass
from typing import Iterable
from .platform import adb_binary
class DeviceError(RuntimeError):
pass
@dataclass(frozen=True)
class Device:
serial: str
state: str
model: str = ""
def raw_adb(arguments: Iterable[str], *, text: bool = True) -> subprocess.CompletedProcess:
try:
return subprocess.run([adb_binary(), *arguments], capture_output=True, text=text, check=False)
except FileNotFoundError as exc:
raise DeviceError("未找到 adb。请将 Android Platform Tools 加入 PATH或设置 AUTOTRAIN_ADB。") from exc
def devices() -> list[Device]:
result = raw_adb(["devices", "-l"])
if result.returncode:
raise DeviceError(result.stderr.strip() or "adb devices 执行失败")
found: list[Device] = []
for line in result.stdout.splitlines()[1:]:
pieces = line.split()
if len(pieces) < 2:
continue
attributes = dict(item.split(":", 1) for item in pieces[2:] if ":" in item)
found.append(Device(pieces[0], pieces[1], attributes.get("model", "")))
return found
def selected_device(serial: str = "") -> Device:
usable = [item for item in devices() if item.state == "device"]
if serial:
match = next((item for item in usable if item.serial == serial), None)
if match:
return match
raise DeviceError(f"指定设备 {serial} 不可用")
if len(usable) == 1:
return usable[0]
if not usable:
raise DeviceError("没有已授权的 Android 设备")
raise DeviceError("检测到多台 Android 设备;请在任务中明确指定 device_serial")
def run(serial: str, arguments: Iterable[str], *, binary: bool = False, check: bool = True) -> subprocess.CompletedProcess:
if not serial:
raise DeviceError("ADB 操作必须指定 device_serial")
result = raw_adb(["-s", serial, *arguments], text=not binary)
if check and result.returncode:
stderr = result.stderr.decode("utf-8", "ignore") if binary else result.stderr
raise DeviceError((stderr or "adb command failed").strip())
return result
def screen_size(serial: str) -> tuple[int, int]:
output = run(serial, ["shell", "wm", "size"]).stdout
if isinstance(output, bytes):
output = output.decode("utf-8", "ignore")
matches = re.findall(r"(\d+)x(\d+)", output)
if not matches:
raise DeviceError(f"无法读取屏幕尺寸:{output}")
return tuple(map(int, matches[-1])) # Override size 优先
def screen_density(serial: str) -> str:
output = str(run(serial, ["shell", "wm", "density"]).stdout)
matches = re.findall(r"(\d+)", output)
return matches[-1] if matches else ""
def fingerprint(serial: str) -> dict[str, str]:
def prop(key: str) -> str:
return str(run(serial, ["shell", "getprop", key]).stdout).strip()
width, height = screen_size(serial)
return {
"serial": serial, "manufacturer": prop("ro.product.manufacturer"),
"model": prop("ro.product.model"), "android_version": prop("ro.build.version.release"),
"screen": f"{width}x{height}", "density": screen_density(serial), "orientation": str(run(serial, ["shell", "settings", "get", "system", "user_rotation"]).stdout).strip(),
}