# 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 "serialavd" 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 validated by asking the cached serial its AVD name, not merely # by checking that the serial is still attached. Those are different questions # once ports get recycled, and the cheap one says yes about the wrong device: # emulator serials are ports, so when ai-app-2 exits and ai-app starts, ai-app # takes 5554 and ai-app-2's stale cache entry stays "valid" for ever. Measured # on 2026-09-04, with both ai-app.serial and ai-app-2.serial holding # emulator-5554: every adb, ui-trace and emu call from ~/repos/ai-app-2 # resolved to the other checkout's emulator, and `emu up` refused to start the # right AVD because it believed it was already running. That is precisely the # fan-out this whole tool exists to prevent, and it is silent. # # The name check is one round trip on a cache hit, against the one device the # cache names -- not the per-device sweep the cache exists to avoid. It lives # here rather than in any caller because `adb`, `ui-trace` and both of `emu`'s # uses (resolving a serial, and deciding whether to start) all come through # this function, and a check in only one of them is the same bug with a # smaller blast radius. avd_serial() { _adb=$1 _avd=$2 _dir="${XDG_RUNTIME_DIR:-/tmp}/emulator-tools" _cache="$_dir/$_avd.serial" if [ -r "$_cache" ]; then _cached=$(cat "$_cache") _name=$("$_adb" -s "$_cached" emu avd name 2>/dev/null | head -n1 | tr -d '\r') if [ "$_name" = "$_avd" ]; then echo "$_cached" return 0 fi rm -f "$_cache" 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" }