Files
2026-05-15 21:26:06 +08:00

297 lines
11 KiB
Python

#[title: mi10rb-aut]
#[language: python]
#[class: 工具类]
#[service: 10010] ADB设备重启管理
#[disable: false]
#[admin: false]
#[rule: ^重启Mi10$]
#[priority: 999]
#[platform: qq,qb,wx,tb,tg,web,wxmp]
#[open_source: false]
#[icon: 图标url]
#[version: 1.0.0]
#[public: false]
#[price: 0.00]
#[description: 通过ADB(TCP)连接到设备并执行重启;发送"重启Mi10"执行。<br>]
# [param: {"required":true,"key":"mi10rb.address","placeholder":"127.0.0.1:5555","name":"ADB地址:","desc":"设备的 ADB 地址,如 192.168.1.100:5555"}]
# [param: {"required":false,"key":"mi10rb.adb_path","placeholder":"adb","name":"adb路径:","desc":"adb 可执行文件路径,默认使用系统 adb"}]
# [param: {"required":false,"key":"mi10rb.timeout","placeholder":"20","name":"超时:","desc":"单次命令超时时间(秒),默认20"}]
import os
import sys
import shlex
import logging
import subprocess
import time
import colorlog
import middleware
import sender
from typing import Tuple, Optional
# 读取插件参数
CONF_ADDRESS = middleware.bucketGet('mi10rb', 'address')
CONF_ADB_PATH = middleware.bucketGet('mi10rb', 'adb_path') or 'adb'
CONF_TIMEOUT = int(middleware.bucketGet('mi10rb', 'timeout') or '20')
sender = middleware.Sender(middleware.getSenderID())
class TOOL:
def __init__(self):
self.plugin_pre = "mi10rb"
self.color_formatter = colorlog.ColoredFormatter(
'%(log_color)s%(levelname)s: %(message)s',
log_colors={
'DEBUG': 'cyan',
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red,bg_white',
}
)
self.logger = self._configure_logger()
def _configure_logger(self):
logger_obj = logging.getLogger(self.plugin_pre)
logger_obj.setLevel(logging.DEBUG)
if not any(isinstance(h, logging.StreamHandler) for h in logger_obj.handlers):
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG)
console_handler.setFormatter(self.color_formatter)
logger_obj.addHandler(console_handler)
return logger_obj
def log(self, level, message):
self.logger.log(level, message)
def reply(self, content):
return sender.reply(content)
def run_command(command: str, timeout_seconds: int = 20) -> Tuple[int, str, str]:
"""
以非交互方式执行命令,返回 (exit_code, stdout, stderr)
"""
try:
process = subprocess.Popen(
shlex.split(command),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
stdout, stderr = process.communicate(timeout=timeout_seconds)
return process.returncode, (stdout or '').strip(), (stderr or '').strip()
except subprocess.TimeoutExpired:
try:
process.kill()
except Exception:
pass
return 124, "", f"Command timed out after {timeout_seconds}s: {command}"
except FileNotFoundError:
return 127, "", f"Command not found: {command.split()[0]}"
class ADBRebootClient:
def __init__(self, address: str, adb_path: str = 'adb', timeout: int = 20, tool: Optional[TOOL] = None):
self.address = address
self.adb = adb_path or 'adb'
self.timeout = timeout
self.tool = tool
def _log(self, level, message: str):
if self.tool:
self.tool.log(level, message)
def disconnect(self) -> None:
# 断开指定地址,忽略错误
run_command(f"{self.adb} disconnect {self.address}", timeout_seconds=min(self.timeout, 5))
def kill_server(self) -> None:
# 尝试重启 adb 服务端,忽略错误
run_command(f"{self.adb} kill-server", timeout_seconds=min(self.timeout, 5))
def ensure_adb(self) -> bool:
code, out, err = run_command(f"{self.adb} version", timeout_seconds=self.timeout)
if code != 0:
self._log(logging.ERROR, "未找到可用的 adb,请安装或在参数中指定 adb 路径")
if err:
self._log(logging.ERROR, err)
return False
self._log(logging.INFO, (out or 'adb 可用'))
return True
def connect(self) -> bool:
# 先尝试断开可能残留的连接
self.disconnect()
# 首次连接
code, out, err = run_command(f"{self.adb} connect {self.address}", timeout_seconds=self.timeout)
msg = ((out or '') + ("\n" + err if err else '')).strip()
if code != 0:
self._log(logging.WARNING, f"adb connect 首次失败(code={code}),尝试重启 adb 并重试...\n{msg}")
# 重启 adb 后重试一次
self.kill_server()
self.disconnect()
code, out, err = run_command(f"{self.adb} connect {self.address}", timeout_seconds=self.timeout)
msg = ((out or '') + ("\n" + err if err else '')).strip()
if code != 0:
self._log(logging.ERROR, f"adb connect 重试失败(code={code}):\n{msg}")
return False
low = msg.lower()
if ("connected to" in low) or ("already connected to" in low):
self._log(logging.INFO, "✓ ADB 连接成功")
else:
self._log(logging.WARNING, "adb connect 返回0,但输出未包含已连接提示")
if msg:
self._log(logging.INFO, msg)
# 连接校验:确认设备在线
ok, detail = self.get_state()
if not ok:
self._log(logging.ERROR, f"连接校验失败:{detail}")
return False
self._log(logging.INFO, f"设备状态:{detail}")
return True
def get_state(self) -> Tuple[bool, str]:
code, out, err = run_command(f"{self.adb} -s {self.address} get-state", timeout_seconds=min(self.timeout, 10))
text = (out or err or '').strip()
if code == 0 and (out or '').strip() == 'device':
return True, 'device'
# 某些 ROM 会返回 unknown/unauthorized,这里给出更友好的提示
return False, (text or f"get-state 失败(code={code})")
def reboot(self) -> bool:
def looks_like_reboot_sent(message: str) -> bool:
low = (message or '').lower()
# 设备重启会导致连接被关闭,adb 可能返回如下错误,但指令已下发
hints = [
'closed',
'device offline',
'no devices/emulators found',
'cannot connect to daemon',
'daemon not running',
'failed to connect to',
'connection refused',
'connection reset',
'transport endpoint is not connected',
'broken pipe',
'read failed',
'i/o error',
'more than one device/emulator',
]
return any(h in low for h in hints)
def observe_post_reboot_disconnect(max_attempts: int = 8, interval_seconds: float = 0.5) -> bool:
# 在短时间内观察设备从在线到离线/不可达的过渡,以此判断重启已生效
for _ in range(max_attempts):
code_s, out_s, err_s = run_command(
f"{self.adb} -s {self.address} get-state",
timeout_seconds=min(self.timeout, 5)
)
text_s = ((out_s or '') + ('\n' + err_s if err_s else '')).strip().lower()
if code_s != 0:
# 命令失败,多半是设备掉线,视为重启进行中
return True
if (out_s or '').strip() != 'device':
# 非 device 状态(例如 offline/unknown),也视为重启进行中
return True
# 仍为在线,继续等待
time.sleep(interval_seconds)
return False
code, out, err = run_command(f"{self.adb} -s {self.address} reboot", timeout_seconds=self.timeout)
combined = ((out or '') + ("\n" + err if err else '')).strip()
if code == 0:
self._log(logging.INFO, "✓ 已发送重启指令")
return True
# 非零返回,但疑似已下发
if looks_like_reboot_sent(combined):
self._log(logging.WARNING, f"adb reboot 返回非零(code={code}),但检测到重启迹象,视为成功:\n{combined}")
return True
# 回退方案:走 shell reboot
self._log(logging.WARNING, f"adb reboot 失败(code={code}),尝试使用 shell reboot 回退...\n{combined}")
code2, out2, err2 = run_command(f"{self.adb} -s {self.address} shell reboot", timeout_seconds=self.timeout)
combined2 = ((out2 or '') + ("\n" + err2 if err2 else '')).strip()
if code2 == 0 or looks_like_reboot_sent(combined2):
self._log(logging.INFO, "✓ 已通过 shell reboot 发送重启指令")
return True
# 最终失败前,观察设备是否已开始断连(重启进行中)
if observe_post_reboot_disconnect():
self._log(logging.WARNING, "检测到重启后的断连迹象,尽管命令返回失败,仍视为已下发成功")
return True
self._log(logging.ERROR, f"shell reboot 仍失败(code={code2}):\n{combined2}")
return False
def run_reboot_flow(tool: TOOL):
"""执行 ADB 设备重启流程"""
tool.reply("开始执行 Mi10 ADB 重启流程...")
# 校验参数
if not CONF_ADDRESS:
tool.reply("❌ 配置参数不完整,请设置 mi10rb.address (如 192.168.1.100:5555)")
return
tool.log(logging.INFO, "配置信息:")
tool.log(logging.INFO, f" ADB地址: {CONF_ADDRESS}")
tool.log(logging.INFO, f" adb路径: {CONF_ADB_PATH}")
tool.log(logging.INFO, f" 超时: {CONF_TIMEOUT}")
client = ADBRebootClient(address=CONF_ADDRESS, adb_path=CONF_ADB_PATH, timeout=CONF_TIMEOUT, tool=tool)
try:
if not client.ensure_adb():
tool.reply("❌ 未检测到 adb,请安装或指定有效的 adb 路径")
return
tool.log(logging.INFO, "🚀 正在连接设备...")
if not client.connect():
tool.reply("❌ ADB 连接失败,请检查地址与网络")
return
tool.log(logging.INFO, "🔁 正在发送重启指令...")
if client.reboot():
tool.reply("✅ 重启指令已发送!设备即将重启")
else:
tool.reply("❌ 重启指令发送失败")
except Exception as exc:
tool.log(logging.ERROR, f"程序执行出错: {exc}")
tool.reply("❌ 程序执行出错,请检查配置与网络")
def exit_with_msg(msg: str):
if msg:
sender.reply(msg)
sender.reply("已退出!")
sys.exit(0)
def _safe_get_message():
try:
return sender.getMessage().strip('"')
except Exception:
return ""
if __name__ == "__main__":
tool = TOOL()
msg = _safe_get_message()
if "重启Mi10" in msg:
run_reboot_flow(tool)
sys.exit(0)
sender.reply("指令不支持,请发送:重启Mi10")