Skip to content

Commit c6ca7d6

Browse files
committed
AVD manager with macOS compatibility and subprocess handling
1 parent 69595f1 commit c6ca7d6

1 file changed

Lines changed: 144 additions & 87 deletions

File tree

Framework/install_handler/android/emulator.py

Lines changed: 144 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,42 @@ def _build_android_process_env(sdk_root: Path | None = None) -> dict[str, str]:
7777
return env
7878

7979

80+
def _run_avdmanager_capture(
81+
avdmanager: Path,
82+
sdk_root: Path,
83+
args: list[str],
84+
timeout: int
85+
) -> subprocess.CompletedProcess:
86+
"""
87+
Run avdmanager with SDK-root first, with a Darwin-only fallback that drops
88+
--sdk_root but keeps the ZeuZ SDK env vars.
89+
"""
90+
env = _build_android_process_env(sdk_root)
91+
cmd_with_sdk_root = [str(avdmanager), f"--sdk_root={sdk_root}", *args]
92+
result = subprocess.run(
93+
cmd_with_sdk_root,
94+
capture_output=True,
95+
text=True,
96+
timeout=timeout,
97+
env=env
98+
)
99+
if result.returncode == 0 or not _is_darwin():
100+
return result
101+
102+
if debug:
103+
print(f"[installer][emulator] avdmanager command failed with --sdk_root, retrying without it on macOS. stderr: {result.stderr}")
104+
105+
cmd_without_sdk_root = [str(avdmanager), *args]
106+
fallback_result = subprocess.run(
107+
cmd_without_sdk_root,
108+
capture_output=True,
109+
text=True,
110+
timeout=timeout,
111+
env=env
112+
)
113+
return fallback_result
114+
115+
80116
def _read_file_tail(path: Path, max_lines: int = 20) -> str:
81117
"""Read tail of a log file for error hints."""
82118
try:
@@ -158,16 +194,9 @@ async def get_available_avds() -> list[dict]:
158194

159195
# Run avdmanager list avd command using async executor
160196
loop = asyncio.get_event_loop()
161-
env = _build_android_process_env(sdk_root)
162197
result = await loop.run_in_executor(
163198
None,
164-
lambda: subprocess.run(
165-
[str(avdmanager), f"--sdk_root={sdk_root}", "list", "avd"],
166-
capture_output=True,
167-
text=True,
168-
timeout=30,
169-
env=env
170-
)
199+
lambda: _run_avdmanager_capture(avdmanager, sdk_root, ["list", "avd"], 30)
171200
)
172201

173202
if result.returncode != 0:
@@ -264,19 +293,29 @@ async def launch_avd(avd_name: str) -> bool:
264293
log_dir.mkdir(parents=True, exist_ok=True)
265294
log_path = log_dir / f"{sanitized_name}.log"
266295

267-
# Launch emulator in background and validate it does not exit immediately.
268-
with open(log_path, "w", encoding="utf-8") as log_file:
269-
process = subprocess.Popen(
270-
[emulator_path, "-avd", avd_name, "-sdk-root", str(sdk_root)],
271-
stdout=log_file,
272-
stderr=subprocess.STDOUT,
273-
start_new_session=True, # Detach from parent process
274-
env=env
275-
)
276-
277-
# Give emulator time to fail fast (common for missing/invalid system image).
296+
def _spawn_emulator(cmd: list[str]) -> subprocess.Popen:
297+
with open(log_path, "w", encoding="utf-8") as log_file:
298+
return subprocess.Popen(
299+
cmd,
300+
stdout=log_file,
301+
stderr=subprocess.STDOUT,
302+
start_new_session=True, # Detach from parent process
303+
env=env
304+
)
305+
306+
# Try launch with explicit sdk-root first.
307+
process = _spawn_emulator([emulator_path, "-avd", avd_name, "-sdk-root", str(sdk_root)])
278308
await asyncio.sleep(3)
279309
returncode = process.poll()
310+
311+
# macOS compatibility: retry without -sdk-root if first launch exits immediately.
312+
if returncode is not None and _is_darwin():
313+
if debug:
314+
print("[installer][emulator] Emulator exited quickly with -sdk-root on macOS. Retrying without -sdk-root.")
315+
process = _spawn_emulator([emulator_path, "-avd", avd_name])
316+
await asyncio.sleep(3)
317+
returncode = process.poll()
318+
280319
if returncode is not None:
281320
launch_hint = _read_file_tail(log_path)
282321
error_msg = f"Emulator process for {avd_name} exited immediately (code {returncode})."
@@ -664,16 +703,9 @@ async def get_available_devices() -> list[dict]:
664703

