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>
This commit is contained in:
irisandClaude Opus 5 committed 2026-08-30 13:06:17 -04:00
commit 5d77599f3f
8 files changed
+1187

No files matched your search

+96
View File
@@ -0,0 +1,96 @@
# Which emulator belongs to the directory you are standing in, and whether
# there is room to start it. Sourced by every command in this repo.
#
# One file rather than the same three functions copied into each script: the
# rule about *which* emulator a call means has to be the same for `adb`,
# `ui-trace` and `emu`, or the tool that reads the screen and the tool that
# taps it can end up on different devices.
# The AVD for a directory: the basename of the enclosing git checkout.
#
# Derived rather than configured, because the point is that nobody has to say
# it. Several agent sessions work here at once out of ~/repos/ai-app,
# ~/repos/ai-app-2 and ~/repos/dev-updater, and an emulator named after the
# checkout is one that cannot be somebody else's. Override with AVD_NAME for
# the case this cannot guess -- a worktree meant to share its parent's.
project_avd() {
if [ -n "${AVD_NAME:-}" ]; then
echo "$AVD_NAME"
return
fi
root=$(git rev-parse --show-toplevel 2>/dev/null || true)
basename "${root:-$PWD}"
}
# Every emulator attached right now, as "serial<TAB>avd" lines.
#
# Takes the real adb rather than finding one, because its caller is usually
# the adb wrapper and a wrapper that calls itself never returns.
running_avds() {
_adb=$1
"$_adb" devices 2>/dev/null | awk '$2 == "device" {print $1}' | while read -r serial; do
name=$("$_adb" -s "$serial" emu avd name 2>/dev/null | head -n1 | tr -d '\r')
[ -n "$name" ] && printf '%s\t%s\n' "$serial" "$name"
done
}
# The serial AVD "$2" is on, or nothing.
#
# Cached, because asking costs one console round trip per attached device and
# this runs on every single adb call -- which includes a `tap` inside a loop.
# The cache is checked against `adb devices` first, so an emulator that has
# gone away cannot leave a stale serial behind; that check is one round trip
# whatever the answer.
avd_serial() {
_adb=$1
_avd=$2
_dir="${XDG_RUNTIME_DIR:-/tmp}/emulator-tools"
_cache="$_dir/$_avd.serial"
if [ -r "$_cache" ]; then
_cached=$(cat "$_cache")
if "$_adb" devices 2>/dev/null | awk '$2 == "device" {print $1}' |
grep -qx "$_cached"; then
echo "$_cached"
return 0
fi
fi
_found=$(running_avds "$_adb" | awk -F'\t' -v want="$_avd" '$2 == want {print $1; exit}')
[ -z "$_found" ] && return 1
mkdir -p "$_dir" && printf '%s' "$_found" >"$_cache" 2>/dev/null || true
echo "$_found"
}
# What the kernel thinks is available, in MiB. MemAvailable rather than
# MemFree: free memory on this box is mostly page cache, and refusing to start
# an emulator because the cache is warm would refuse always.
mem_available_mb() {
awk '/^MemAvailable:/ {print int($2 / 1024)}' /proc/meminfo
}
# The Android SDK, without consulting PATH.
#
# PATH is how a wrapper here got called, so searching it again is how a
# wrapper calls itself for ever. The ambient $ANDROID_HOME on this machine
# still points at a root-owned /opt/android-sdk with no platform-tools,
# cmdline-tools or emulator under it, which is why the user-owned SDK is
# checked before it rather than after.
android_sdk() {
for dir in \
"$HOME/Android/Sdk" \
"${ANDROID_HOME:-}" \
"${ANDROID_SDK_ROOT:-}" \
/opt/android-sdk; do
if [ -n "$dir" ] && [ -d "$dir/platform-tools" ]; then
echo "$dir"
return 0
fi
done
return 1
}
# The real adb, as opposed to whichever wrapper is asking.
android_adb() {
sdk=$(android_sdk) || return 1
[ -x "$sdk/platform-tools/adb" ] || return 1
echo "$sdk/platform-tools/adb"
}