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>
97 lines
3.6 KiB
Bash
97 lines
3.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 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"
|
|
}
|