From 08742921721122a826bbb0761d9547fe9357a09a Mon Sep 17 00:00:00 2001 From: Builder Date: Wed, 14 Oct 2026 14:52:37 +1100 Subject: [PATCH] Tooling: emu.sh selects the emulator explicitly The Pixel 8a is attached to .27 at the same time as the headless emulator, so bare adb calls failed with "error: more than one device/emulator". Every device-specific adb call now gets an explicit serial. - Add serial selection: ANDROID_SERIAL when set, else the first emulator-* entry from "adb devices". - Pass -s "$SERIAL" to the adb calls in install, screenshot, status and stop. - wait polls for the emulator serial instead of bare "adb wait-for-device", which fails at once when the Pixel 8a is attached, then waits with -s and accepts an optional timeout. - Fail with a clear message, and not a raw adb error, when no emulator runs; screenshot also fails on an empty capture. - Interface unchanged: start|wait|install |screenshot [out]|stop|status. Proven on .27 with the Pixel 8a attached (USB and wireless): install -> Success, screenshot -> 1392860 bytes from emulator-5554. The deployed copy at ~/android/emu.sh is byte-identical to this file. Co-Authored-By: Paperclip --- android/tooling/emu.sh | 166 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100755 android/tooling/emu.sh diff --git a/android/tooling/emu.sh b/android/tooling/emu.sh new file mode 100755 index 0000000..182cd14 --- /dev/null +++ b/android/tooling/emu.sh @@ -0,0 +1,166 @@ +#!/usr/bin/env bash +# emu.sh — headless Android emulator helper on .27. +# +# The physical Pixel 8a is normally attached to .27 (USB and/or wireless) at the +# same time as the emulator. adb therefore refuses bare commands with +# "error: more than one device/emulator". Every device-specific adb call below +# is given an explicit serial. +# +# Serial selection order: +# 1. $ANDROID_SERIAL, when set (a serial or an emulator port). +# 2. The first emulator-* entry in "adb devices". +# Commands that need a device fail with a clear message when no emulator runs. +# +# Called from .13, e.g.: +# ssh 192.168.20.27 '~/android/emu.sh start' +# ssh 192.168.20.27 '~/android/emu.sh wait' +# ssh 192.168.20.27 '~/android/emu.sh install /home/sam/app-debug.apk' +# ssh 192.168.20.27 '~/android/emu.sh screenshot /tmp/shot.png' +# ssh 192.168.20.27 '~/android/emu.sh status' +# ssh 192.168.20.27 '~/android/emu.sh stop' +# +# Source of truth for this copy: sam/kotlin_phone_app_admin, android/tooling/emu.sh +set -euo pipefail + +export ANDROID_HOME="${ANDROID_HOME:-$HOME/Android/Sdk}" +export PATH="$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$ANDROID_HOME/cmdline-tools/latest/bin:$PATH" +AVD="${AVD_NAME:-pixel35}" +LOG="/tmp/emu-$AVD.log" + +# Serial of the emulator this script acts on. Prints nothing when no emulator +# entry is present. Matches any state (device, offline) so that "wait" can pick +# up an emulator that is still booting. +select_serial() { + if [ -n "${ANDROID_SERIAL:-}" ]; then + printf '%s\n' "$ANDROID_SERIAL" + return 0 + fi + adb devices 2>/dev/null | awk '$1 ~ /^emulator-/ { print $1; exit }' +} + +# State column of one serial in "adb devices": device, offline, unauthorized, +# or empty when the serial is not listed. +device_state() { + adb devices 2>/dev/null | awk -v s="$1" '$1 == s { print $2; exit }' +} + +# Echo the selected serial, or exit 1 with a clear message. Used by every +# command that must talk to a specific device. +require_serial() { + local serial state + serial="$(select_serial)" + if [ -z "$serial" ]; then + echo "ERROR: no emulator is running (no emulator-* device, and ANDROID_SERIAL is unset)." >&2 + echo " Start one with: $0 start" >&2 + echo " 'adb devices' says:" >&2 + adb devices >&2 + exit 1 + fi + state="$(device_state "$serial")" + if [ -z "$state" ]; then + echo "ERROR: serial '$serial' is not attached. 'adb devices' says:" >&2 + adb devices >&2 + exit 1 + fi + if [ "$state" != "device" ]; then + echo "ERROR: '$serial' is '$state', not ready." >&2 + echo " Wait for it with: $0 wait" >&2 + exit 1 + fi + printf '%s\n' "$serial" +} + +case "${1:-status}" in + start) + if adb devices 2>/dev/null | awk '$1 ~ /^emulator-/ { found = 1 } END { exit !found }'; then + echo "already running ($(select_serial))" + exit 0 + fi + setsid nohup "$ANDROID_HOME/emulator/emulator" -avd "$AVD" \ + -no-window -no-audio -no-snapshot -no-boot-anim -gpu swiftshader_indirect \ + > "$LOG" 2>&1 & + echo "starting $AVD (log: $LOG)" + ;; + + wait) + boot_timeout="${2:-420}" + deadline=$(( $(date +%s) + boot_timeout )) + # Poll for a serial instead of using bare "adb wait-for-device": with the + # Pixel 8a attached, bare adb fails at once with + # "error: more than one device/emulator", even before the emulator appears. + serial="$(select_serial)" + while [ -z "$serial" ]; do + if [ "$(date +%s)" -ge "$deadline" ]; then + echo "ERROR: no emulator-* device appeared within ${boot_timeout}s. 'adb devices' says:" >&2 + adb devices >&2 + echo " Start one with: $0 start" >&2 + exit 1 + fi + sleep 2 + serial="$(select_serial)" + done + remaining=$(( deadline - $(date +%s) )) + [ "$remaining" -gt 0 ] || remaining=1 + timeout "$remaining" adb -s "$serial" wait-for-device \ + || { echo "ERROR: emulator $serial did not come online within ${boot_timeout}s"; exit 1; } + n=0 + while [ "$n" -lt 72 ]; do + if [ "$(adb -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" = "1" ]; then + echo "booted ($serial)" + exit 0 + fi + n=$((n + 1)) + sleep 5 + done + echo "ERROR: boot timeout (6 minutes) waiting for $serial" + exit 1 + ;; + + install) + apk="${2:?usage: emu.sh install }" + if [ ! -f "$apk" ]; then echo "ERROR: no such apk: $apk"; exit 1; fi + serial="$(require_serial)" + adb -s "$serial" install -r "$apk" + ;; + + screenshot) + out="${2:-/tmp/emulator-screen.png}" + serial="$(require_serial)" + adb -s "$serial" exec-out screencap -p > "$out" + size="$(stat -c%s "$out")" + if [ "$size" -eq 0 ]; then + echo "ERROR: screencap from $serial produced an empty file: $out" >&2 + exit 1 + fi + echo "wrote $out ($size bytes) from $serial" + ;; + + stop) + serial="$(select_serial)" + if [ -n "$serial" ]; then + adb -s "$serial" emu kill >/dev/null 2>&1 || true + fi + sleep 2 + pkill -f qemu-system-x86_64 >/dev/null 2>&1 || true + echo "stopped" + ;; + + status) + serial="$(select_serial)" + adb devices + if [ -z "$serial" ]; then + echo "ERROR: no emulator is running (no emulator-* device, and ANDROID_SERIAL is unset)." >&2 + echo " Start one with: $0 start" >&2 + exit 1 + fi + state="$(device_state "$serial")" + printf "emulator=%s state=%s android=" "$serial" "$state" + adb -s "$serial" shell getprop ro.build.version.release 2>/dev/null | tr -d '\r' + [ "$state" = "device" ] || exit 1 + ;; + + *) + echo "usage: $0 start|wait|install |screenshot [out]|stop|status" + exit 2 + ;; +esac