Files
irisandClaude Opus 5 43f3786bed Validate the serial cache by AVD name, not by the serial still existing
Emulator serials are ports, and ports get recycled. The cache checked only
that its cached serial was still in `adb devices`, which stays true after
the AVD that was on it exits and a different one takes the port -- so the
entry never expires and every call resolves to somebody else's emulator.

Measured on 2026-09-04 with ai-app.serial and ai-app-2.serial both holding
emulator-5554: from ~/repos/ai-app-2, `emu up` reported "'ai-app-2' is
already running (emulator-5554)" and handed back a device that
`adb -s emulator-5554 emu avd name` calls ai-app. Every adb, ui-trace and
emu call from that checkout went to the other session's emulator, and the
right AVD could not be started at all because the tool believed it was up.
That is the exact fan-out this repo exists to prevent, and nothing says so.

Asking the cached serial its name is one round trip against one device, not
the per-device sweep the cache exists to avoid, and a mismatch drops the
entry so the slow path can answer. It goes in avd_serial because `adb`,
`ui-trace` and both of `emu`'s uses -- resolving a serial, and deciding
whether to start one -- already come through it; a check in only one of
them would be the same bug with a smaller blast radius.

Found from ai-app-2; ai-app's session confirmed its own work had landed on
the right device and asked for the fix here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 18:01:17 -04:00

113 lines
4.6 KiB
Bash

# 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 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"
}