mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 04:38:30 +09:00
702 lines
24 KiB
Python
702 lines
24 KiB
Python
#!/usr/bin/env python3
|
|
"""Capture an exact frame from MobileGL's Android trace replay activity."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
import shlex
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Any, Sequence
|
|
|
|
from rdc.discover import find_renderdoc
|
|
|
|
|
|
DEFAULT_PACKAGE = "top.mobilegl.plugin.trace"
|
|
DEFAULT_ACTIVITY = "top.mobilegl.plugin.trace.TraceReplayActivity"
|
|
DEFAULT_ACTION = "top.mobilegl.plugin.TRACE_REPLAY"
|
|
RENDERDOC_VULKAN_LAYER = "VK_LAYER_RENDERDOC_Capture"
|
|
RENDERDOC_GLES_LAYER = "libVkLayer_GLES_RenderDoc.so"
|
|
RENDERDOC_CMD_PACKAGE = "org.renderdoc.renderdoccmd.arm64"
|
|
REMOTE_GLES_LAYER = f"/data/local/debug/vulkan/{RENDERDOC_GLES_LAYER}"
|
|
|
|
|
|
class CommandRunner:
|
|
def __init__(self, *, verbose: bool = True) -> None:
|
|
self.verbose = verbose
|
|
self.commands: list[list[str]] = []
|
|
|
|
def run(
|
|
self,
|
|
command: Sequence[object],
|
|
*,
|
|
check: bool = True,
|
|
timeout: float | None = None,
|
|
capture_output: bool = True,
|
|
) -> subprocess.CompletedProcess[str]:
|
|
argv = [str(part) for part in command]
|
|
self.commands.append(argv)
|
|
if self.verbose:
|
|
print(f"+ {subprocess.list2cmdline(argv)}", file=sys.stderr, flush=True)
|
|
completed = subprocess.run(
|
|
argv,
|
|
check=False,
|
|
timeout=timeout,
|
|
capture_output=capture_output,
|
|
text=True,
|
|
encoding="utf-8",
|
|
errors="replace",
|
|
)
|
|
if self.verbose and completed.stdout and not capture_output:
|
|
print(completed.stdout, end="", file=sys.stderr)
|
|
if check and completed.returncode != 0:
|
|
detail = completed.stderr.strip() or completed.stdout.strip() or "no output"
|
|
raise RuntimeError(
|
|
f"command failed with exit code {completed.returncode}: "
|
|
f"{subprocess.list2cmdline(argv)}\n{detail}"
|
|
)
|
|
return completed
|
|
|
|
|
|
def _adb(runner: CommandRunner, serial: str, *args: object, **kwargs: Any) -> subprocess.CompletedProcess[str]:
|
|
return runner.run(["adb", "-s", serial, *args], **kwargs)
|
|
|
|
|
|
def _connected_serials(runner: CommandRunner) -> list[str]:
|
|
completed = runner.run(["adb", "devices"], timeout=10)
|
|
serials: list[str] = []
|
|
for line in completed.stdout.splitlines()[1:]:
|
|
fields = line.split()
|
|
if len(fields) >= 2 and fields[1] == "device":
|
|
serials.append(fields[0])
|
|
return serials
|
|
|
|
|
|
def _resolve_serial(runner: CommandRunner, requested: str | None) -> str:
|
|
serials = _connected_serials(runner)
|
|
if requested:
|
|
if requested not in serials:
|
|
raise RuntimeError(
|
|
f"Android device {requested!r} is not connected; available: {serials or 'none'}"
|
|
)
|
|
return requested
|
|
if len(serials) != 1:
|
|
raise RuntimeError(
|
|
"exactly one Android device must be connected when --serial is omitted; "
|
|
f"found {serials or 'none'}"
|
|
)
|
|
return serials[0]
|
|
|
|
|
|
def _setup_renderdoc(runner: CommandRunner, serial: str, connected: list[str]) -> None:
|
|
command = ["rdc", "android", "setup", "--serial", serial, "--json"]
|
|
completed = runner.run(command, check=False, timeout=90)
|
|
if completed.returncode == 0:
|
|
return
|
|
# rdc-cli 1.41 can enumerate a raw serial but compare it against adb://SERIAL.
|
|
# Falling back is safe only when the requested device is the sole connected device.
|
|
if connected == [serial] and "not found" in (completed.stderr + completed.stdout).lower():
|
|
runner.run(["rdc", "android", "setup", "--json"], timeout=90)
|
|
return
|
|
detail = completed.stderr.strip() or completed.stdout.strip()
|
|
raise RuntimeError(f"rdc android setup failed: {detail}")
|
|
|
|
|
|
def _get_property(runner: CommandRunner, serial: str, key: str) -> str:
|
|
return _adb(runner, serial, "shell", "getprop", key, timeout=10).stdout.strip()
|
|
|
|
|
|
def _set_property(runner: CommandRunner, serial: str, key: str, value: str) -> None:
|
|
_adb(runner, serial, "shell", "setprop", key, value, timeout=10)
|
|
|
|
|
|
def _get_setting(runner: CommandRunner, serial: str, key: str) -> str | None:
|
|
value = _adb(
|
|
runner, serial, "shell", "settings", "get", "global", key, timeout=10
|
|
).stdout.strip()
|
|
return None if value in {"", "null"} else value
|
|
|
|
|
|
def _set_setting(runner: CommandRunner, serial: str, key: str, value: str | None) -> None:
|
|
if value is None:
|
|
_adb(runner, serial, "shell", "settings", "delete", "global", key, timeout=10)
|
|
else:
|
|
_adb(runner, serial, "shell", "settings", "put", "global", key, value, timeout=10)
|
|
|
|
|
|
def _ensure_debug_settings(
|
|
runner: CommandRunner,
|
|
serial: str,
|
|
package: str,
|
|
changed: dict[str, str | None],
|
|
) -> None:
|
|
expected = {
|
|
"gpu_debug_app": package,
|
|
"enable_gpu_debug_layers": "1",
|
|
"gpu_debug_layers": RENDERDOC_VULKAN_LAYER,
|
|
}
|
|
original = {key: _get_setting(runner, serial, key) for key in expected}
|
|
for key, value in expected.items():
|
|
if original[key] != value:
|
|
changed[key] = original[key]
|
|
_set_setting(runner, serial, key, value)
|
|
actual = _get_setting(runner, serial, key)
|
|
if actual != value:
|
|
raise RuntimeError(
|
|
f"failed to set Android global setting {key}={value!r}; got {actual!r}. "
|
|
"Enable the developer option that permits USB debugging to write secure "
|
|
"settings, or configure RenderDoc's GPU debug layer from the device UI."
|
|
)
|
|
|
|
|
|
def _encode_capture_options(soft_memory_limit_mib: int) -> str:
|
|
rd = find_renderdoc()
|
|
if rd is None:
|
|
raise RuntimeError("RenderDoc Python module not found; run 'rdc setup-renderdoc'")
|
|
options = rd.GetDefaultCaptureOptions()
|
|
options.softMemoryLimit = soft_memory_limit_mib
|
|
return options.EncodeAsString()
|
|
|
|
|
|
def _map_target_call_to_swap(
|
|
runner: CommandRunner, trace: Path, target_call: int, apitrace: str
|
|
) -> tuple[int, int, str]:
|
|
completed = runner.run(
|
|
[apitrace, "dump", "--grep=eglSwapBuffers", str(trace)], timeout=120
|
|
)
|
|
swaps: list[int] = []
|
|
for line in completed.stdout.splitlines():
|
|
match = re.match(r"^(\d+)\s+eglSwapBuffers\b", line)
|
|
if match:
|
|
swaps.append(int(match.group(1)))
|
|
if not swaps:
|
|
raise RuntimeError(f"no eglSwapBuffers calls found in {trace}")
|
|
for index, call in enumerate(swaps):
|
|
if call == target_call:
|
|
return index, call, "exact-swap-call"
|
|
if call > target_call:
|
|
return index, call, "first-swap-after-call"
|
|
raise RuntimeError(
|
|
f"target call {target_call} is after the final eglSwapBuffers call {swaps[-1]}"
|
|
)
|
|
|
|
|
|
def _capture_frame_for_backend(backend: str, target_swap: int) -> int:
|
|
if backend == "DirectVulkan":
|
|
if target_swap == 0:
|
|
raise RuntimeError("DirectVulkan cannot capture target swap 0 with the -1 frame rule")
|
|
return target_swap - 1
|
|
return target_swap
|
|
|
|
|
|
def _forward_map(runner: CommandRunner, serial: str) -> dict[int, str]:
|
|
completed = _adb(runner, serial, "forward", "--list", timeout=10)
|
|
forwards: dict[int, str] = {}
|
|
for line in completed.stdout.splitlines():
|
|
fields = line.split()
|
|
if len(fields) < 3 or fields[0] != serial or not fields[1].startswith("tcp:"):
|
|
continue
|
|
try:
|
|
forwards[int(fields[1].removeprefix("tcp:"))] = fields[2]
|
|
except ValueError:
|
|
continue
|
|
return forwards
|
|
|
|
|
|
def _sha256(path: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as source:
|
|
for chunk in iter(lambda: source.read(1024 * 1024), b""):
|
|
digest.update(chunk)
|
|
return digest.hexdigest().upper()
|
|
|
|
|
|
def _read_remote_json(
|
|
runner: CommandRunner, serial: str, package: str, path: str
|
|
) -> dict[str, object] | None:
|
|
completed = _adb(
|
|
runner,
|
|
serial,
|
|
"exec-out",
|
|
"run-as",
|
|
package,
|
|
"cat",
|
|
path,
|
|
check=False,
|
|
timeout=15,
|
|
)
|
|
if completed.returncode != 0 or not completed.stdout.strip():
|
|
return None
|
|
return json.loads(completed.stdout)
|
|
|
|
|
|
def _write_manifest(path: Path, data: dict[str, object]) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description=(
|
|
"Replay an apitrace fixture with MobileGL on Android and capture an exact "
|
|
"DirectVulkan or DirectGLES frame with RenderDoc."
|
|
)
|
|
)
|
|
parser.add_argument("--trace", type=Path, required=True)
|
|
parser.add_argument("--golden", type=Path, required=True)
|
|
parser.add_argument("--target-call", type=int, required=True)
|
|
parser.add_argument("--target-swap", type=int, help="zero-based target swap; infer when omitted")
|
|
parser.add_argument("--capture-frame", type=int, help="override backend-specific frame mapping")
|
|
parser.add_argument("--backend", choices=("DirectVulkan", "DirectGLES"), required=True)
|
|
parser.add_argument("--output", type=Path, required=True)
|
|
parser.add_argument("--serial")
|
|
parser.add_argument("--package", default=DEFAULT_PACKAGE)
|
|
parser.add_argument("--activity", default=DEFAULT_ACTIVITY)
|
|
parser.add_argument("--action", default=DEFAULT_ACTION)
|
|
parser.add_argument("--width", type=int, default=854)
|
|
parser.add_argument("--height", type=int, default=480)
|
|
parser.add_argument("--ssim-threshold", type=float, default=0.99)
|
|
parser.add_argument("--apitrace", default="apitrace")
|
|
parser.add_argument("--local-port", type=int, default=38920)
|
|
parser.add_argument("--target-ident", type=int, default=38920)
|
|
parser.add_argument("--connect-timeout", type=float, default=20.0)
|
|
parser.add_argument("--capture-timeout", type=float, default=90.0)
|
|
parser.add_argument("--soft-memory-limit", type=int, default=256, metavar="MIB")
|
|
parser.add_argument("--no-setup-renderdoc", action="store_true")
|
|
parser.add_argument("--keep-device-files", action="store_true")
|
|
parser.add_argument("--keep-device-state", action="store_true")
|
|
parser.add_argument("--overwrite", action="store_true")
|
|
parser.add_argument("--json", action="store_true")
|
|
args = parser.parse_args()
|
|
|
|
runner = CommandRunner(verbose=True)
|
|
trace = args.trace.resolve()
|
|
golden = args.golden.resolve()
|
|
output = args.output.resolve()
|
|
manifest_path = output.with_suffix(output.suffix + ".json")
|
|
for path, label in ((trace, "trace"), (golden, "golden")):
|
|
if not path.is_file():
|
|
raise RuntimeError(f"{label} file does not exist: {path}")
|
|
if output.exists() and not args.overwrite:
|
|
raise RuntimeError(f"output exists; pass --overwrite to replace it: {output}")
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
serial = _resolve_serial(runner, args.serial)
|
|
connected = _connected_serials(runner)
|
|
initial_forwards = _forward_map(runner, serial)
|
|
|
|
if args.target_swap is None:
|
|
target_swap, target_swap_call, mapping = _map_target_call_to_swap(
|
|
runner, trace, args.target_call, args.apitrace
|
|
)
|
|
else:
|
|
target_swap = args.target_swap
|
|
target_swap_call = args.target_call
|
|
mapping = "explicit"
|
|
capture_frame = (
|
|
args.capture_frame
|
|
if args.capture_frame is not None
|
|
else _capture_frame_for_backend(args.backend, target_swap)
|
|
)
|
|
|
|
token = f"{int(time.time())}-{os.getpid()}"
|
|
remote_trace = f"/data/local/tmp/renderdoc-retrace-{token}.trace"
|
|
remote_golden = f"/data/local/tmp/renderdoc-retrace-{token}-golden.png"
|
|
replay_root = (
|
|
f"/data/user/0/{args.package}/files/trace-replay/renderdoc-capture-{token}"
|
|
)
|
|
replay_output = f"{replay_root}/output"
|
|
remote_result = f"{replay_output}/result.json"
|
|
queue_script = Path(__file__).with_name("queue_android_frame.py")
|
|
|
|
original_capopts = _get_property(runner, serial, "debug.rdoc.RENDERDOC_CAPOPTS")
|
|
original_gles_layers = _get_property(runner, serial, "debug.gles.layers")
|
|
original_settings: dict[str, str | None] = {}
|
|
original_forward = initial_forwards.get(args.local_port)
|
|
touched_forward_ports = {args.local_port}
|
|
private_gles_layer = f"/data/user/0/{args.package}/{RENDERDOC_GLES_LAYER}"
|
|
private_layer_existed = (
|
|
_adb(
|
|
runner,
|
|
serial,
|
|
"shell",
|
|
"run-as",
|
|
args.package,
|
|
"test",
|
|
"-f",
|
|
private_gles_layer,
|
|
check=False,
|
|
timeout=10,
|
|
).returncode
|
|
== 0
|
|
)
|
|
capture_process: subprocess.Popen[str] | None = None
|
|
result: dict[str, object] = {}
|
|
succeeded = False
|
|
stop_renderdoc_server = False
|
|
|
|
try:
|
|
if not args.no_setup_renderdoc:
|
|
_setup_renderdoc(runner, serial, connected)
|
|
setup_forwards = _forward_map(runner, serial)
|
|
stop_renderdoc_server = any(
|
|
remote == "localabstract:renderdoc_39920"
|
|
and initial_forwards.get(port) != remote
|
|
for port, remote in setup_forwards.items()
|
|
)
|
|
touched_forward_ports.update(
|
|
port
|
|
for port in set(initial_forwards) | set(setup_forwards)
|
|
if initial_forwards.get(port) != setup_forwards.get(port)
|
|
)
|
|
original_forward = setup_forwards.get(args.local_port)
|
|
|
|
_adb(runner, serial, "shell", "am", "force-stop", args.package, timeout=15)
|
|
_ensure_debug_settings(runner, serial, args.package, original_settings)
|
|
if args.backend == "DirectVulkan":
|
|
capopts = _encode_capture_options(args.soft_memory_limit)
|
|
_set_property(runner, serial, "debug.rdoc.RENDERDOC_CAPOPTS", capopts)
|
|
_set_property(runner, serial, "debug.gles.layers", ":")
|
|
else:
|
|
_adb(
|
|
runner,
|
|
serial,
|
|
"shell",
|
|
"run-as",
|
|
args.package,
|
|
"cp",
|
|
REMOTE_GLES_LAYER,
|
|
private_gles_layer,
|
|
timeout=30,
|
|
)
|
|
_adb(
|
|
runner,
|
|
serial,
|
|
"shell",
|
|
"run-as",
|
|
args.package,
|
|
"chmod",
|
|
"755",
|
|
private_gles_layer,
|
|
timeout=10,
|
|
)
|
|
_set_property(runner, serial, "debug.gles.layers", RENDERDOC_GLES_LAYER)
|
|
|
|
_adb(runner, serial, "push", trace, remote_trace, timeout=120)
|
|
_adb(runner, serial, "push", golden, remote_golden, timeout=60)
|
|
_adb(
|
|
runner,
|
|
serial,
|
|
"shell",
|
|
"run-as",
|
|
args.package,
|
|
"rm",
|
|
"-rf",
|
|
replay_root,
|
|
timeout=20,
|
|
)
|
|
_adb(
|
|
runner,
|
|
serial,
|
|
"shell",
|
|
"run-as",
|
|
args.package,
|
|
"mkdir",
|
|
"-p",
|
|
replay_output,
|
|
timeout=20,
|
|
)
|
|
|
|
wanted_forward = f"localabstract:renderdoc_{args.target_ident}"
|
|
if original_forward != wanted_forward:
|
|
if original_forward is not None:
|
|
_adb(
|
|
runner,
|
|
serial,
|
|
"forward",
|
|
"--remove",
|
|
f"tcp:{args.local_port}",
|
|
timeout=10,
|
|
)
|
|
_adb(
|
|
runner,
|
|
serial,
|
|
"forward",
|
|
f"tcp:{args.local_port}",
|
|
wanted_forward,
|
|
timeout=10,
|
|
)
|
|
|
|
queue_command = [
|
|
sys.executable,
|
|
str(queue_script),
|
|
"--host",
|
|
"127.0.0.1",
|
|
"--ident",
|
|
str(args.local_port),
|
|
"--frame",
|
|
str(capture_frame),
|
|
"--connect-timeout",
|
|
str(args.connect_timeout),
|
|
"--capture-timeout",
|
|
str(args.capture_timeout),
|
|
"--json",
|
|
]
|
|
runner.commands.append(queue_command)
|
|
print(f"+ {subprocess.list2cmdline(queue_command)}", file=sys.stderr, flush=True)
|
|
capture_process = subprocess.Popen(
|
|
queue_command,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
encoding="utf-8",
|
|
errors="replace",
|
|
)
|
|
time.sleep(0.5)
|
|
|
|
component = f"{args.package}/{args.activity}"
|
|
launch_command = [
|
|
"adb",
|
|
"-s",
|
|
serial,
|
|
"shell",
|
|
"am",
|
|
"start",
|
|
"-W",
|
|
"-a",
|
|
args.action,
|
|
"-n",
|
|
component,
|
|
"--es",
|
|
"trace_path",
|
|
remote_trace,
|
|
"--es",
|
|
"golden_path",
|
|
remote_golden,
|
|
"--es",
|
|
"output_dir",
|
|
replay_output,
|
|
"--es",
|
|
"diff_path",
|
|
f"{replay_output}/diff.png",
|
|
"--es",
|
|
"backend",
|
|
args.backend,
|
|
"--el",
|
|
"target_call",
|
|
str(args.target_call),
|
|
"--ei",
|
|
"width",
|
|
str(args.width),
|
|
"--ei",
|
|
"height",
|
|
str(args.height),
|
|
"--es",
|
|
"ssim_threshold",
|
|
str(args.ssim_threshold),
|
|
"--ei",
|
|
"crop_x",
|
|
"0",
|
|
"--ei",
|
|
"crop_y",
|
|
"0",
|
|
"--ei",
|
|
"crop_width",
|
|
"0",
|
|
"--ei",
|
|
"crop_height",
|
|
"0",
|
|
"--ez",
|
|
"coherent_as_flush",
|
|
"true",
|
|
]
|
|
runner.run(launch_command, timeout=30)
|
|
|
|
stdout, stderr = capture_process.communicate(
|
|
timeout=args.connect_timeout + args.capture_timeout + 15
|
|
)
|
|
capture_returncode = capture_process.returncode
|
|
capture_process = None
|
|
if stderr.strip():
|
|
print(stderr, end="", file=sys.stderr)
|
|
if not stdout.strip():
|
|
raise RuntimeError("queue helper returned no JSON output")
|
|
capture_result = json.loads(stdout)
|
|
if capture_result.get("success") is not True:
|
|
raise RuntimeError(f"RenderDoc capture failed: {capture_result.get('error')}")
|
|
if capture_returncode != 0:
|
|
raise RuntimeError(f"queue helper exited with {capture_returncode}")
|
|
capture = capture_result["capture"]
|
|
if not isinstance(capture, dict) or not capture.get("path"):
|
|
raise RuntimeError(f"queue helper returned invalid capture metadata: {capture!r}")
|
|
|
|
_adb(runner, serial, "pull", capture["path"], output, timeout=180)
|
|
if not output.is_file() or output.stat().st_size <= 0:
|
|
raise RuntimeError(f"adb pull did not produce a non-empty RDC: {output}")
|
|
retrace_result = _read_remote_json(
|
|
runner, serial, args.package, remote_result
|
|
)
|
|
result = {
|
|
"success": True,
|
|
"deviceSerial": serial,
|
|
"backend": args.backend,
|
|
"targetCall": args.target_call,
|
|
"targetSwap": target_swap,
|
|
"targetSwapCall": target_swap_call,
|
|
"targetSwapMapping": mapping,
|
|
"captureFrame": capture_frame,
|
|
"capture": capture,
|
|
"retraceResult": retrace_result,
|
|
"output": str(output),
|
|
"byteSize": output.stat().st_size,
|
|
"sha256": _sha256(output),
|
|
}
|
|
succeeded = True
|
|
except Exception as exc: # noqa: BLE001 - stable CLI error boundary
|
|
result = {
|
|
"success": False,
|
|
"deviceSerial": serial,
|
|
"backend": args.backend,
|
|
"targetCall": args.target_call,
|
|
"targetSwap": target_swap,
|
|
"captureFrame": capture_frame,
|
|
"output": str(output),
|
|
"error": str(exc),
|
|
}
|
|
finally:
|
|
if capture_process is not None and capture_process.poll() is None:
|
|
capture_process.kill()
|
|
capture_process.wait(timeout=10)
|
|
if not args.keep_device_state:
|
|
try:
|
|
_set_property(
|
|
runner,
|
|
serial,
|
|
"debug.rdoc.RENDERDOC_CAPOPTS",
|
|
original_capopts,
|
|
)
|
|
_set_property(runner, serial, "debug.gles.layers", original_gles_layers or ":")
|
|
for key, value in original_settings.items():
|
|
_set_setting(runner, serial, key, value)
|
|
if args.backend == "DirectGLES" and not private_layer_existed:
|
|
_adb(
|
|
runner,
|
|
serial,
|
|
"shell",
|
|
"run-as",
|
|
args.package,
|
|
"rm",
|
|
"-f",
|
|
private_gles_layer,
|
|
check=False,
|
|
timeout=10,
|
|
)
|
|
_adb(
|
|
runner,
|
|
serial,
|
|
"shell",
|
|
"am",
|
|
"force-stop",
|
|
args.package,
|
|
check=False,
|
|
timeout=15,
|
|
)
|
|
except Exception as exc: # noqa: BLE001
|
|
print(f"warning: device state cleanup failed: {exc}", file=sys.stderr)
|
|
try:
|
|
current_forwards = _forward_map(runner, serial)
|
|
for port in sorted(touched_forward_ports):
|
|
current = current_forwards.get(port)
|
|
original = initial_forwards.get(port)
|
|
if current == original:
|
|
continue
|
|
if current is not None:
|
|
_adb(
|
|
runner,
|
|
serial,
|
|
"forward",
|
|
"--remove",
|
|
f"tcp:{port}",
|
|
check=False,
|
|
timeout=10,
|
|
)
|
|
if original is not None:
|
|
_adb(
|
|
runner,
|
|
serial,
|
|
"forward",
|
|
f"tcp:{port}",
|
|
original,
|
|
timeout=10,
|
|
)
|
|
if stop_renderdoc_server:
|
|
_adb(
|
|
runner,
|
|
serial,
|
|
"shell",
|
|
"am",
|
|
"force-stop",
|
|
RENDERDOC_CMD_PACKAGE,
|
|
check=False,
|
|
timeout=15,
|
|
)
|
|
except Exception as exc: # noqa: BLE001
|
|
print(f"warning: adb forward cleanup failed: {exc}", file=sys.stderr)
|
|
|
|
if not args.keep_device_files:
|
|
try:
|
|
_adb(
|
|
runner,
|
|
serial,
|
|
"shell",
|
|
"rm",
|
|
"-f",
|
|
remote_trace,
|
|
remote_golden,
|
|
check=False,
|
|
timeout=20,
|
|
)
|
|
_adb(
|
|
runner,
|
|
serial,
|
|
"shell",
|
|
"run-as",
|
|
args.package,
|
|
"rm",
|
|
"-rf",
|
|
replay_root,
|
|
check=False,
|
|
timeout=20,
|
|
)
|
|
except Exception as exc: # noqa: BLE001
|
|
print(f"warning: device file cleanup failed: {exc}", file=sys.stderr)
|
|
|
|
result["commands"] = runner.commands
|
|
result["commandLines"] = [
|
|
subprocess.list2cmdline(command) for command in runner.commands
|
|
]
|
|
try:
|
|
_write_manifest(manifest_path, result)
|
|
except Exception as exc: # noqa: BLE001
|
|
print(f"warning: failed to write manifest: {exc}", file=sys.stderr)
|
|
|
|
if args.json:
|
|
print(json.dumps(result, ensure_ascii=False))
|
|
elif succeeded:
|
|
print(output)
|
|
print(f"manifest: {manifest_path}", file=sys.stderr)
|
|
print(f"sha256: {result['sha256']}", file=sys.stderr)
|
|
else:
|
|
print(f"error: {result['error']}", file=sys.stderr)
|
|
return 0 if succeeded else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|