665704
# Run avdmanager list device using async executor
666705
loop = asyncio.get_event_loop()
667-
env = _build_android_process_env(sdk_root)
668706
result = await loop.run_in_executor(
669707
None,
670-
lambda: subprocess.run(
671-
[str(avdmanager), f"--sdk_root={sdk_root}", "list", "device"],
672-
capture_output=True,
673-
text=True,
674-
timeout=60,
675-
env=env
676-
)
708+
lambda: _run_avdmanager_capture(avdmanager, sdk_root, ["list", "device"], 60)
677709
)
678710

679711
if result.returncode != 0:
@@ -848,13 +880,7 @@ def _get_existing_avd_names() -> list[str]:
848880
if not avdmanager:
849881
return []
850882

851-
result = subprocess.run(
852-
[str(avdmanager), f"--sdk_root={sdk_root}", "list", "avd"],
853-
capture_output=True,
854-
text=True,
855-
timeout=30,
856-
env=_build_android_process_env(sdk_root)
857-
)
883+
result = _run_avdmanager_capture(avdmanager, sdk_root, ["list", "avd"], 30)
858884

859885
if result.returncode != 0:
860886
return []
@@ -1321,60 +1347,91 @@ def _run_avdmanager_create_linux(avdmanager: Path, sdk_root: Path, avd_name: str
13211347
def _run_avdmanager_create_darwin(avdmanager: Path, sdk_root: Path, avd_name: str, system_image: str, device_id: str) -> tuple[bool, str]:
13221348
"""Create AVD on macOS with real-time output"""
13231349
try:
1324-
# Create AVD: avdmanager create avd -n {avd_name} -k {system_image} -d {device_id}
1325-
# Answer "no" to custom hardware profile prompt
13261350
env = _build_android_process_env(sdk_root)
1327-
process = subprocess.Popen(
1328-
[str(avdmanager), f"--sdk_root={sdk_root}", "create", "avd", "-n", avd_name, "-k", system_image, "-d", device_id],
1329-
stdin=subprocess.PIPE,
1330-
stdout=subprocess.PIPE,
1331-
stderr=subprocess.STDOUT,
1332-
text=True,
1333-
bufsize=1, # Line buffered
1334-
env=env
1335-
)
1336-
1337-
# Send "no" to custom hardware profile prompt
1338-
process.stdin.write("no\n")
1339-
process.stdin.close()
1340-
1341-
# Print output in real-time as it comes, showing progress on single line
1342-
output_lines = []
1343-
last_progress = ""
1344-
try:
1345-
for line in iter(process.stdout.readline, ''):
1346-
if line:
1347-
stripped = line.strip()
1348-
output_lines.append(stripped)
1349-
1350-
# Extract progress percentage from lines like "[====] 25% Loading..."
1351-
progress_match = re.search(r'\[.*?\]\s*(\d+)%\s*(.+)', stripped)
1352-
if progress_match:
1353-
percent = progress_match.group(1)
1354-
status = progress_match.group(2).strip()
1355-
current_progress = f"{percent}% {status}"
1356-
if current_progress != last_progress:
1357-
print(f"\r[installer][emulator] Download progress: {current_progress}", end='', flush=True)
1358-
last_progress = current_progress
1359-
elif stripped and not stripped.startswith('[') and '%' not in stripped:
1360-
# Print important non-progress messages on new line
1361-
print(f"\n[installer][emulator] {stripped}")
1362-
elif stripped.endswith('%'):
1363-
# Handle lines that end with just percentage
1364-
print(f"\r[installer][emulator] Download progress: {stripped}", end='', flush=True)
1365-
except Exception as e:
1366-
print(f"\n[installer][emulator] Output reading error: {e}")
1367-
finally:
1368-
print() # New line after progress completes
1369-
1370-
process.stdout.close()
1371-
returncode = process.wait(timeout=120)
1372-
1373-
output = "\n".join(output_lines)
1374-
if returncode == 0:
1351+
1352+
def _run_create_command(cmd: list[str]) -> tuple[bool, str]:
1353+
# Create AVD: avdmanager create avd -n {avd_name} -k {system_image} -d {device_id}
1354+
# Answer "no" to custom hardware profile prompt
1355+
process = subprocess.Popen(
1356+
cmd,
1357+
stdin=subprocess.PIPE,
1358+
stdout=subprocess.PIPE,
1359+
stderr=subprocess.STDOUT,
1360+
text=True,
1361+
bufsize=1, # Line buffered
1362+
env=env
1363+
)
1364+
1365+
process.stdin.write("no\n")
1366+
process.stdin.close()
1367+
1368+
output_lines = []
1369+
last_progress = ""
1370+
try:
1371+
for line in iter(process.stdout.readline, ''):
1372+
if line:
1373+
stripped = line.strip()
1374+
output_lines.append(stripped)
1375+
1376+
# Extract progress percentage from lines like "[====] 25% Loading..."
1377+
progress_match = re.search(r'\[.*?\]\s*(\d+)%\s*(.+)', stripped)
1378+
if progress_match:
1379+
percent = progress_match.group(1)
1380+
status = progress_match.group(2).strip()
1381+
current_progress = f"{percent}% {status}"
1382+
if current_progress != last_progress:
1383+
print(f"\r[installer][emulator] Download progress: {current_progress}", end='', flush=True)
1384+
last_progress = current_progress
1385+
elif stripped and not stripped.startswith('[') and '%' not in stripped:
1386+
# Print important non-progress messages on new line
1387+
print(f"\n[installer][emulator] {stripped}")
1388+
elif stripped.endswith('%'):
1389+
# Handle lines that end with just percentage
1390+
print(f"\r[installer][emulator] Download progress: {stripped}", end='', flush=True)
1391+
except Exception as e:
1392+
print(f"\n[installer][emulator] Output reading error: {e}")
1393+
finally:
1394+
print() # New line after progress completes
1395+
1396+
process.stdout.close()
1397+
returncode = process.wait(timeout=120)
1398+
output = "\n".join(output_lines)
1399+
return returncode == 0, output
1400+
1401+
primary_cmd = [
1402+
str(avdmanager),
1403+
f"--sdk_root={sdk_root}",
1404+
"create",
1405+
"avd",
1406+
"-n",
1407+
avd_name,
1408+
"-k",
1409+
system_image,
1410+
"-d",
1411+
device_id,
1412+
]
1413+
success, output = _run_create_command(primary_cmd)
1414+
if success:
13751415
return True, output
1376-
else:
1377-
return False, output
1416+
1417+
# macOS-only compatibility fallback for avdmanager builds that reject --sdk_root.
1418+
if debug:
1419+
print("[installer][emulator] Retrying AVD create without --sdk_root on macOS.")
1420+
fallback_cmd = [
1421+
str(avdmanager),
1422+
"create",
1423+
"avd",
1424+
"-n",
1425+
avd_name,
1426+
"-k",
1427+
system_image,
1428+
"-d",
1429+
device_id,
1430+
]
1431+
fallback_success, fallback_output = _run_create_command(fallback_cmd)
1432+
if fallback_success:
1433+
return True, fallback_output
1434+
return False, fallback_output or output
13781435
except subprocess.TimeoutExpired:
13791436
return False, "AVD creation timed out"
13801437
except Exception as e:
@@ -1871,4 +1928,4 @@ async def create_avd_from_system_image(device_param: str) -> bool:
18711928
"comment": error_msg,
18721929
}
18731930
})
1874-
return False
1931+
return False

0 commit comments

Comments
 (0)