Skip to content

Commit 8f2a2d1

Browse files
authored
Improve android inspect performace (#660)
1 parent 6a55e07 commit 8f2a2d1

1 file changed

Lines changed: 85 additions & 22 deletions

File tree

server/mobile.py

Lines changed: 85 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,8 @@
44
import os
55
import shutil
66
import subprocess
7-
import base64
87
import json
98
from typing import Literal, Optional
10-
import asyncio
11-
import socket
129
import xml.etree.ElementTree as ET
1310
import zipfile
1411
import plistlib
@@ -125,26 +122,91 @@ def get_ios_devices():
125122
return []
126123

127124

128-
@router.get("/inspect")
129-
def inspect(device_serial: str | None = None):
130-
"""Get the Mobile DOM and screenshot."""
125+
def run_adb_command_bytes(cmd: str, timeout: int = 30) -> bytes:
126+
"""
127+
Run an adb command and return stdout as raw bytes.
128+
129+
- cmd: full command string (example: 'adb -s SERIAL exec-out screencap -p')
130+
- timeout: seconds
131+
132+
Raises RuntimeError if adb fails.
133+
"""
131134
try:
132-
# Capture UI and screenshot
133-
capture_ui_dump(device_serial=device_serial)
134-
capture_screenshot(device_serial=device_serial)
135+
# Use shell=True ONLY if you are passing a full string with quotes
136+
# (like the sh -c command). For normal adb commands, shell=False is better.
137+
#
138+
# Since your combined command uses quotes heavily, we keep shell=True.
139+
p = subprocess.run(
140+
cmd,
141+
stdout=subprocess.PIPE,
142+
stderr=subprocess.PIPE,
143+
timeout=timeout,
144+
shell=True,
145+
)
135146

136-
# Read XML file
137-
with open(UI_XML_PATH, "r") as xml_file:
138-
xml_content = xml_file.read()
147+
if p.returncode != 0:
148+
err = p.stderr.decode("utf-8", errors="replace").strip()
149+
raise RuntimeError(f"ADB command failed ({p.returncode}): {err}")
139150

140-
# Read and encode screenshot
141-
with open(SCREENSHOT_PATH, "rb") as img_file:
142-
screenshot_bytes = img_file.read()
143-
screenshot_base64 = base64.b64encode(screenshot_bytes).decode("utf-8")
151+
return p.stdout
152+
153+
except subprocess.TimeoutExpired:
154+
raise RuntimeError(f"ADB command timed out after {timeout}s: {cmd}")
155+
156+
157+
def fetch_xml_and_screenshot(device_serial: str | None = None) -> tuple[str, bytes]:
158+
"""
159+
Single-function fetch. Primary path uses ONE adb exec-out command to capture
160+
UI XML + PNG (base64) in a single stream. Falls back (still inside this function)
161+
if markers or outputs are invalid.
162+
"""
163+
device_flag = f"-s {device_serial}" if device_serial else ""
164+
165+
SPLIT = "__ZEUZ_SPLIT__"
166+
END = "__ZEUZ_END__"
167+
168+
cmd = (
169+
f'{ADB_PATH} {device_flag} exec-out sh -c '
170+
f'"uiautomator dump /dev/tty; '
171+
f'echo {SPLIT}; '
172+
f'screencap -p | base64; '
173+
f'echo {END}"'
174+
).strip()
175+
176+
out = run_adb_command_bytes(cmd)
177+
178+
text = out.decode("utf-8", errors="replace")
179+
if SPLIT in text and END in text:
180+
xml_part, rest = text.split(SPLIT, 1)
181+
b64_part = rest.split(END, 1)[0]
182+
183+
# Extract real XML starting from <hierarchy
184+
i = xml_part.find("<hierarchy")
185+
xml = xml_part[i:] if i != -1 else ""
186+
187+
# Decode PNG (ignore newlines/spaces)
188+
b64_compact = "".join(b64_part.split())
189+
try:
190+
png = base64.b64decode(b64_compact, validate=False)
191+
except Exception:
192+
png = b""
193+
194+
if "<hierarchy" in xml and png.startswith(b"\x89PNG"):
195+
return xml, png
196+
197+
198+
@router.get("/inspect", response_model=InspectorResponse)
199+
async def inspect(device_serial: str | None = None):
200+
"""Get the Mobile DOM and screenshot (XML + screenshot fetched together)."""
201+
global stop_android_ui_dump
202+
try:
203+
xml, png = await asyncio.to_thread(fetch_xml_and_screenshot, device_serial)
204+
if not xml or not png:
205+
return InspectorResponse(status="error", error="Failed to capture xml/screenshot")
206+
207+
screenshot_base64 = base64.b64encode(png).decode("utf-8")
208+
return InspectorResponse(status="ok", ui_xml=xml, screenshot=screenshot_base64)
144209

145-
return InspectorResponse(
146-
status="ok", ui_xml=xml_content, screenshot=screenshot_base64
147-
)
148210
except Exception as e:
149211
return InspectorResponse(status="error", error=str(e))
150212

@@ -259,7 +321,6 @@ def run_adb_command(command):
259321
result = subprocess.run(
260322
command,
261323
shell=True,
262-
check=True,
263324
stdout=subprocess.PIPE,
264325
stderr=subprocess.PIPE,
265326
text=True,
@@ -412,7 +473,7 @@ async def upload_android_ui_dump():
412473
prev_xml_hash = ""
413474
while True:
414475
try:
415-
capture_ui_dump()
476+
await asyncio.to_thread(capture_ui_dump)
416477
try:
417478
with open(UI_XML_PATH, "r") as xml_file:
418479
xml_content = xml_file.read()
@@ -440,13 +501,15 @@ async def upload_android_ui_dump():
440501
+ "/node_ai_contents/"
441502
)
442503
apiKey = ConfigModule.get_config_value("Authentication", "api-key").strip()
443-
res = requests.post(
504+
res = await asyncio.to_thread(
505+
requests.post,
444506
url,
445507
headers={"X-Api-Key": apiKey},
446508
json={
447509
"dom_mob": {"dom": xml_content},
448510
"node_id": CommonUtil.MachineInfo().getLocalUser().lower(),
449511
},
512+
timeout=10,
450513
)
451514
if res.ok:
452515
CommonUtil.ExecLog("", "UI dump uploaded successfully", iLogLevel=1)

0 commit comments

Comments
 (0)