Files
zeekayandHanzo Dev 19794c19d5 sandbox: android — a phone is a window on the screen that already exists
An Android emulator is qemu drawing an ordinary X window, and the desktop class
already runs an X server whose pixels leave the pod through the exec channel. So
looking at a phone needs no port, no socket, no second client and no second
streaming path — it needs an SDK on the display that is already served. The class
is `FROM desktop AS android` plus an AVD plus one argument.

sandbox-desktop takes a command now. Its last line was `exec sleep`; it is
`exec "$@"` when given something, so `CMD ["sandbox-desktop", "avd"]` is one
screen with the class's own program on it. A second script would be a second copy
of the same four processes, and only one copy ever gets the next fix.

`avd` refuses at startup where /dev/kvm is absent or refuses to open. Present and
openable are different facts — a mounted device the container's cgroup denies
shows up in `ls -l` and fails only at open() — and without KVM a cold Android
boot does not finish in any time a person waits, which is a failure that looks
exactly like progress.

Proven on x86_64 with KVM, running these scripts verbatim: boot completed in
24.7s, adb sees emulator-5554, one X window titled "Android Emulator", 5900 and
6080 LISTENing on loopback only, and a booted Pixel 6 rendered in a browser.

amd64 only, and this time that is the image's limit rather than the fleet's:
Google publishes no linux-aarch64 emulator.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 13:19:44 -07:00

67 lines
2.9 KiB
Bash

#!/usr/bin/env bash
# The android class's program: one phone, on the screen the desktop already
# started. Run as `sandbox-desktop avd`, so the X server, the window manager and
# the VNC pair are that script's business and this one only draws into them.
#
# WHY THIS IS NOT A SECOND STREAMING PATH, which is the thing it would be easiest
# to accidentally build. The emulator opens an ordinary X window on ${DISPLAY}.
# The screen the fleet already serves is that display, scraped by TigerVNC and
# carried out through the Kubernetes exec subresource
# (hanzoai/cloud apps/sandbox/screen.go). A phone is therefore a WINDOW, and
# looking at one costs no new port, no new socket and no new client — which is
# why the android class is `desktop` plus an SDK rather than a display of its own.
#
# THE EMULATOR IS QEMU AND IT WANTS /dev/kvm. Without it the emulator falls back
# to software CPU emulation, where a cold boot of Android 15 does not finish in
# any time a person will wait — measured, not feared. That failure is silent in
# the worst way: qemu starts, the window appears, the boot animation runs, and
# nothing ever becomes usable. So the device is checked FIRST and its absence is
# an exit rather than twenty minutes of apparent progress.
set -euo pipefail
AVD="${SANDBOX_AVD:-hanzo}"
SDK="${ANDROID_HOME:-/opt/android}"
export ANDROID_HOME="${SDK}" ANDROID_SDK_ROOT="${SDK}"
export PATH="${SDK}/platform-tools:${SDK}/emulator:${SDK}/cmdline-tools/latest/bin:${PATH}"
log() { printf '[avd] %s\n' "$*" >&2; }
if [ ! -e /dev/kvm ]; then
log "no /dev/kvm: this pod was scheduled somewhere the android class cannot run"
exit 1
fi
# PRESENT AND OPENABLE ARE DIFFERENT FACTS, and the gap between them is a whole
# evening. A hostPath mount puts the node's /dev/kvm in the container and the
# container's device cgroup still refuses every open with EPERM — the file is
# right there, `ls` shows it, and only a program that opens it finds out. Ask the
# question the emulator will ask.
if ! (exec 3<>/dev/kvm) 2>/dev/null; then
log "/dev/kvm is present and refuses to open for uid $(id -u) groups $(id -G)"
ls -l /dev/kvm >&2 || true
exit 1
fi
log "booting ${AVD}"
# -gpu swiftshader_indirect renders with Mesa into a real X window. `-no-window`
# is the flag that looks right here and is exactly wrong: it runs the emulator
# headless, which is a phone nobody can see.
emulator -avd "${AVD}" \
-no-snapshot -no-audio -no-boot-anim \
-gpu swiftshader_indirect \
-memory "${SANDBOX_AVD_MEM:-4096}" \
-cores "${SANDBOX_AVD_CORES:-2}" \
-accel on &
qemu=$!
# One line in the log that means ANDROID IS UP, as opposed to qemu is up. They
# are ninety seconds apart and nothing else distinguishes them.
(
adb wait-for-device >/dev/null 2>&1 || exit 0
until [ "$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" = "1" ]; do
sleep 2
done
log "boot_completed"
) &
wait "${qemu}"