chore(watch): require explicit target in run.sh and prompt on ambiguity
Drop the legacy "(none)" target fallback that quietly picked the first attached device. Every device-bound subcommand now requires a `watch` or `emulator` prefix (or DOCTATE_TARGET env var). The emulator path never auto-picks a running emulator: even with exactly one running, the menu shows so the user can boot a different AVD instead. ADB_SERIAL / WEAR_AVD still bypass the menu for scripted runs. Menu results now travel through a global RESOLVED_SERIAL/PICKED instead of stdout+$(), so `die` inside the resolvers aborts the main shell directly rather than getting swallowed by a command-substitution subshell.
This commit is contained in:
+206
-59
@@ -2,20 +2,25 @@
|
||||
# Build / install / run helper for the Wear OS app.
|
||||
#
|
||||
# Usage:
|
||||
# ./run.sh [target] <subcommand> [args]
|
||||
# ./run.sh <target> <subcommand> [args]
|
||||
#
|
||||
# Optional target prefix (selects which device + which build config):
|
||||
# A target prefix is REQUIRED for every device-bound subcommand. The script
|
||||
# never silently picks "the first attached device" — if more than one candidate
|
||||
# exists it asks, and if ADB_SERIAL/WEAR_AVD pins a choice it uses that.
|
||||
#
|
||||
# Target prefix (or DOCTATE_TARGET env var):
|
||||
# watch Real Pixel Watch over ADB-WiFi. Uses the cached serial
|
||||
# from .watch_serial and points the build at the dev
|
||||
# laptop's LAN URL ($DOCTATE_DEV_SERVER).
|
||||
# emulator Wear OS AVD on this host. Uses the first attached
|
||||
# emulator-* serial; build keeps its emulator default
|
||||
# (10.0.2.2:3000).
|
||||
# (none) Backwards-compatible: no serial pinning, no URL override.
|
||||
# ADB / Gradle pick the first attached device — fine when
|
||||
# only one is connected.
|
||||
# from .watch_serial when it's attached; otherwise menu.
|
||||
# Points the build at the dev laptop's LAN URL
|
||||
# ($DOCTATE_DEV_SERVER).
|
||||
# emulator Wear OS AVD on this host.
|
||||
# - 0 emulators running → menu of AVDs from `emulator -list-avds`
|
||||
# - ≥1 running → menu of running serials + "Start a new AVD"
|
||||
# The script never auto-picks a running emulator. To bypass the
|
||||
# menu, pin the choice via ADB_SERIAL=<serial> or WEAR_AVD=<name>.
|
||||
# Build keeps its emulator default (10.0.2.2:3000).
|
||||
#
|
||||
# Subcommands:
|
||||
# Subcommands that need a target:
|
||||
# (none) install + start (default inner loop)
|
||||
# build ./gradlew :app:assembleDebug
|
||||
# install ./gradlew :app:installDebug
|
||||
@@ -23,29 +28,31 @@
|
||||
# stop adb shell am force-stop ...
|
||||
# logcat adb logcat, filtered to the app's PID
|
||||
# shot [path] screencap to PNG (default /tmp/wear_screen.png)
|
||||
# devices list attached devices and which one this script would use
|
||||
# test run JVM unit tests, plus instrumented tests on the target
|
||||
# (set SKIP_INSTRUMENTED=1 to skip the on-device tier)
|
||||
#
|
||||
# Target-free subcommands (run without a prefix):
|
||||
# devices list attached devices
|
||||
# connect [host:port]
|
||||
# adb connect to a Pixel Watch over WiFi and remember
|
||||
# the serial in .watch_serial. Without an argument:
|
||||
# reuse the cached serial.
|
||||
# test run JVM unit tests, plus instrumented tests on emulator
|
||||
# (set SKIP_INSTRUMENTED=1 to skip the on-device tier)
|
||||
# clean ./gradlew clean
|
||||
# help show this help
|
||||
#
|
||||
# Env vars:
|
||||
# DOCTATE_TARGET Same as the [target] prefix above (watch|emulator).
|
||||
# DOCTATE_TARGET Same as the <target> prefix (watch|emulator).
|
||||
# The CLI prefix takes precedence when both are set.
|
||||
# ADB_SERIAL Pin a specific device serial. Bypasses target logic.
|
||||
# ADB_SERIAL Pin a specific device serial. Skips the menu entirely.
|
||||
# DOCTATE_DEV_SERVER
|
||||
# LAN URL of the Axum dev server, used by 'watch' target.
|
||||
# Default: http://192.168.178.27:3000
|
||||
# DOCTATE_API_KEY If set, passed as -Pdoctate.apiKey=... to Gradle and
|
||||
# baked into BuildConfig.API_KEY at compile time.
|
||||
# Otherwise the value from local.properties is used.
|
||||
# WEAR_AVD AVD name to auto-boot when no device is attached
|
||||
# (skips the interactive menu; useful for CI / scripted runs)
|
||||
# SKIP_INSTRUMENTED set to 1 to limit `test` to JVM unit tests (no emulator needed)
|
||||
# WEAR_AVD AVD name to auto-boot when no emulator is attached
|
||||
# (skips the AVD menu; useful for CI / scripted runs).
|
||||
# SKIP_INSTRUMENTED set to 1 to limit `test` to JVM unit tests (no device needed).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -109,30 +116,173 @@ read_cached_watch_serial() {
|
||||
fi
|
||||
}
|
||||
|
||||
first_emulator_serial() {
|
||||
detect_adb
|
||||
"$ADB" devices | awk 'NR>1 && $2=="device" && $1 ~ /^emulator-/ {print $1; exit}'
|
||||
# Interactive picker. Writes the chosen item to the global PICKED so the
|
||||
# caller doesn't need $(...) (which would swallow `die` inside a subshell).
|
||||
# Aborts (via die) on bad input or when stdin is not a TTY.
|
||||
# Usage: pick_from_list "Prompt" "label1" "label2" ...; use "$PICKED"
|
||||
PICKED=""
|
||||
pick_from_list() {
|
||||
local prompt="$1"; shift
|
||||
local -a items=("$@")
|
||||
if [[ ! -t 0 ]]; then
|
||||
die "$prompt need a TTY to prompt; pin via ADB_SERIAL / WEAR_AVD instead. Items: ${items[*]}"
|
||||
fi
|
||||
info "$prompt"
|
||||
local i
|
||||
for i in "${!items[@]}"; do
|
||||
printf " %d) %s\n" "$((i+1))" "${items[$i]}"
|
||||
done
|
||||
printf " 0) abort\n"
|
||||
local choice
|
||||
read -r -p "Choose: " choice
|
||||
if [[ ! "$choice" =~ ^[0-9]+$ ]]; then
|
||||
die "Invalid choice: '$choice' (expected a number)."
|
||||
fi
|
||||
if [[ "$choice" -eq 0 ]]; then
|
||||
die "Aborted by user."
|
||||
fi
|
||||
if [[ "$choice" -lt 1 || "$choice" -gt "${#items[@]}" ]]; then
|
||||
die "Out of range: $choice (have ${#items[@]} items)."
|
||||
fi
|
||||
PICKED="${items[$((choice-1))]}"
|
||||
}
|
||||
|
||||
# Picks an attached watch by Wear OS device code. The Pixel Watch 2's
|
||||
# device code is "eos"; the Pixel Watch 1 is "rohan". This is a fallback
|
||||
# for first-time users who haven't run ./run.sh connect yet.
|
||||
auto_detect_watch_serial() {
|
||||
# Lists all running emulator serials (one per line).
|
||||
list_emulator_serials() {
|
||||
detect_adb
|
||||
"$ADB" devices -l | awk '$2=="device" && /device:(eos|rohan)/ {print $1; exit}'
|
||||
"$ADB" devices | awk 'NR>1 && $2=="device" && $1 ~ /^emulator-/ {print $1}'
|
||||
}
|
||||
|
||||
# Lists attached Wear OS watch serials (one per line). Pixel Watch 2
|
||||
# reports device code "eos", Pixel Watch 1 is "rohan".
|
||||
list_watch_serials() {
|
||||
detect_adb
|
||||
"$ADB" devices -l | awk '$2=="device" && /device:(eos|rohan)/ {print $1}'
|
||||
}
|
||||
|
||||
# Human-readable label for a serial, built from `adb devices -l` metadata.
|
||||
# Falls back to the bare serial if no extra info is available.
|
||||
serial_label() {
|
||||
local serial="$1"
|
||||
local line
|
||||
line=$("$ADB" devices -l | awk -v s="$serial" '$1==s {$1=""; sub(/^ +/,""); print}')
|
||||
if [[ -n "$line" ]]; then
|
||||
printf '%s — %s\n' "$serial" "$line"
|
||||
else
|
||||
printf '%s\n' "$serial"
|
||||
fi
|
||||
}
|
||||
|
||||
# Output variable for the resolve_*_serial functions. Using a global
|
||||
# instead of stdout+$() lets `die` inside these functions abort the main
|
||||
# shell directly — $() would wrap everything in a subshell and swallow
|
||||
# the exit, which is how the earlier version silently leaked past errors.
|
||||
RESOLVED_SERIAL=""
|
||||
|
||||
# Resolves the emulator serial the user wants to target. The script never
|
||||
# auto-picks a running emulator — even when only one is up, the user may
|
||||
# want to boot a different AVD instead. Bypass the menu by setting
|
||||
# ADB_SERIAL=<serial> or WEAR_AVD=<name>.
|
||||
# - 0 emulators running → boot one (WEAR_AVD env or AVD menu); take its serial.
|
||||
# - ≥1 emulators running → always prompt; "Start a new AVD" is the last option.
|
||||
resolve_emulator_serial() {
|
||||
detect_adb
|
||||
local -a running
|
||||
mapfile -t running < <(list_emulator_serials)
|
||||
|
||||
if [[ "${#running[@]}" -eq 0 ]]; then
|
||||
boot_emulator_or_die
|
||||
mapfile -t running < <(list_emulator_serials)
|
||||
if [[ "${#running[@]}" -eq 0 ]]; then
|
||||
die "Boot reported success but no emulator serial visible."
|
||||
fi
|
||||
RESOLVED_SERIAL="${running[0]}"
|
||||
return
|
||||
fi
|
||||
|
||||
local -a labels=()
|
||||
local s
|
||||
for s in "${running[@]}"; do
|
||||
labels+=("$(serial_label "$s")")
|
||||
done
|
||||
local start_new_label="Start a new AVD"
|
||||
labels+=("$start_new_label")
|
||||
|
||||
pick_from_list "Pick emulator:" "${labels[@]}"
|
||||
local choice="$PICKED"
|
||||
|
||||
if [[ "$choice" == "$start_new_label" ]]; then
|
||||
local -a before=("${running[@]}")
|
||||
boot_emulator_or_die
|
||||
local -a after
|
||||
mapfile -t after < <(list_emulator_serials)
|
||||
local new=""
|
||||
local a p found
|
||||
for a in "${after[@]}"; do
|
||||
found=0
|
||||
for p in "${before[@]}"; do
|
||||
[[ "$p" == "$a" ]] && { found=1; break; }
|
||||
done
|
||||
if [[ "$found" -eq 0 ]]; then
|
||||
new="$a"
|
||||
break
|
||||
fi
|
||||
done
|
||||
[[ -z "$new" ]] && die "Could not identify newly booted emulator serial."
|
||||
RESOLVED_SERIAL="$new"
|
||||
return
|
||||
fi
|
||||
|
||||
# Chosen label has the form "emulator-5554 — <metadata>"; strip to serial.
|
||||
RESOLVED_SERIAL="${choice%% *}"
|
||||
}
|
||||
|
||||
# Resolves the watch serial to target. Prefers the cached pairing when
|
||||
# it's actually attached, falls back to a menu on ambiguity.
|
||||
resolve_watch_serial() {
|
||||
detect_adb
|
||||
local -a attached
|
||||
mapfile -t attached < <(list_watch_serials)
|
||||
local cached
|
||||
cached="$(read_cached_watch_serial)"
|
||||
|
||||
if [[ -n "$cached" ]]; then
|
||||
local s
|
||||
for s in "${attached[@]}"; do
|
||||
if [[ "$s" == "$cached" ]]; then
|
||||
RESOLVED_SERIAL="$cached"
|
||||
return
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ "${#attached[@]}" -eq 0 ]]; then
|
||||
if [[ -n "$cached" ]]; then
|
||||
die "Cached watch serial '$cached' is not attached. Try './run.sh connect $cached' or './run.sh connect <ip:port>' for a fresh pairing."
|
||||
fi
|
||||
die "No watch attached. Run './run.sh connect <ip:port>' first."
|
||||
fi
|
||||
|
||||
if [[ "${#attached[@]}" -eq 1 ]]; then
|
||||
RESOLVED_SERIAL="${attached[0]}"
|
||||
return
|
||||
fi
|
||||
|
||||
local -a labels=()
|
||||
local s
|
||||
for s in "${attached[@]}"; do
|
||||
labels+=("$(serial_label "$s")")
|
||||
done
|
||||
pick_from_list "Multiple watches attached — pick one:" "${labels[@]}"
|
||||
RESOLVED_SERIAL="${PICKED%% *}"
|
||||
}
|
||||
|
||||
apply_target() {
|
||||
case "$DOCTATE_TARGET" in
|
||||
watch)
|
||||
if [[ -z "$ADB_SERIAL" ]]; then
|
||||
ADB_SERIAL="$(read_cached_watch_serial)"
|
||||
if [[ -z "$ADB_SERIAL" ]]; then
|
||||
ADB_SERIAL="$(auto_detect_watch_serial || true)"
|
||||
fi
|
||||
if [[ -z "$ADB_SERIAL" ]]; then
|
||||
die "No watch serial known. Run './run.sh connect <ip:port>' first."
|
||||
fi
|
||||
resolve_watch_serial
|
||||
ADB_SERIAL="$RESOLVED_SERIAL"
|
||||
fi
|
||||
local server="${DOCTATE_DEV_SERVER:-$DEFAULT_DEV_SERVER}"
|
||||
GRADLE_PROPS+=("-Pdoctate.serverUrl=$server")
|
||||
@@ -140,15 +290,13 @@ apply_target() {
|
||||
;;
|
||||
emulator)
|
||||
if [[ -z "$ADB_SERIAL" ]]; then
|
||||
ADB_SERIAL="$(first_emulator_serial)"
|
||||
if [[ -z "$ADB_SERIAL" ]]; then
|
||||
die "No emulator attached. Start one first."
|
||||
fi
|
||||
resolve_emulator_serial
|
||||
ADB_SERIAL="$RESOLVED_SERIAL"
|
||||
fi
|
||||
info "Target: emulator (serial=$ADB_SERIAL, server defaults to 10.0.2.2:3000)"
|
||||
;;
|
||||
"")
|
||||
: # no override; ADB / Gradle pick the first device
|
||||
die "No target selected. Use './run.sh watch …' or './run.sh emulator …' (or set DOCTATE_TARGET)."
|
||||
;;
|
||||
*)
|
||||
die "Unknown target: '$DOCTATE_TARGET' (expected: watch | emulator)"
|
||||
@@ -165,23 +313,13 @@ apply_target() {
|
||||
|
||||
ensure_device() {
|
||||
detect_adb
|
||||
if [[ -n "$ADB_SERIAL" ]]; then
|
||||
# Pinned device must actually be attached.
|
||||
if ! "$ADB" devices | awk 'NR>1 {print $1}' | grep -qx "$ADB_SERIAL"; then
|
||||
die "Pinned device '$ADB_SERIAL' is not attached. Check './run.sh devices'."
|
||||
fi
|
||||
return
|
||||
# apply_target must have resolved a serial before any device-bound command runs.
|
||||
if [[ -z "$ADB_SERIAL" ]]; then
|
||||
die "Internal error: ADB_SERIAL not set. This is a bug in run.sh dispatch."
|
||||
fi
|
||||
local count
|
||||
count=$("$ADB" devices | awk 'NR>1 && $2=="device"' | wc -l)
|
||||
if [[ "$count" -gt 0 ]]; then
|
||||
if [[ "$count" -gt 1 ]]; then
|
||||
info "Multiple devices attached; ADB will pick the first."
|
||||
info "Tip: use './run.sh watch ...' or './run.sh emulator ...' to disambiguate."
|
||||
fi
|
||||
return
|
||||
if ! "$ADB" devices | awk 'NR>1 && $2=="device" {print $1}' | grep -qx "$ADB_SERIAL"; then
|
||||
die "Pinned device '$ADB_SERIAL' is no longer attached. Check './run.sh devices'."
|
||||
fi
|
||||
boot_emulator_or_die
|
||||
}
|
||||
|
||||
boot_emulator_or_die() {
|
||||
@@ -375,12 +513,25 @@ cmd_help() {
|
||||
}
|
||||
|
||||
# --- Dispatch --------------------------------------------------------------
|
||||
# Optional target prefix (watch | emulator) before the subcommand.
|
||||
# Required target prefix (watch | emulator) before the subcommand. The legacy
|
||||
# "(none)" fallback is gone: every device-bound subcommand must know which
|
||||
# device it's talking to, otherwise we'd quietly pick "the first" — which is
|
||||
# exactly what the user asked to stop doing.
|
||||
if [[ "${1:-}" == "watch" || "${1:-}" == "emulator" ]]; then
|
||||
DOCTATE_TARGET="$1"
|
||||
shift
|
||||
fi
|
||||
|
||||
# Subcommands that are device- and target-agnostic bypass apply_target so
|
||||
# they don't trigger a resolver menu as a side-effect. Ordering matters:
|
||||
# this short-circuit must run before apply_target.
|
||||
case "${1:-}" in
|
||||
help|-h|--help) cmd_help; exit 0 ;;
|
||||
devices) cmd_devices; exit 0 ;;
|
||||
connect) shift; cmd_connect "$@"; exit 0 ;;
|
||||
clean) cmd_clean; exit 0 ;;
|
||||
esac
|
||||
|
||||
apply_target
|
||||
|
||||
case "${1:-}" in
|
||||
@@ -391,10 +542,6 @@ case "${1:-}" in
|
||||
stop) cmd_stop ;;
|
||||
logcat) cmd_logcat ;;
|
||||
shot) shift; cmd_shot "$@" ;;
|
||||
devices) cmd_devices ;;
|
||||
connect) shift; cmd_connect "$@" ;;
|
||||
test) cmd_test ;;
|
||||
clean) cmd_clean ;;
|
||||
-h|--help|help) cmd_help ;;
|
||||
*) die "Unknown command: $1 (try './run.sh help')" ;;
|
||||
esac
|
||||
|
||||
Reference in New Issue
Block a user