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.
|
# Build / install / run helper for the Wear OS app.
|
||||||
#
|
#
|
||||||
# Usage:
|
# 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
|
# watch Real Pixel Watch over ADB-WiFi. Uses the cached serial
|
||||||
# from .watch_serial and points the build at the dev
|
# from .watch_serial when it's attached; otherwise menu.
|
||||||
# laptop's LAN URL ($DOCTATE_DEV_SERVER).
|
# Points the build at the dev laptop's LAN URL
|
||||||
# emulator Wear OS AVD on this host. Uses the first attached
|
# ($DOCTATE_DEV_SERVER).
|
||||||
# emulator-* serial; build keeps its emulator default
|
# emulator Wear OS AVD on this host.
|
||||||
# (10.0.2.2:3000).
|
# - 0 emulators running → menu of AVDs from `emulator -list-avds`
|
||||||
# (none) Backwards-compatible: no serial pinning, no URL override.
|
# - ≥1 running → menu of running serials + "Start a new AVD"
|
||||||
# ADB / Gradle pick the first attached device — fine when
|
# The script never auto-picks a running emulator. To bypass the
|
||||||
# only one is connected.
|
# 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)
|
# (none) install + start (default inner loop)
|
||||||
# build ./gradlew :app:assembleDebug
|
# build ./gradlew :app:assembleDebug
|
||||||
# install ./gradlew :app:installDebug
|
# install ./gradlew :app:installDebug
|
||||||
@@ -23,29 +28,31 @@
|
|||||||
# stop adb shell am force-stop ...
|
# stop adb shell am force-stop ...
|
||||||
# logcat adb logcat, filtered to the app's PID
|
# logcat adb logcat, filtered to the app's PID
|
||||||
# shot [path] screencap to PNG (default /tmp/wear_screen.png)
|
# 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]
|
# connect [host:port]
|
||||||
# adb connect to a Pixel Watch over WiFi and remember
|
# adb connect to a Pixel Watch over WiFi and remember
|
||||||
# the serial in .watch_serial. Without an argument:
|
# the serial in .watch_serial. Without an argument:
|
||||||
# reuse the cached serial.
|
# 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
|
# clean ./gradlew clean
|
||||||
# help show this help
|
# help show this help
|
||||||
#
|
#
|
||||||
# Env vars:
|
# 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.
|
# 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
|
# DOCTATE_DEV_SERVER
|
||||||
# LAN URL of the Axum dev server, used by 'watch' target.
|
# LAN URL of the Axum dev server, used by 'watch' target.
|
||||||
# Default: http://192.168.178.27:3000
|
# Default: http://192.168.178.27:3000
|
||||||
# DOCTATE_API_KEY If set, passed as -Pdoctate.apiKey=... to Gradle and
|
# DOCTATE_API_KEY If set, passed as -Pdoctate.apiKey=... to Gradle and
|
||||||
# baked into BuildConfig.API_KEY at compile time.
|
# baked into BuildConfig.API_KEY at compile time.
|
||||||
# Otherwise the value from local.properties is used.
|
# Otherwise the value from local.properties is used.
|
||||||
# WEAR_AVD AVD name to auto-boot when no device is attached
|
# WEAR_AVD AVD name to auto-boot when no emulator is attached
|
||||||
# (skips the interactive menu; useful for CI / scripted runs)
|
# (skips the AVD menu; useful for CI / scripted runs).
|
||||||
# SKIP_INSTRUMENTED set to 1 to limit `test` to JVM unit tests (no emulator needed)
|
# SKIP_INSTRUMENTED set to 1 to limit `test` to JVM unit tests (no device needed).
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -109,30 +116,173 @@ read_cached_watch_serial() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
first_emulator_serial() {
|
# Interactive picker. Writes the chosen item to the global PICKED so the
|
||||||
detect_adb
|
# caller doesn't need $(...) (which would swallow `die` inside a subshell).
|
||||||
"$ADB" devices | awk 'NR>1 && $2=="device" && $1 ~ /^emulator-/ {print $1; exit}'
|
# 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
|
# Lists all running emulator serials (one per line).
|
||||||
# device code is "eos"; the Pixel Watch 1 is "rohan". This is a fallback
|
list_emulator_serials() {
|
||||||
# for first-time users who haven't run ./run.sh connect yet.
|
|
||||||
auto_detect_watch_serial() {
|
|
||||||
detect_adb
|
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() {
|
apply_target() {
|
||||||
case "$DOCTATE_TARGET" in
|
case "$DOCTATE_TARGET" in
|
||||||
watch)
|
watch)
|
||||||
if [[ -z "$ADB_SERIAL" ]]; then
|
if [[ -z "$ADB_SERIAL" ]]; then
|
||||||
ADB_SERIAL="$(read_cached_watch_serial)"
|
resolve_watch_serial
|
||||||
if [[ -z "$ADB_SERIAL" ]]; then
|
ADB_SERIAL="$RESOLVED_SERIAL"
|
||||||
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
|
|
||||||
fi
|
fi
|
||||||
local server="${DOCTATE_DEV_SERVER:-$DEFAULT_DEV_SERVER}"
|
local server="${DOCTATE_DEV_SERVER:-$DEFAULT_DEV_SERVER}"
|
||||||
GRADLE_PROPS+=("-Pdoctate.serverUrl=$server")
|
GRADLE_PROPS+=("-Pdoctate.serverUrl=$server")
|
||||||
@@ -140,15 +290,13 @@ apply_target() {
|
|||||||
;;
|
;;
|
||||||
emulator)
|
emulator)
|
||||||
if [[ -z "$ADB_SERIAL" ]]; then
|
if [[ -z "$ADB_SERIAL" ]]; then
|
||||||
ADB_SERIAL="$(first_emulator_serial)"
|
resolve_emulator_serial
|
||||||
if [[ -z "$ADB_SERIAL" ]]; then
|
ADB_SERIAL="$RESOLVED_SERIAL"
|
||||||
die "No emulator attached. Start one first."
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
info "Target: emulator (serial=$ADB_SERIAL, server defaults to 10.0.2.2:3000)"
|
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)"
|
die "Unknown target: '$DOCTATE_TARGET' (expected: watch | emulator)"
|
||||||
@@ -165,23 +313,13 @@ apply_target() {
|
|||||||
|
|
||||||
ensure_device() {
|
ensure_device() {
|
||||||
detect_adb
|
detect_adb
|
||||||
if [[ -n "$ADB_SERIAL" ]]; then
|
# apply_target must have resolved a serial before any device-bound command runs.
|
||||||
# Pinned device must actually be attached.
|
if [[ -z "$ADB_SERIAL" ]]; then
|
||||||
if ! "$ADB" devices | awk 'NR>1 {print $1}' | grep -qx "$ADB_SERIAL"; then
|
die "Internal error: ADB_SERIAL not set. This is a bug in run.sh dispatch."
|
||||||
die "Pinned device '$ADB_SERIAL' is not attached. Check './run.sh devices'."
|
|
||||||
fi
|
|
||||||
return
|
|
||||||
fi
|
fi
|
||||||
local count
|
if ! "$ADB" devices | awk 'NR>1 && $2=="device" {print $1}' | grep -qx "$ADB_SERIAL"; then
|
||||||
count=$("$ADB" devices | awk 'NR>1 && $2=="device"' | wc -l)
|
die "Pinned device '$ADB_SERIAL' is no longer attached. Check './run.sh devices'."
|
||||||
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
|
|
||||||
fi
|
fi
|
||||||
boot_emulator_or_die
|
|
||||||
}
|
}
|
||||||
|
|
||||||
boot_emulator_or_die() {
|
boot_emulator_or_die() {
|
||||||
@@ -375,12 +513,25 @@ cmd_help() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Dispatch --------------------------------------------------------------
|
# --- 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
|
if [[ "${1:-}" == "watch" || "${1:-}" == "emulator" ]]; then
|
||||||
DOCTATE_TARGET="$1"
|
DOCTATE_TARGET="$1"
|
||||||
shift
|
shift
|
||||||
fi
|
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
|
apply_target
|
||||||
|
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
@@ -391,10 +542,6 @@ case "${1:-}" in
|
|||||||
stop) cmd_stop ;;
|
stop) cmd_stop ;;
|
||||||
logcat) cmd_logcat ;;
|
logcat) cmd_logcat ;;
|
||||||
shot) shift; cmd_shot "$@" ;;
|
shot) shift; cmd_shot "$@" ;;
|
||||||
devices) cmd_devices ;;
|
|
||||||
connect) shift; cmd_connect "$@" ;;
|
|
||||||
test) cmd_test ;;
|
test) cmd_test ;;
|
||||||
clean) cmd_clean ;;
|
|
||||||
-h|--help|help) cmd_help ;;
|
|
||||||
*) die "Unknown command: $1 (try './run.sh help')" ;;
|
*) die "Unknown command: $1 (try './run.sh help')" ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
Reference in New Issue
Block a user