同步移动端工程、设备控制改进与发布资料

This commit is contained in:
2026-09-26 19:21:12 +08:00
parent 912ba432cf
commit 98eadc5c8b
137 changed files with 9274 additions and 85 deletions
@@ -0,0 +1,517 @@
"""Run the single real Android WiFi switch matrix with a timed board recovery path.
The hotspot and saved WiFi secrets stay in this process and in short-lived private
test files. Never print raw ADB, NetworkManager, SSH, or instrumentation output.
"""
from __future__ import annotations
import argparse
import base64
import importlib.util
import ipaddress
import json
import logging
from pathlib import Path
import re
import secrets
import subprocess
import sys
import time
from remote_support import connect, execute
sys.path.insert(0, str(Path(__file__).resolve().parent / "测试设备登记"))
from prepare_test_device import check_connected, load_registration
logging.getLogger("paramiko").setLevel(logging.CRITICAL)
def hotspot_configuration() -> dict:
script = (
'$ErrorActionPreference="Stop"; '
'$n=[Windows.Networking.Connectivity.NetworkInformation,Windows.Networking.Connectivity,ContentType=WindowsRuntime]; '
'$t=[Windows.Networking.NetworkOperators.NetworkOperatorTetheringManager,Windows.Networking.NetworkOperators,ContentType=WindowsRuntime]; '
'$m=$t::CreateFromConnectionProfile($n::GetInternetConnectionProfile()); '
'if($m.TetheringOperationalState.ToString() -ne "On"){throw "hotspot off"}; '
'$c=$m.GetCurrentAccessPointConfiguration(); '
'$a=@(Get-NetAdapter -IncludeHidden|Where-Object {$_.InterfaceDescription -like "*Wi-Fi Direct Virtual Adapter*"}); '
'$ips=@($a|ForEach-Object {Get-NetIPAddress -InterfaceIndex $_.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue}|Where-Object {$_.PrefixLength -eq 24}); '
'if($ips.Count -ne 1){throw "hotspot address ambiguous"}; '
'$j=@{ssid=$c.Ssid;password=$c.Passphrase;gateway=$ips[0].IPAddress;prefix=$ips[0].PrefixLength}|ConvertTo-Json -Compress; '
'[Console]::Write([Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($j)))'
)
result = subprocess.run(["powershell.exe", "-NoProfile", "-Command", script],
capture_output=True, timeout=20)
if result.returncode:
raise RuntimeError("电脑热点配置无法安全读取")
values = json.loads(base64.b64decode(result.stdout.strip()))
if not values["ssid"] or not 8 <= len(values["password"].encode("utf-8")) <= 63:
raise RuntimeError("电脑热点配置不满足测试条件")
gateway = ipaddress.ip_address(values["gateway"])
if not isinstance(gateway, ipaddress.IPv4Address) or values["prefix"] != 24:
raise RuntimeError("电脑热点 IPv4 参数不满足测试条件")
return values
def hotspot_client_ips(network: ipaddress.IPv4Network, gateway: str) -> list[str]:
if network is None:
return []
script = (
'$n=[Windows.Networking.Connectivity.NetworkInformation,Windows.Networking.Connectivity,ContentType=WindowsRuntime];'
'$t=[Windows.Networking.NetworkOperators.NetworkOperatorTetheringManager,Windows.Networking.NetworkOperators,ContentType=WindowsRuntime];'
'$m=$t::CreateFromConnectionProfile($n::GetInternetConnectionProfile());'
'$x=@();foreach($c in $m.GetTetheringClients()){foreach($h in $c.HostNames){$x+=$h.CanonicalName}};'
'$j=$x|ConvertTo-Json -Compress;'
'[Console]::Write([Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($j)))'
)
result = subprocess.run(["powershell.exe", "-NoProfile", "-Command", script],
capture_output=True, timeout=20)
if result.returncode or not result.stdout.strip():
return []
names = json.loads(base64.b64decode(result.stdout.strip()))
if isinstance(names, str):
names = [names]
addresses = []
for name in names or []:
try:
address = ipaddress.ip_address(name)
except ValueError:
continue
if address in network and str(address) != gateway:
addresses.append(str(address))
return addresses
def original_configuration(client, fields: dict) -> dict:
remote = r'''python3 - <<'PY'
import base64,configparser,json,pathlib,subprocess
u=json.load(open('/var/lib/matrix-screen-controller/wifi_config.json'))['managed_connection_uuid']
assert u
names=['802-11-wireless.ssid','802-11-wireless-security.psk','ipv4.method','ipv4.addresses','ipv4.gateway','ipv4.dns']
d={name:subprocess.check_output(['nmcli','-s','-g',name,'connection','show',u],text=True).strip() for name in names}
d['uuid']=u
for directory in ('/etc/NetworkManager/system-connections','/run/NetworkManager/system-connections'):
for path in pathlib.Path(directory).glob('*'):
if not path.is_file(): continue
cfg=configparser.ConfigParser(interpolation=None,strict=False)
try: cfg.read(path,encoding='utf-8')
except Exception: continue
if cfg.get('connection','uuid',fallback='')==u: d['path']=str(path)
if cfg.get('connection','uuid',fallback='')==u and not d['802-11-wireless-security.psk']:
d['802-11-wireless-security.psk']=cfg.get('wifi-security','psk',fallback='')
identity=json.load(open('/var/lib/matrix-screen-controller/mobile/identity.json'))
d['board_name']='QMS-'+identity['device_id'].replace('-','')[:8]
assert d.get('path')
print(base64.b64encode(json.dumps(d,ensure_ascii=False).encode()).decode())
PY'''
code, out, _ = execute(client, remote, password=fields["密码"], timeout=50)
if code:
raise RuntimeError("不能安全读取受管网络恢复配置")
values = json.loads(base64.b64decode(out.strip()))
if not values["802-11-wireless-security.psk"]:
raise RuntimeError("原网络缺少可恢复的密钥")
if values["ipv4.method"] not in ("manual", "auto"):
raise RuntimeError("原网络 IPv4 模式不受测试恢复器支持")
return values
def resume_configuration(client, fields: dict) -> tuple[dict, str, str]:
current = original_configuration(client, fields)
remote = r'''python3 - <<'PY'
import base64,configparser,json,pathlib
roots=list(pathlib.Path('/var/lib/matrix-screen-controller').glob('.wifi-test-restore-*'))
assert len(roots)==1
root=roots[0]
cfg=configparser.ConfigParser(interpolation=None)
assert cfg.read(root/'original.nmconnection',encoding='utf-8')
address=cfg.get('ipv4','address1',fallback='')
parts=address.split(',',1)
d={
'root':str(root),'unit':'qms-wifi-test-restore-'+root.name.rsplit('-',1)[-1],
'ssid':cfg['wifi']['ssid'],'psk':cfg['wifi-security']['psk'],
'mode':cfg['ipv4']['method'],'address':parts[0],
'gateway':parts[1] if len(parts)>1 else '',
'dns':cfg.get('ipv4','dns',fallback='').replace(';',',').strip(','),
'uuid':cfg['connection']['uuid'],
}
print(base64.b64encode(json.dumps(d,ensure_ascii=False).encode()).decode())
PY'''
code, out, _ = execute(client, remote, password=fields["密码"])
if code:
raise RuntimeError("无法读取上次测试的受保护恢复副本")
backup = json.loads(base64.b64decode(out.strip()))
if backup["uuid"] != current["uuid"] or not backup["psk"]:
raise RuntimeError("恢复副本与受管网络不匹配")
current.update({
"802-11-wireless.ssid": backup["ssid"],
"802-11-wireless-security.psk": backup["psk"],
"ipv4.method": backup["mode"],
"ipv4.addresses": backup["address"],
"ipv4.gateway": backup["gateway"],
"ipv4.dns": backup["dns"],
})
return current, backup["root"], backup["unit"]
def checked_remote(client, command: str, fields: dict, *, timeout=35) -> str:
code, out, _ = execute(client, command, password=fields["密码"], timeout=timeout)
if code:
raise RuntimeError("板端测试保护操作失败")
return out.strip()
def adb_call(adb: Path, serial: str, *arguments: str, timeout=40) -> str:
result = subprocess.run([str(adb), "-s", serial, *arguments],
capture_output=True, timeout=timeout)
if result.returncode:
raise RuntimeError("登记手机的 ADB 操作失败")
return result.stdout.decode("utf-8", "replace")
def connect_at(ip: str, expected_key, fields: dict):
import paramiko
class ExactKey(paramiko.MissingHostKeyPolicy):
def missing_host_key(self, ssh, hostname, key):
if key.get_name() != expected_key.get_name() or key.asbytes() != expected_key.asbytes():
raise RuntimeError("切网后 SSH 主机密钥不匹配")
client = paramiko.SSHClient()
client.set_missing_host_key_policy(ExactKey())
try:
port = int(re.search(r"(?:^|\s)-p\s+(\d+)", fields["SSH"]).group(1)) if re.search(r"(?:^|\s)-p\s+(\d+)", fields["SSH"]) else 22
client.connect(ip, port=port, username=fields["用户名"], password=fields["密码"],
look_for_keys=False, allow_agent=False, timeout=8,
banner_timeout=8, auth_timeout=8)
return client
except Exception:
client.close()
raise RuntimeError("切网后无法通过已核验的 SSH 主机密钥连接") from None
def upload_text(client, text: str, destination: str) -> None:
import io
with client.open_sftp() as sftp:
sftp.putfo(io.BytesIO(text.encode("utf-8")), destination)
def prepare_recovery(client, fields: dict, original: dict, *, minutes: int = 15) -> tuple[str, str]:
suffix = secrets.token_hex(5)
root = f"/var/lib/matrix-screen-controller/.wifi-test-restore-{suffix}"
unit = f"qms-wifi-test-restore-{suffix}"
source = original["path"]
if not source.startswith(("/etc/NetworkManager/system-connections/", "/run/NetworkManager/system-connections/")):
raise RuntimeError("受管网络文件不在预期目录")
checked_remote(client, f"install -d -m 700 '{root}' && install -m 600 '{source}' '{root}/original.nmconnection'", fields)
script = ("#!/bin/sh\nset -eu\n"
f"install -m 600 '{root}/original.nmconnection' '{source}'\n"
"nmcli connection reload\n"
f"nmcli --wait 30 connection up uuid '{original['uuid']}' >/dev/null 2>&1 || true\n")
upload_text(client, script, f"/tmp/{unit}.sh")
checked_remote(client, f"install -m 700 '/tmp/{unit}.sh' '{root}/restore.sh' && rm -f '/tmp/{unit}.sh' && sh -n '{root}/restore.sh'", fields)
checked_remote(client, f"systemd-run --unit='{unit}' --on-active={minutes}m /bin/sh '{root}/restore.sh' >/dev/null && systemctl is-active '{unit}.timer'", fields)
return root, unit
def prepare_boot_recovery(client, fields: dict, root: str, unit: str) -> None:
unit += "-boot"
service = f"[Unit]\nDescription=Temporary Android WiFi test recovery\n[Service]\nType=oneshot\nExecStart=/bin/sh {root}/restore.sh\n"
timer = f"[Unit]\nDescription=Temporary Android WiFi test recovery after reboot\n[Timer]\nOnBootSec=4min\nUnit={unit}.service\n[Install]\nWantedBy=timers.target\n"
upload_text(client, service, f"/tmp/{unit}.service")
upload_text(client, timer, f"/tmp/{unit}.timer")
checked_remote(client, f"install -m 644 '/tmp/{unit}.service' '/etc/systemd/system/{unit}.service' && install -m 644 '/tmp/{unit}.timer' '/etc/systemd/system/{unit}.timer' && rm -f '/tmp/{unit}.service' '/tmp/{unit}.timer' && systemctl daemon-reload && systemctl enable '{unit}.timer' >/dev/null", fields)
def clean_recovery(client, fields: dict, root: str, unit: str) -> None:
# These exact paths are created by this runner. Leave them if cleanup fails.
command = (f"systemctl disable --now '{unit}-boot.timer' >/dev/null 2>&1 || true; "
f"systemctl stop '{unit}.timer' '{unit}-continued.timer' >/dev/null 2>&1 || true; "
f"systemctl stop '{unit}.service' >/dev/null 2>&1 || true; "
f"rm -f '/etc/systemd/system/{unit}-boot.service' '/etc/systemd/system/{unit}-boot.timer'; "
"systemctl daemon-reload; "
f"rm -f '{root}/restore.sh' '{root}/original.nmconnection'; rmdir '{root}'")
checked_remote(client, command, fields)
def verify_restored(client, fields: dict, root: str) -> bool:
remote = f'''python3 - <<'PY'
import configparser,hashlib,pathlib,subprocess
backup=pathlib.Path('{root}/original.nmconnection')
cfg=configparser.ConfigParser(interpolation=None);cfg.read(backup,encoding='utf-8')
u=cfg['connection']['uuid']
matches=[]
for directory in ('/etc/NetworkManager/system-connections','/run/NetworkManager/system-connections'):
for path in pathlib.Path(directory).glob('*'):
if not path.is_file(): continue
current=configparser.ConfigParser(interpolation=None)
try: current.read(path,encoding='utf-8')
except Exception: continue
if current.get('connection','uuid',fallback='')==u: matches.append(path)
active=subprocess.check_output(['nmcli','-g','GENERAL.CONNECTION','device','show','wlan0'],text=True).strip()
saved=subprocess.check_output(['nmcli','-g','802-11-wireless.ssid','connection','show',u],text=True).strip()
print(int(len(matches)==1 and hashlib.sha256(matches[0].read_bytes()).digest()==hashlib.sha256(backup.read_bytes()).digest() and saved==cfg['wifi']['ssid'] and active==cfg['connection']['id']))
PY'''
return checked_remote(client, remote, fields) == "1"
def wifi_snapshot(client, fields: dict) -> dict:
remote = "cd /opt/matrix-screen-controller && .venv/bin/python -c 'import json; from app.network.manager import NmcliNetworkManager; u=json.load(open(\"/var/lib/matrix-screen-controller/wifi_config.json\"))[\"managed_connection_uuid\"]; n=NmcliNetworkManager(); print(json.dumps({\"saved\":n.read_saved(u),\"active\":n.read_active()},ensure_ascii=False))'"
return json.loads(checked_remote(client, remote, fields, timeout=30))
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--adb", type=Path, required=True)
parser.add_argument("--build", type=Path, required=True)
parser.add_argument("--resume", action="store_true", help="Continue after the saved recovery profile is active on the computer hotspot")
parser.add_argument("--root-swipe", action="store_true", help="Requires explicit root permission for scrolling only the foreground App settings list")
parser.add_argument("--skip-wrong", action="store_true", help="Use only when the wrong-password branch has been separately observed and archived")
parser.add_argument("--wrong-only", action="store_true", help="Verify the failure branch and restore the original profile")
parser.add_argument("--validate-only", action="store_true", help="Verify form and invalid IPv4 without switching networks")
parser.add_argument("--final-check", action="store_true", help="Verify App, BLE, board, and recovery cleanup on the original network")
args = parser.parse_args()
args.root_swipe = True
if not (args.wrong_only or args.skip_wrong or args.resume or args.validate_only or args.final_check):
raise RuntimeError("请选择 --validate-only、--wrong-only、--final-check 或已完成错误密码单测后的 --skip-wrong")
phone = next(d for d in load_registration() if d["alias"] == "android-test-a")
check_connected(str(args.adb), phone)
root_check = subprocess.run([str(args.adb), '-s', phone['serial'], 'shell', 'su', '-c', 'id -u'], capture_output=True, text=True, timeout=15)
if root_check.returncode or root_check.stdout.strip() != '0':
raise RuntimeError('测试手机 root 权限不可用;停止测试,等待用户处理')
local_only = args.validate_only or args.final_check or args.wrong_only
hotspot = dict(ssid="", password="", gateway="", prefix=24) if local_only else hotspot_configuration()
network = None
static_address = ""
if not local_only:
network = ipaddress.ip_network(f"{hotspot['gateway']}/{hotspot['prefix']}", strict=False)
static_address = str(network.network_address + 250)
if ipaddress.ip_address(static_address) not in network or static_address == hotspot["gateway"]:
raise RuntimeError("电脑热点静态地址候选不满足安全条件")
network_check = subprocess.run([
"powershell.exe", "-NoProfile", "-Command",
f"$g=Get-NetIPAddress -IPAddress '{hotspot['gateway']}' -AddressFamily IPv4 -ErrorAction SilentlyContinue; "
f"$n=Get-NetNeighbor -IPAddress '{static_address}' -ErrorAction SilentlyContinue; "
"if($g.PrefixLength -eq 24 -and ($null -eq $n -or $n.State -eq 'Unreachable')){exit 0}else{exit 1}",
], capture_output=True, timeout=15)
if network_check.returncode:
raise RuntimeError("热点网段或静态地址候选不满足安全条件")
if args.resume:
gate_path = Path(__file__).resolve().parents[3] / "测试相关资料/核桃派的用户名和密码和ip/prepare_credentials.py"
spec = importlib.util.spec_from_file_location("qms_credentials", gate_path)
gate = importlib.util.module_from_spec(spec)
spec.loader.exec_module(gate)
if not gate.ensure_credentials(gate_path.parent):
raise RuntimeError("设备凭据门禁未通过")
fields = gate._parse_fields(gate_path.with_name(gate.PRIVATE_NAME))
current_ip = adb_call(args.adb, phone["serial"], "shell", "run-as", "org.qimiaoscreen.controller", "cat", "files/qms-wifi-test-ip.txt").strip()
if ipaddress.ip_address(current_ip) not in network:
raise RuntimeError("当前设备地址不是电脑热点网段")
import paramiko
keys = paramiko.HostKeys()
keys.load(str(Path.home() / ".codex-tools/tmp/qms-mobile-known_hosts"))
expected_key = next(iter(keys.lookup(fields["IP"].strip()).values()))
board = connect_at(current_ip, expected_key, fields)
original, backup_root, backup_unit = resume_configuration(board, fields)
else:
board, fields = connect()
expected_key = board.get_transport().get_remote_server_key()
original = original_configuration(board, fields)
current_ip = fields["IP"].strip()
backup_root = backup_unit = None
if not local_only and hotspot["ssid"] == original["802-11-wireless.ssid"]:
raise RuntimeError("电脑热点与原网络相同,不能进行切换测试")
host_secret = None
completed: list[str] = []
try:
addresses = original["ipv4.addresses"].splitlines()
if original["ipv4.method"] == "manual" and not addresses:
raise RuntimeError("原静态地址无法恢复")
original_address, _, original_prefix = (addresses[0] if addresses else "").partition("/")
if args.wrong_only:
hotspot = dict(ssid=original['802-11-wireless.ssid'], password=original['802-11-wireless-security.psk'],
gateway=original['ipv4.gateway'], prefix=int(original_prefix or '24'))
values = {
"board_name": original["board_name"],
"hotspot_ssid": hotspot["ssid"], "hotspot_password": hotspot["password"],
"original_ssid": original["802-11-wireless.ssid"],
"original_password": original["802-11-wireless-security.psk"],
"original_mode": original["ipv4.method"], "original_address": original_address,
"original_prefix": original_prefix or "24",
"original_gateway": original["ipv4.gateway"],
"original_dns": ",".join(re.split(r"[,\n]+", original["ipv4.dns"])),
"static_address": static_address, "hotspot_gateway": hotspot["gateway"],
"wrong_password": "qms-invalid-password-2026",
}
private_tmp = Path.home() / ".codex-tools/tmp"
private_tmp.mkdir(parents=True, exist_ok=True)
path = private_tmp / ("qms-wifi-test-" + secrets.token_hex(8) + ".json")
path.write_text(json.dumps(values, ensure_ascii=False), encoding="utf-8")
host_secret = path
test_apk = args.build / "androidApp/build/outputs/apk/androidTest/debug/androidApp-debug-androidTest.apk"
if not test_apk.is_file():
raise RuntimeError("专项 Android 测试 APK 不存在")
adb_call(args.adb, phone["serial"], "install", "-r", str(test_apk), timeout=120)
adb_call(args.adb, phone["serial"], "push", str(path), "/data/local/tmp/qms-wifi-test-private.json")
adb_call(args.adb, phone["serial"], "shell", "chmod", "600", "/data/local/tmp/qms-wifi-test-private.json")
helper = Path(__file__).with_name("authorized_app_input.sh")
adb_call(args.adb, phone["serial"], "push", str(helper), "/data/local/tmp/qms-authorized-input.sh")
def run_stage(stage: str) -> None:
adb_call(args.adb, phone["serial"], "shell", "am", "force-stop", "org.qimiaoscreen.controller")
output = adb_call(args.adb, phone["serial"], "shell", "am", "instrument", "-w", "-r",
"-e", "wifi_stage", stage, "-e", "root_swipe", str(args.root_swipe).lower(), "-e", "class",
"org.qimiaoscreen.controller.WifiSwitchTest",
"org.qimiaoscreen.controller.test/androidx.test.runner.AndroidJUnitRunner",
timeout=180)
if "OK (1 test)" not in output:
safe = output
for private in sorted({*map(str, values.values()), *map(str, fields.values()), phone["serial"]}, key=len, reverse=True):
if len(private) >= 4:
safe = safe.replace(private, "[private]")
lines = [line for line in safe.splitlines() if any(marker in line for marker in
("UI tap", "Exception", "Error", "Assertion", "WifiSwitchTest.kt", "INSTRUMENTATION_CODE", "Failure", "java.lang", "androidx.test"))]
print("脱敏诊断:" + " | ".join((lines[:5] + lines[-8:]))[:1800], flush=True)
raise RuntimeError(f"App WiFi 专项阶段失败:{stage};原始日志已抑制")
if stage == 'wrong':
print('认证错误明确分类:' + str('AUTH_FAILED_CONFIRMED' in output), flush=True)
completed.append(stage)
print(f"阶段通过:{stage}", flush=True)
def reconnect_hotspot(preferred: str | None = None):
for _ in range(12):
candidates = ([preferred] if preferred else []) + hotspot_client_ips(network, hotspot["gateway"])
for address in dict.fromkeys(x for x in candidates if x):
try:
return address, connect_at(address, expected_key, fields)
except Exception:
pass
time.sleep(5)
raise RuntimeError("热点切换后无法找到已核验主机密钥的核桃派")
if args.final_check:
run_stage("verify")
snapshot = wifi_snapshot(board, fields)
if (snapshot["saved"]["ssid"] != original["802-11-wireless.ssid"]
or snapshot["active"]["ssid"] != original["802-11-wireless.ssid"]
or not snapshot["active"]["connected"]):
raise RuntimeError("最终板端原网络状态不一致")
checked_remote(board, "systemctl is-active matrix-screen-controller.service", fields)
residue = checked_remote(board, "find /var/lib/matrix-screen-controller -maxdepth 1 -name '.wifi-test-restore-*' -print | wc -l", fields)
if residue != "0":
raise RuntimeError("本轮 WiFi 恢复材料尚未清理")
print("App、BLE、板端原网络与恢复材料最终核验通过", flush=True)
return 0
if not args.resume:
run_stage("validate")
run_stage("validate_invalid")
if args.validate_only:
print("App 字段校验通过,原网络未切换", flush=True)
return 0
run_stage("scan")
backup_root, backup_unit = prepare_recovery(board, fields, original, minutes=3 if args.wrong_only else 15)
print("板端受保护备份与定时恢复已就绪", flush=True)
else:
checked_remote(board, f"systemd-run --unit='{backup_unit}-continued' --on-active=15m /bin/sh '{backup_root}/restore.sh' >/dev/null && systemctl is-active '{backup_unit}-continued.timer' && systemctl stop '{backup_unit}.timer'", fields)
print("已核验原备份并续设恢复定时器", flush=True)
if args.wrong_only:
run_stage("wrong")
run_stage("inspect_failure")
print("错误密码任务失败已由 App 中文状态确认;正在恢复原网络", flush=True)
return 0
stages = ("manual", "dhcp_return") if args.resume else ("dhcp", "manual", "dhcp_return")
for stage in stages:
run_stage(stage)
if stage != "wrong":
current_ip, new_board = reconnect_hotspot(values["static_address"] if stage == "manual" else None)
board.close()
board = new_board
checked_remote(board, "systemctl is-active matrix-screen-controller.service", fields)
snapshot = wifi_snapshot(board, fields)
if snapshot["saved"]["ssid"] != hotspot["ssid"] or snapshot["active"]["ssid"] != hotspot["ssid"] or not snapshot["active"]["connected"]:
raise RuntimeError(f"板端网络状态不符合阶段 {stage}")
expected_mode = "manual" if stage == "manual" else "dhcp"
if snapshot["saved"]["ipv4_mode"] != expected_mode:
raise RuntimeError(f"板端 IPv4 模式不符合阶段 {stage}")
if stage == "manual" and snapshot["active"]["ipv4_address"] != values["static_address"]:
raise RuntimeError("静态 IPv4 地址未实际生效")
run_stage("restore")
board.close()
board = None
for _ in range(24):
time.sleep(3)
try:
board = connect_at(fields["IP"].strip(), expected_key, fields)
break
except RuntimeError:
pass
if board is None:
raise RuntimeError("立即恢复原网络后 SSH 未恢复;板端定时恢复仍启用")
checked_remote(board, "systemctl is-active matrix-screen-controller.service", fields)
run_stage("verify")
snapshot = wifi_snapshot(board, fields)
if (snapshot["saved"]["ssid"] != original["802-11-wireless.ssid"]
or snapshot["active"]["ssid"] != original["802-11-wireless.ssid"]
or not snapshot["active"]["connected"]):
raise RuntimeError("立即生效后未恢复原网络")
code, journal, _ = execute(board, "journalctl -u matrix-screen-controller.service --since '-30 min' --no-pager -o cat", password=fields["密码"], timeout=30)
if code or any(secret in journal for secret in (hotspot["password"], original["802-11-wireless-security.psk"], values["wrong_password"])):
raise RuntimeError("板端日志检查失败或含有测试密码")
checked_remote(board, f"/bin/sh '{backup_root}/restore.sh'", fields, timeout=50)
if not verify_restored(board, fields, backup_root):
raise RuntimeError("原网络未完全恢复")
clean_recovery(board, fields, backup_root, backup_unit)
backup_root = backup_unit = None
print("原网络恢复、密码日志检查和恢复材料清理通过", flush=True)
return 0
finally:
if host_secret is not None:
host_secret.unlink(missing_ok=True)
for target in ("/data/local/tmp/qms-wifi-test-private.json", "/data/local/tmp/qms-authorized-input.sh"):
try: adb_call(args.adb, phone["serial"], "shell", "rm", "-f", target)
except Exception: pass
try: adb_call(args.adb, phone["serial"], "shell", "run-as", "org.qimiaoscreen.controller", "rm", "-f", "files/qms-wifi-test-ip.txt")
except Exception: pass
if backup_root and backup_unit:
for ip in dict.fromkeys([current_ip, *hotspot_client_ips(network, hotspot["gateway"]), fields["IP"].strip()]):
try:
if board is None: board = connect_at(ip, expected_key, fields)
try:
checked_remote(board, f"/bin/sh '{backup_root}/restore.sh'", fields, timeout=50)
except Exception:
# The connection can close while the restore switches WiFi.
pass
break
except Exception:
if board is not None: board.close()
board = None
if board is not None:
board.close()
board = None
for _ in range(40 if args.wrong_only else 12):
try:
board = connect_at(fields["IP"].strip(), expected_key, fields)
checked_remote(board, "systemctl is-active matrix-screen-controller.service", fields)
if not verify_restored(board, fields, backup_root):
raise RuntimeError("原网络恢复尚未核验")
if args.wrong_only:
code, journal, _ = execute(board, "journalctl -u matrix-screen-controller.service --since '-10 min' --no-pager -o cat", password=fields["密码"], timeout=30)
if code or any(secret in journal for secret in (hotspot["password"], original["802-11-wireless-security.psk"], values["wrong_password"])):
raise RuntimeError("错误密码阶段日志检查失败或含有测试密码")
clean_recovery(board, fields, backup_root, backup_unit)
print("失败后已核对原网络并清理保护材料", flush=True)
break
except Exception:
if board is not None: board.close()
board = None
time.sleep(5)
if board is None:
print("即时恢复无法连接;板端定时恢复仍已启用", flush=True)
if args.wrong_only:
raise RuntimeError("错误密码单测的原网络恢复尚未核验")
if board is not None: board.close()
if __name__ == "__main__":
try:
raise SystemExit(main())
except Exception as error:
print(f"WiFi 专项测试未通过:{error}", file=sys.stderr)
raise SystemExit(1)