Files
emulator-tools/bin/emu
T
irisandClaude Opus 5 5d77599f3f Collect the emulator tooling into one repo
ui-trace, the adb wrapper and its device recorder were loose files under
~/.local, and each Android checkout carried its own copy of the same
emulator boot sequence. This puts them together, with an install script that
symlinks them back so editing the repo is editing what runs.

Two things are new rather than moved. `emu` is the emulator lifecycle --
name, serial, list, up, down -- keyed on the AVD named after the enclosing
checkout, which is the rule that lets several sessions work here at once;
and `adb` now fills in `-s` from that same rule, because with two emulators
attached a bare `adb shell pm list packages` comes back empty rather than
failing, which reads as the app being uninstalled rather than the question
being ambiguous.

`emu up` refuses when the machine has no room. On 2026-08-30 an emulator
started with 2.8 GB available invoked the OOM killer, and what it took was
not the emulator that had just started: it walked the user slice and killed
pipewire, dbus-broker and another session's emulator first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 13:06:17 -04:00

188 lines
7.2 KiB
Bash
Executable File

#!/bin/bash
# This checkout's emulator: which one it is, whether there is room for it,
# and getting it up and down.
#
# The AVD is named after the enclosing git checkout and nobody has to say so
# (see `project_avd`). That is the whole point: several agent sessions work on
# this machine at once, and an emulator named after the checkout is one that
# cannot be somebody else's to install onto or take the foreground from.
#
# `up` refuses rather than tries when memory is short, which is not caution
# for its own sake. On 2026-08-30 an emulator started with 2.8 GB available
# invoked the OOM killer, and what it took was not the emulator that had just
# started: it walked the user slice and killed pipewire, dbus-broker and
# another session's running emulator first. The cost of one too many lands on
# somebody else's work, minutes later, looking like an unrelated crash.
set -euo pipefail
. "$(dirname "$(readlink -f "$0")")/../lib/project-avd.sh"
# What one costs, measured: 3.8 GB resident for a headless x86_64 AVD, plus
# what it grows into while an app builds and installs.
EMU_EXPECTED_MB=${EMU_EXPECTED_MB:-4200}
# What must be left afterwards. A machine at exactly zero swaps itself to a
# halt rather than failing cleanly.
EMU_HEADROOM_MB=${EMU_HEADROOM_MB:-1200}
# Two are comfortable on 23 GB; three is what invoked the OOM killer.
EMU_MAX_RUNNING=${EMU_MAX_RUNNING:-2}
DEVICE_PROFILE=${DEVICE_PROFILE:-pixel_10}
SYSTEM_IMAGE=${SYSTEM_IMAGE:-system-images;android-36;google_apis;x86_64}
sdk=$(android_sdk) || {
echo "emu: no Android SDK found; set ANDROID_HOME" >&2
exit 127
}
adb=$(android_adb)
avd=$(project_avd)
export ANDROID_AVD_HOME="${ANDROID_AVD_HOME:-$HOME/.android/avd}"
usage() {
cat >&2 <<'USAGE'
usage: emu <command>
name the AVD this directory means
serial its adb serial, if it is running (exit 1 if not)
list every emulator attached, with its AVD and what it is costing
up start it, refusing if the machine has no room
down stop it
Environment: AVD_NAME overrides the name, EMU_FORCE=1 overrides the memory
refusal, DEVICE_PROFILE and SYSTEM_IMAGE decide what `up` creates.
USAGE
exit 2
}
# Every emulator's serial, AVD and resident size -- the last being the number
# the refusal below is about, so it is worth seeing before asking for another.
cmd_list() {
found=false
while IFS=$'\t' read -r serial name; do
found=true
port=${serial#emulator-}
rss=$(ps -eo rss,args | awk -v p="-port $port" '$0 ~ p && $0 ~ /qemu-system/ {print int($1/1024); exit}')
printf ' %-16s %-24s %s\n' "$serial" "$name" "${rss:+$rss MB}"
done < <(running_avds "$adb")
[ "$found" = true ] || echo " (none attached)"
}
# Whether starting one now is likely to cost somebody else their work.
#
# Reported in full rather than as a bare refusal: the caller cannot see this
# machine, and "not enough memory" without the numbers or a way forward is a
# message that gets overridden blind.
check_room() {
available=$(mem_available_mb)
needed=$((EMU_EXPECTED_MB + EMU_HEADROOM_MB))
running=$(running_avds "$adb" | wc -l)
problem=""
if [ "$running" -ge "$EMU_MAX_RUNNING" ]; then
problem="$running emulator(s) are already running, and $EMU_MAX_RUNNING is as many as this machine takes"
elif [ "$available" -lt "$needed" ]; then
problem="$available MB available, and one of these needs about $EMU_EXPECTED_MB MB plus $EMU_HEADROOM_MB MB of headroom"
fi
[ -z "$problem" ] && return 0
if [ "${EMU_FORCE:-}" = 1 ]; then
echo "emu: $problem -- starting anyway because EMU_FORCE=1" >&2
return 0
fi
{
echo "emu: not starting '$avd' -- $problem."
echo
echo " Emulators attached:"
cmd_list
echo
echo " Biggest processes:"
ps -eo rss,comm --sort=-rss | awk 'NR>1 && NR<=6 {printf " %-24s %6.0f MB\n", $2, $1/1024}'
echo
echo " What usually frees enough:"
echo " gradle --stop (an idle Gradle daemon holds 1-2 GB between builds)"
echo " emu down (in the checkout that owns an emulator you are done with)"
echo " ask the session that owns one -- they are named after their checkout"
echo " EMU_FORCE=1 emu up (if you have decided the numbers above are wrong)"
} >&2
exit 1
}
cmd_up() {
if serial=$(avd_serial "$adb" "$avd"); then
echo "emu: '$avd' is already running ($serial)" >&2
echo "$serial"
return 0
fi
check_room
if [ ! -f "$ANDROID_AVD_HOME/$avd.ini" ]; then
echo "emu: creating AVD '$avd' ($DEVICE_PROFILE, $SYSTEM_IMAGE)" >&2
"$sdk/cmdline-tools/latest/bin/sdkmanager" "emulator" "$SYSTEM_IMAGE" >/dev/null ||
echo "emu: sdkmanager could not confirm the system image; trying anyway" >&2
echo no | "$sdk/cmdline-tools/latest/bin/avdmanager" create avd \
-n "$avd" -k "$SYSTEM_IMAGE" --device "$DEVICE_PROFILE" --sdcard 512M >&2
fi
# The host keyboard, so anything with a text field can be typed into
# rather than tapped out on the on-screen one.
config="$ANDROID_AVD_HOME/$avd.avd/config.ini"
if [ -f "$config" ] && ! grep -qx 'hw.keyboard=yes' "$config"; then
grep -v '^hw\.keyboard=' "$config" >"$config.tmp"
echo "hw.keyboard=yes" >>"$config.tmp"
mv "$config.tmp" "$config"
fi
# A stray process for this AVD that never registered with adb. The
# bracketed first character keeps the pattern from matching the shell
# running this script, which would kill it mid-flight.
pkill -f "[e]mulator.*-avd $avd" >/dev/null 2>&1 || true
log="/tmp/$avd-emulator.log"
: >"$log"
if [ -n "${DISPLAY:-}" ] || [ -n "${WAYLAND_DISPLAY:-}" ]; then
echo "emu: starting '$avd' with GPU acceleration" >&2
"$sdk/emulator/emulator" -avd "$avd" -gpu host -no-audio >"$log" 2>&1 &
else
echo "emu: no display -- starting '$avd' headless" >&2
"$sdk/emulator/emulator" -avd "$avd" -gpu swiftshader_indirect -no-audio -no-window \
>"$log" 2>&1 &
fi
pid=$!
for _ in $(seq 150); do
if ! kill -0 "$pid" 2>/dev/null; then
echo "emu: the emulator exited before it finished booting:" >&2
tail -20 "$log" >&2
exit 1
fi
if serial=$(avd_serial "$adb" "$avd"); then
booted=$("$adb" -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')
if [ "$booted" = 1 ]; then
echo "emu: '$avd' booted ($serial)" >&2
echo "$serial"
return 0
fi
fi
sleep 2
done
echo "emu: '$avd' did not finish booting in five minutes; log at $log" >&2
exit 1
}
cmd_down() {
if ! serial=$(avd_serial "$adb" "$avd"); then
echo "emu: '$avd' is not running" >&2
return 0
fi
"$adb" -s "$serial" emu kill >/dev/null
rm -f "${XDG_RUNTIME_DIR:-/tmp}/emulator-tools/$avd.serial"
echo "emu: stopped '$avd' ($serial)" >&2
}
case "${1:-}" in
name) echo "$avd" ;;
serial) avd_serial "$adb" "$avd" || { echo "emu: '$avd' is not running" >&2; exit 1; } ;;
list) cmd_list ;;
up) cmd_up ;;
down) cmd_down ;;
*) usage ;;
esac