|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +# ───────────────────────────────────────────── |
| 5 | +# 📟 ADB + Expo Device Manager (WSL Safe) |
| 6 | +# by digitalnomad91 |
| 7 | +# ───────────────────────────────────────────── |
| 8 | + |
| 9 | +# 🖍️ Color codes |
| 10 | +RED='\033[0;31m' |
| 11 | +GREEN='\033[0;32m' |
| 12 | +BLUE='\033[1;34m' |
| 13 | +CYAN='\033[1;36m' |
| 14 | +YELLOW='\033[1;33m' |
| 15 | +BOLD='\033[1m' |
| 16 | +RESET='\033[0m' |
| 17 | + |
| 18 | +divider() { echo -e "${BLUE}────────────────────────────────────────────${RESET}"; } |
| 19 | +log() { echo -e "${CYAN}ℹ️ $1${RESET}"; } |
| 20 | +warn() { echo -e "${YELLOW}⚠️ $1${RESET}"; } |
| 21 | +success() { echo -e "${GREEN}✅ $1${RESET}"; } |
| 22 | +error() { echo -e "${RED}❌ $1${RESET}" >&2; } |
| 23 | + |
| 24 | +ADB_BIN="${ADB_BIN:-adb}" |
| 25 | +EMULATOR_BIN="${EMULATOR_BIN:-emulator}" |
| 26 | + |
| 27 | +require_cmd() { |
| 28 | + command -v "$1" >/dev/null 2>&1 || { |
| 29 | + error "Missing required command: $1" |
| 30 | + exit 1 |
| 31 | + } |
| 32 | +} |
| 33 | + |
| 34 | +require_cmd "$ADB_BIN" |
| 35 | + |
| 36 | +list_all_devices() { |
| 37 | + "$ADB_BIN" devices -l | tr -d '\r' | awk 'NR>1 && NF' |
| 38 | +} |
| 39 | + |
| 40 | +normalize_state() { |
| 41 | + echo -n "$1" | tr -d '\r\n' |
| 42 | +} |
| 43 | + |
| 44 | +device_state_for_serial() { |
| 45 | + local serial="$1" |
| 46 | + "$ADB_BIN" devices | awk -v s="$serial" '$1==s {print $2}' | tr -d '\r\n' |
| 47 | +} |
| 48 | + |
| 49 | +get_avd_name_for_emulator_serial() { |
| 50 | + local serial="$1" |
| 51 | + # Output is typically: |
| 52 | + # <AVD_NAME> |
| 53 | + # OK |
| 54 | + # We only want the first non-empty line. |
| 55 | + "$ADB_BIN" -s "$serial" emu avd name 2>/dev/null | tr -d '\r' | awk 'NF{print; exit}' |
| 56 | +} |
| 57 | + |
| 58 | +list_avds() { |
| 59 | + if command -v "$EMULATOR_BIN" >/dev/null 2>&1; then |
| 60 | + "$EMULATOR_BIN" -list-avds 2>/dev/null || true |
| 61 | + fi |
| 62 | +} |
| 63 | + |
| 64 | +is_expo_device_supported() { |
| 65 | + npx expo run:android --help 2>/dev/null | grep -q -- '--device' |
| 66 | +} |
| 67 | + |
| 68 | +divider |
| 69 | +echo -e "${BOLD}📟 ADB + Expo Device Manager (WSL Safe)${RESET}" |
| 70 | +divider |
| 71 | + |
| 72 | +# 🔌 Start ADB server if not running |
| 73 | +log "Checking ADB server..." |
| 74 | +if "$ADB_BIN" start-server >/dev/null 2>&1; then |
| 75 | + success "ADB server ready." |
| 76 | +else |
| 77 | + warn "ADB server check failed; attempting to continue." |
| 78 | +fi |
| 79 | + |
| 80 | +divider |
| 81 | +log "Detecting connected devices..." |
| 82 | + |
| 83 | +ALL_SERIALS=() |
| 84 | +ALL_NAMES=() |
| 85 | +ALL_STATES=() |
| 86 | +ALL_DEVICES_RAW=() |
| 87 | + |
| 88 | +for _ in {1..5}; do |
| 89 | + sleep 0.5 |
| 90 | + mapfile -t ALL_DEVICES_RAW < <(list_all_devices) |
| 91 | + (( ${#ALL_DEVICES_RAW[@]} > 0 )) && break |
| 92 | +done |
| 93 | + |
| 94 | +for line in "${ALL_DEVICES_RAW[@]}"; do |
| 95 | + serial=$(echo "$line" | awk '{print $1}') |
| 96 | + state=$(echo "$line" | awk '{print $2}') |
| 97 | + description=$(echo "$line" | cut -d' ' -f3-) |
| 98 | + |
| 99 | + # skip any unexpected blank lines |
| 100 | + [[ -z "${serial:-}" ]] && continue |
| 101 | + |
| 102 | + name="$serial" |
| 103 | + if [[ "$serial" =~ ^emulator- ]]; then |
| 104 | + avd_name="$(get_avd_name_for_emulator_serial "$serial" || true)" |
| 105 | + [[ -n "${avd_name:-}" ]] && name="$avd_name" |
| 106 | + else |
| 107 | + model=$(echo "$description" | sed -n 's/.*model:\([^ ]*\).*/\1/p') |
| 108 | + [[ -n "$model" ]] && name="$model" |
| 109 | + fi |
| 110 | + |
| 111 | + ALL_SERIALS+=("$serial") |
| 112 | + ALL_NAMES+=("$name") |
| 113 | + ALL_STATES+=("$state") |
| 114 | +done |
| 115 | + |
| 116 | +# Expo CLI currently probes all emulator serials (even offline) via `adb -s emulator-XXXX emu avd name`. |
| 117 | +# If an emulator is stuck in `offline`, that probe fails and aborts the run. |
| 118 | +# Workaround: best-effort kill offline emulator entries so they don't break device discovery. |
| 119 | +OFFLINE_EMULATORS=() |
| 120 | +for i in "${!ALL_SERIALS[@]}"; do |
| 121 | + serial="${ALL_SERIALS[$i]}" |
| 122 | + state="${ALL_STATES[$i]:-}" |
| 123 | + if [[ "$serial" =~ ^emulator- ]] && [[ "$state" == "offline" ]]; then |
| 124 | + OFFLINE_EMULATORS+=("$serial") |
| 125 | + fi |
| 126 | +done |
| 127 | + |
| 128 | +if (( ${#OFFLINE_EMULATORS[@]} > 0 )); then |
| 129 | + warn "Found offline emulator(s) that can break Expo: ${OFFLINE_EMULATORS[*]}" |
| 130 | + warn "Attempting to remove them from adb (best effort)..." |
| 131 | + for serial in "${OFFLINE_EMULATORS[@]}"; do |
| 132 | + ("$ADB_BIN" -s "$serial" emu kill >/dev/null 2>&1 || true) |
| 133 | + done |
| 134 | + |
| 135 | + warn "Restarting adb server to clear stale devices..." |
| 136 | + ("$ADB_BIN" kill-server >/dev/null 2>&1 || true) |
| 137 | + ("$ADB_BIN" start-server >/dev/null 2>&1 || true) |
| 138 | + |
| 139 | + # Re-enumerate devices after cleanup so later logic doesn't see the stale offline entries. |
| 140 | + ALL_SERIALS=() |
| 141 | + ALL_NAMES=() |
| 142 | + ALL_STATES=() |
| 143 | + ALL_DEVICES_RAW=() |
| 144 | + |
| 145 | + for _ in {1..5}; do |
| 146 | + sleep 0.5 |
| 147 | + mapfile -t ALL_DEVICES_RAW < <(list_all_devices) |
| 148 | + (( ${#ALL_DEVICES_RAW[@]} > 0 )) && break |
| 149 | + done |
| 150 | + |
| 151 | + for line in "${ALL_DEVICES_RAW[@]}"; do |
| 152 | + serial=$(echo "$line" | awk '{print $1}') |
| 153 | + state=$(echo "$line" | awk '{print $2}') |
| 154 | + description=$(echo "$line" | cut -d' ' -f3-) |
| 155 | + |
| 156 | + [[ -z "${serial:-}" ]] && continue |
| 157 | + |
| 158 | + name="$serial" |
| 159 | + if [[ "$serial" =~ ^emulator- ]]; then |
| 160 | + avd_name="$(get_avd_name_for_emulator_serial "$serial" || true)" |
| 161 | + [[ -n "${avd_name:-}" ]] && name="$avd_name" |
| 162 | + else |
| 163 | + model=$(echo "$description" | sed -n 's/.*model:\([^ ]*\).*/\1/p') |
| 164 | + [[ -n "$model" ]] && name="$model" |
| 165 | + fi |
| 166 | + |
| 167 | + ALL_SERIALS+=("$serial") |
| 168 | + ALL_NAMES+=("$name") |
| 169 | + ALL_STATES+=("$state") |
| 170 | + done |
| 171 | +fi |
| 172 | + |
| 173 | +divider |
| 174 | +log "Devices detected (any state):" |
| 175 | +if (( ${#ALL_SERIALS[@]} > 0 )); then |
| 176 | + for i in "${!ALL_SERIALS[@]}"; do |
| 177 | + serial="${ALL_SERIALS[$i]}" |
| 178 | + state="${ALL_STATES[$i]:-unknown}" |
| 179 | + name="${ALL_NAMES[$i]:-$serial}" |
| 180 | + prefix="📱" |
| 181 | + [[ "$serial" =~ ^emulator- ]] && prefix="🖥️ " |
| 182 | + if [[ "$state" == "device" ]]; then |
| 183 | + echo -e " $prefix ${GREEN}${serial}${RESET} (${BOLD}${name}${RESET}, ${state})" |
| 184 | + else |
| 185 | + echo -e " $prefix ${YELLOW}${serial}${RESET} (${BOLD}${name}${RESET}, ${state})" |
| 186 | + fi |
| 187 | + done |
| 188 | +else |
| 189 | + warn "No devices detected." |
| 190 | +fi |
| 191 | + |
| 192 | +# Optional: start an AVD (always offer) |
| 193 | +AVD_LIST=() |
| 194 | +mapfile -t AVD_LIST < <(list_avds) |
| 195 | + |
| 196 | +if (( ${#ALL_SERIALS[@]} == 0 )) && (( ${#AVD_LIST[@]} == 0 )); then |
| 197 | + error "No devices/emulators found." |
| 198 | + exit 1 |
| 199 | +fi |
| 200 | + |
| 201 | +divider |
| 202 | +log "Select a device to launch Expo:" |
| 203 | +for i in "${!ALL_SERIALS[@]}"; do |
| 204 | + serial="${ALL_SERIALS[$i]}" |
| 205 | + name="${ALL_NAMES[$i]}" |
| 206 | + st="${ALL_STATES[$i]:-unknown}" |
| 207 | + prefix="📱" |
| 208 | + [[ "$serial" =~ ^emulator- ]] && prefix="🖥️ " |
| 209 | + if [[ "$st" == "device" ]]; then |
| 210 | + echo -e " $((i + 1))) $prefix ${serial} (${name})" |
| 211 | + else |
| 212 | + echo -e " $((i + 1))) $prefix ${serial} (${name}) ${YELLOW}[${st}]${RESET}" |
| 213 | + fi |
| 214 | +done |
| 215 | + |
| 216 | +if (( ${#AVD_LIST[@]} > 0 )); then |
| 217 | + echo -e " $(( ${#ALL_SERIALS[@]} + 1 ))) 🧩 Start an AVD..." |
| 218 | +fi |
| 219 | + |
| 220 | +MAX_CHOICE=${#ALL_SERIALS[@]} |
| 221 | +(( ${#AVD_LIST[@]} > 0 )) && MAX_CHOICE=$((MAX_CHOICE + 1)) |
| 222 | + |
| 223 | +read -rp "🚀 Enter choice [1-${MAX_CHOICE}]: " CHOICE |
| 224 | +((CHOICE >= 1 && CHOICE <= MAX_CHOICE)) || { |
| 225 | + error "Invalid choice. Exiting." |
| 226 | + exit 1 |
| 227 | +} |
| 228 | + |
| 229 | +if (( ${#AVD_LIST[@]} > 0 )) && (( CHOICE == MAX_CHOICE )) && (( ${#ALL_SERIALS[@]} < MAX_CHOICE )); then |
| 230 | + divider |
| 231 | + log "Available AVDs (can be started):" |
| 232 | + for i in "${!AVD_LIST[@]}"; do |
| 233 | + echo -e " $((i + 1))) 🧩 ${AVD_LIST[$i]}" |
| 234 | + done |
| 235 | + read -rp "🧩 Enter AVD number to start: " AVD_CHOICE |
| 236 | + ((AVD_CHOICE >= 1 && AVD_CHOICE <= ${#AVD_LIST[@]})) || { |
| 237 | + error "Invalid AVD choice. Exiting." |
| 238 | + exit 1 |
| 239 | + } |
| 240 | + require_cmd "$EMULATOR_BIN" |
| 241 | + AVD_NAME="${AVD_LIST[$((AVD_CHOICE - 1))]}" |
| 242 | + log "Starting AVD: ${AVD_NAME}" |
| 243 | + #nohup emulator -avd "${AVD_NAME}" -no-snapshot -no-boot-anim -no-audio -no-metrics |
| 244 | + nohup "${EMULATOR_BIN}" -avd "${AVD_NAME}" -no-snapshot -no-boot-anim -no-audio -no-metrics >/tmp/avd-start.log 2>&1 & |
| 245 | + echo "EMULATOR_BIN: ${EMULATOR_BIN}" |
| 246 | + echo "AVD_NAME: ${AVD_NAME}" |
| 247 | + |
| 248 | + |
| 249 | + log "Waiting for emulator to show up in adb..." |
| 250 | + for _ in {1..60}; do |
| 251 | + sleep 1 |
| 252 | + if "$ADB_BIN" devices | awk 'NR>1 {print $1, $2}' | grep -q '^emulator-.* device$'; then |
| 253 | + break |
| 254 | + fi |
| 255 | + done |
| 256 | + |
| 257 | + # Pick the emulator that corresponds to the AVD we just started. |
| 258 | + SELECTED_SERIAL="" |
| 259 | + for _ in {1..60}; do |
| 260 | + mapfile -t ALL_DEVICES_RAW < <(list_all_devices) |
| 261 | + for line in "${ALL_DEVICES_RAW[@]}"; do |
| 262 | + serial=$(echo "$line" | awk '{print $1}') |
| 263 | + st=$(echo "$line" | awk '{print $2}') |
| 264 | + if [[ "$serial" =~ ^emulator- ]] && [[ "$st" == "device" ]]; then |
| 265 | + running_avd="$(get_avd_name_for_emulator_serial "$serial" || true)" |
| 266 | + if [[ "$running_avd" == "$AVD_NAME" ]]; then |
| 267 | + SELECTED_SERIAL="$serial" |
| 268 | + break |
| 269 | + fi |
| 270 | + fi |
| 271 | + done |
| 272 | + [[ -n "$SELECTED_SERIAL" ]] && break |
| 273 | + sleep 1 |
| 274 | + done |
| 275 | + |
| 276 | + if [[ -z "$SELECTED_SERIAL" ]]; then |
| 277 | + error "Started AVD '${AVD_NAME}', but could not find its running emulator via adb." |
| 278 | + warn "Check: $ADB_BIN devices -l" |
| 279 | + exit 1 |
| 280 | + fi |
| 281 | + |
| 282 | + SELECTED_NAME="$AVD_NAME" |
| 283 | +else |
| 284 | + SELECTED_SERIAL="${ALL_SERIALS[$((CHOICE - 1))]}" |
| 285 | + SELECTED_NAME="${ALL_NAMES[$((CHOICE - 1))]}" |
| 286 | +fi |
| 287 | + |
| 288 | +divider |
| 289 | +echo -e "🎯 Selected: ${GREEN}${SELECTED_SERIAL}${RESET} (${BOLD}${SELECTED_NAME}${RESET})" |
| 290 | + |
| 291 | +# ⏳ Wait for device to be online |
| 292 | +divider |
| 293 | +echo -e "⏳ Waiting for ${SELECTED_SERIAL} to be online..." |
| 294 | +"$ADB_BIN" -s "$SELECTED_SERIAL" wait-for-device |
| 295 | + |
| 296 | +STATE=$("$ADB_BIN" -s "$SELECTED_SERIAL" get-state 2>/dev/null | tr -d '\r\n' || true) |
| 297 | +if [[ "$STATE" != "device" ]]; then |
| 298 | + error "Selected target is not ready (state: ${STATE:-unknown})." |
| 299 | + warn "If this is USB: unlock phone and accept USB debugging prompt." |
| 300 | + exit 1 |
| 301 | +fi |
| 302 | + |
| 303 | +# 🚀 Launch Expo |
| 304 | +divider |
| 305 | +echo -e "📲 Launching ${BOLD}Expo${RESET} on ${GREEN}${SELECTED_NAME}${RESET}…" |
| 306 | + |
| 307 | +# Force Android tooling to the chosen serial. |
| 308 | +export ANDROID_SERIAL="${SELECTED_SERIAL}" |
| 309 | + |
| 310 | +# Expo chooses by "device name"; for USB we pass the model, for emulators the serial. |
| 311 | +EXPO_DEVICE_ARG="${SELECTED_NAME}" |
| 312 | +if [[ "$SELECTED_SERIAL" =~ ^emulator- ]]; then |
| 313 | + AVD_NAME="$(get_avd_name_for_emulator_serial "$SELECTED_SERIAL" || true)" |
| 314 | + if [[ -n "${AVD_NAME:-}" ]]; then |
| 315 | + EXPO_DEVICE_ARG="$AVD_NAME" |
| 316 | + else |
| 317 | + error "Could not resolve AVD name for ${SELECTED_SERIAL}." |
| 318 | + warn "Try: $ADB_BIN -s ${SELECTED_SERIAL} emu avd name" |
| 319 | + exit 1 |
| 320 | + fi |
| 321 | +fi |
| 322 | + |
| 323 | +npx expo run:android --device "$EXPO_DEVICE_ARG" |
0 commit comments