mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-08 06:46:11 +00:00
210 lines
7.1 KiB
Python
210 lines
7.1 KiB
Python
#!/usr/bin/env python3
|
|
"""USB/ADB control helpers for the physical release gate.
|
|
|
|
Device selectors are accepted only as ephemeral command inputs. They are never
|
|
printed or written to artifacts; all output uses the operator-assigned alias.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Callable
|
|
|
|
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
|
|
if str(REPOSITORY_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(REPOSITORY_ROOT))
|
|
|
|
from tools.release_gate.release_gate import (
|
|
GateError,
|
|
SAFE_ID_RE,
|
|
append_trace_event,
|
|
)
|
|
|
|
|
|
APPLICATION_ID = "com.bitchat.droid"
|
|
|
|
|
|
def find_adb() -> str:
|
|
direct = shutil.which("adb")
|
|
if direct:
|
|
return direct
|
|
android_home = os.environ.get("ANDROID_HOME")
|
|
if android_home:
|
|
candidate = Path(android_home) / "platform-tools" / "adb"
|
|
if candidate.is_file():
|
|
return str(candidate)
|
|
raise GateError("adb was not found; set ANDROID_HOME or add adb to PATH")
|
|
|
|
|
|
def run_adb(
|
|
serial: str,
|
|
arguments: list[str],
|
|
*,
|
|
runner: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run,
|
|
) -> str:
|
|
result = runner(
|
|
[find_adb(), "-s", serial, *arguments],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
)
|
|
if result.returncode != 0:
|
|
raise GateError("ADB command failed for the selected logical device")
|
|
return result.stdout.strip()
|
|
|
|
|
|
def count_connected_devices(
|
|
*,
|
|
runner: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run,
|
|
) -> int:
|
|
result = runner(
|
|
[find_adb(), "devices"],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
)
|
|
if result.returncode != 0:
|
|
raise GateError("could not enumerate ADB devices")
|
|
return sum(
|
|
1
|
|
for line in result.stdout.splitlines()[1:]
|
|
if line.strip().endswith("\tdevice")
|
|
)
|
|
|
|
|
|
def probe_device(serial: str, alias: str) -> dict[str, object]:
|
|
if not SAFE_ID_RE.fullmatch(alias):
|
|
raise GateError("device alias must be a lowercase logical identifier")
|
|
api_text = run_adb(serial, ["shell", "getprop", "ro.build.version.sdk"])
|
|
if not api_text.isdigit():
|
|
raise GateError("selected device returned an invalid API level")
|
|
features = run_adb(serial, ["shell", "pm", "list", "features"])
|
|
manufacturer = run_adb(
|
|
serial, ["shell", "getprop", "ro.product.manufacturer"]
|
|
).strip().lower()
|
|
model = run_adb(serial, ["shell", "getprop", "ro.product.model"]).strip()
|
|
capabilities = ["ble-central", "ble-peripheral"]
|
|
if "android.hardware.wifi.aware" in features:
|
|
capabilities.append("wifi-aware")
|
|
return {
|
|
"alias": alias,
|
|
"platform": "android",
|
|
"model": model,
|
|
"manufacturer_class": re.sub(r"[^a-z0-9._-]", "-", manufacturer)[:64],
|
|
"api_level": int(api_text),
|
|
"physical": True,
|
|
"capabilities": capabilities,
|
|
}
|
|
|
|
|
|
def prepare_disposable_device(serial: str, confirmed: bool) -> None:
|
|
if not confirmed:
|
|
raise GateError("prepare requires --confirm-disposable-app-data")
|
|
run_adb(serial, ["shell", "am", "force-stop", APPLICATION_ID])
|
|
output = run_adb(serial, ["shell", "pm", "clear", APPLICATION_ID])
|
|
if "Success" not in output:
|
|
raise GateError("could not clear disposable app data")
|
|
|
|
|
|
def collect_resource_snapshot(serial: str) -> dict[str, int | bool]:
|
|
pid_text = run_adb(serial, ["shell", "pidof", APPLICATION_ID])
|
|
pid = pid_text.split()[0] if pid_text else ""
|
|
metrics: dict[str, int | bool] = {"process-running": bool(pid)}
|
|
if not pid.isdigit():
|
|
return metrics
|
|
meminfo = run_adb(serial, ["shell", "dumpsys", "meminfo", APPLICATION_ID])
|
|
total_match = re.search(r"TOTAL\s+(\d+)", meminfo)
|
|
metrics["total-pss-kb"] = int(total_match.group(1)) if total_match else -1
|
|
thread_text = run_adb(
|
|
serial,
|
|
["shell", "sh", "-c", f"find /proc/{pid}/task -mindepth 1 -maxdepth 1 | wc -l"],
|
|
)
|
|
fd_text = run_adb(
|
|
serial,
|
|
["shell", "sh", "-c", f"find /proc/{pid}/fd -mindepth 1 -maxdepth 1 | wc -l"],
|
|
)
|
|
metrics["thread-count"] = int(thread_text) if thread_text.isdigit() else -1
|
|
metrics["fd-count"] = int(fd_text) if fd_text.isdigit() else -1
|
|
power = run_adb(serial, ["shell", "dumpsys", "power"])
|
|
metrics["app-wakelock-count"] = sum(
|
|
1
|
|
for line in power.splitlines()
|
|
if APPLICATION_ID in line and "WakeLock" in line
|
|
)
|
|
battery = run_adb(serial, ["shell", "dumpsys", "battery"])
|
|
battery_level = re.search(r"^\s*level:\s*(\d+)", battery, re.MULTILINE)
|
|
metrics["battery-level-percent"] = (
|
|
int(battery_level.group(1)) if battery_level else -1
|
|
)
|
|
return metrics
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
commands = parser.add_subparsers(dest="command", required=True)
|
|
|
|
commands.add_parser("count")
|
|
|
|
probe = commands.add_parser("probe")
|
|
probe.add_argument("--serial", required=True, help=argparse.SUPPRESS)
|
|
probe.add_argument("--alias", required=True)
|
|
|
|
prepare = commands.add_parser("prepare")
|
|
prepare.add_argument("--serial", required=True, help=argparse.SUPPRESS)
|
|
prepare.add_argument("--confirm-disposable-app-data", action="store_true")
|
|
|
|
cleanup = commands.add_parser("cleanup")
|
|
cleanup.add_argument("--serial", required=True, help=argparse.SUPPRESS)
|
|
cleanup.add_argument("--confirm-disposable-app-data", action="store_true")
|
|
|
|
snapshot = commands.add_parser("snapshot")
|
|
snapshot.add_argument("--serial", required=True, help=argparse.SUPPRESS)
|
|
snapshot.add_argument("--alias", required=True)
|
|
snapshot.add_argument("--run", type=Path, required=True)
|
|
snapshot.add_argument("--scenario", required=True)
|
|
snapshot.add_argument("--event", default="resource-snapshot")
|
|
return parser
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
args = build_parser().parse_args(argv)
|
|
try:
|
|
if args.command == "count":
|
|
print(json.dumps({"authorized-device-count": count_connected_devices()}))
|
|
elif args.command == "probe":
|
|
print(json.dumps(probe_device(args.serial, args.alias), sort_keys=True))
|
|
elif args.command in {"prepare", "cleanup"}:
|
|
prepare_disposable_device(
|
|
args.serial, args.confirm_disposable_app_data
|
|
)
|
|
print(json.dumps({"status": "clean", "application": APPLICATION_ID}))
|
|
elif args.command == "snapshot":
|
|
metrics = collect_resource_snapshot(args.serial)
|
|
append_trace_event(
|
|
args.run,
|
|
args.scenario,
|
|
args.alias,
|
|
args.event,
|
|
"observed",
|
|
None,
|
|
metrics,
|
|
)
|
|
print(json.dumps({"source_alias": args.alias, "metrics": metrics}, sort_keys=True))
|
|
return 0
|
|
except (GateError, OSError, subprocess.SubprocessError) as error:
|
|
print(f"android lab error: {error}", file=sys.stderr)
|
|
return 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|