同步移动端工程、设备控制改进与发布资料
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user