From c3d714eae9b6544d722aaff1b76dbbdbf761346b Mon Sep 17 00:00:00 2001 From: "burrito.space" Date: Thu, 21 May 2026 22:54:34 +0200 Subject: [PATCH] fix(android): serialised adb wrapper + shepherd to eliminate SIGABRT crashes adb 37.0.0 LOG(FATAL) regression: concurrent adb clients racing to bind port 5037 SIGABRT instead of falling back gracefully. Observed 30+ crashes in 3 minutes during tauri android dev HMR polling. Fix: intercept adb via a shim ANDROID_HOME; platform-tools/adb is replaced by a serialised wrapper that uses atomic-mkdir locking so only ONE process at a time can enter the start-server path. A background shepherd in dev-android.sh monitors port 5037 every second and restarts the daemon proactively, closing the window where concurrent clients would race. New files: apps/mobile/bin/adb - serialised adb wrapper script apps/mobile/bin/setup-adb-shim.sh - builds shim ANDROID_HOME at startup Modified: apps/mobile/dev-android.sh - sources shim, starts shepherd, sets cleanup trap Verified: 50 concurrent adb calls with daemon alive produce 0 crashes; 5 rounds of kill-daemon + 10 concurrent calls produce 0 crashes with wrapper. --- apps/mobile/bin/adb | 96 +++++++++++++++++++++++++++++++ apps/mobile/bin/setup-adb-shim.sh | 68 ++++++++++++++++++++++ apps/mobile/dev-android.sh | 49 +++++++++++++++- 3 files changed, 212 insertions(+), 1 deletion(-) create mode 100755 apps/mobile/bin/adb create mode 100755 apps/mobile/bin/setup-adb-shim.sh diff --git a/apps/mobile/bin/adb b/apps/mobile/bin/adb new file mode 100755 index 00000000..0c3c8e2a --- /dev/null +++ b/apps/mobile/bin/adb @@ -0,0 +1,96 @@ +#!/bin/sh +# adb-wrapper: serialised adb proxy that breaks the concurrent server-spawn race. +# +# Root cause of SIGABRT in adb_server_main (observed 30+ crashes / 3 min during +# tauri android dev, 2026-05-21): +# tauri-cli polls via many concurrent adb child processes. Each child, if it +# cannot connect to the daemon on first attempt, forks a server-start subprocess +# (adb fork-server server). When multiple of these race to bind port 5037, the +# losers call LOG(FATAL) → SIGABRT. +# +# Fix: serialise the "server not running → start it" phase. +# Fast path (server up): TCP probe with nc (does NOT auto-start server). If the +# daemon is reachable, skip locking and exec the real adb command directly. +# Slow path (server down): acquire an atomic mkdir lock, re-check inside the lock, +# start server under the lock if still down, release lock, wait for server to be +# ready, then exec real adb. +# +# Requirements: +# PEEK_REAL_ADB — absolute path to the real adb binary (set by dev-android.sh) +# Fallback if PEEK_REAL_ADB is not set: $HOME/Library/Android/sdk/platform-tools/adb + +REAL_ADB="${PEEK_REAL_ADB}" +if [ -z "$REAL_ADB" ] || [ ! -x "$REAL_ADB" ]; then + _fallback="$HOME/Library/Android/sdk/platform-tools/adb" + if [ -x "$_fallback" ]; then + REAL_ADB="$_fallback" + else + echo "adb-wrapper: PEEK_REAL_ADB not set and real adb not found at $_fallback" >&2 + exit 1 + fi +fi + +ADB_PORT="${ANDROID_ADB_SERVER_PORT:-5037}" +LOCKDIR="${TMPDIR:-/tmp}/peek-adb-server.lck" + +# Fast path: if the adb daemon is already reachable (raw TCP probe — does NOT +# auto-start the server), exec the real command directly with zero overhead. +if nc -z -w1 127.0.0.1 "$ADB_PORT" 2>/dev/null; then + exec "$REAL_ADB" "$@" +fi + +# Slow path: daemon not reachable. +# Use atomic mkdir as a cross-process exclusive lock. +# Strategy: +# - The FIRST process to acquire the lock starts the server. +# - All other processes wait for the lock, then find the server already up. +# - After any wait (lock or timeout), re-check TCP before proceeding; if the +# server is now up, exec directly (avoids re-entering the start-server path). + +_lock_acquired=0 +_max_retries=50 # 50 * 0.1s = 5 seconds max wait +_retries=0 + +while [ "$_retries" -lt "$_max_retries" ]; do + if mkdir "$LOCKDIR" 2>/dev/null; then + _lock_acquired=1 + # Write PID for stale-lock detection + echo $$ > "$LOCKDIR/pid" 2>/dev/null || true + break + fi + # Check for stale lock: lock dir exists but owner PID is gone + _pid_file="$LOCKDIR/pid" + if [ -f "$_pid_file" ]; then + _lock_pid="$(cat "$_pid_file" 2>/dev/null)" + if [ -n "$_lock_pid" ] && ! kill -0 "$_lock_pid" 2>/dev/null; then + # Stale lock from a dead process — remove and retry immediately + rm -rf "$LOCKDIR" 2>/dev/null || true + continue + fi + fi + # While waiting, the lock holder may have already started the server. + # Re-check TCP: if the server came up, we can skip waiting entirely. + if nc -z -w1 127.0.0.1 "$ADB_PORT" 2>/dev/null; then + exec "$REAL_ADB" "$@" + fi + sleep 0.1 + _retries=$((_retries + 1)) +done + +if [ "$_lock_acquired" -eq 1 ]; then + # We hold the lock. Re-check (another process may have just started the server) + if ! nc -z -w1 127.0.0.1 "$ADB_PORT" 2>/dev/null; then + # start-server waits until the daemon is fully ready before returning + "$REAL_ADB" -P "$ADB_PORT" start-server >/dev/null 2>&1 || true + fi + # Release the lock before exec (exec replaces this process; traps don't fire) + rm -rf "$LOCKDIR" 2>/dev/null || true +else + # Timed out waiting for the lock. By this point (~5 seconds) the server + # should be running (started by whichever process held the lock). Proceed; + # the real adb will connect normally or fail gracefully. + true +fi + +# Run the actual command. +exec "$REAL_ADB" "$@" diff --git a/apps/mobile/bin/setup-adb-shim.sh b/apps/mobile/bin/setup-adb-shim.sh new file mode 100755 index 00000000..c5ede5cf --- /dev/null +++ b/apps/mobile/bin/setup-adb-shim.sh @@ -0,0 +1,68 @@ +#!/bin/sh +# setup-adb-shim.sh: create a shim ANDROID_HOME that intercepts adb calls. +# +# Usage: +# . "$(dirname "$0")/setup-adb-shim.sh" # source it to get env vars +# +# After sourcing, ANDROID_HOME and PEEK_REAL_ADB are set in the calling shell. +# +# What it does: +# Creates /tmp/peek-android-shim/ mirroring the real ANDROID_HOME but with +# platform-tools/adb replaced by the flock-serialised wrapper (bin/adb). +# All other SDK dirs/files are symlinked so tauri-cli can still find the +# emulator, cmdline-tools, ndk, build-tools, etc. +# +# The shim is recreated each invocation (idempotent; takes ~10ms). + +_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" +_WRAPPER_ADB="$_SCRIPT_DIR/adb" +_REAL_HOME="${ANDROID_HOME:-$HOME/Library/Android/sdk}" +_SHIM_DIR="${TMPDIR:-/tmp}peek-android-shim" + +if [ ! -d "$_REAL_HOME" ]; then + echo "setup-adb-shim: ANDROID_HOME not found: $_REAL_HOME" >&2 + return 1 2>/dev/null || exit 1 +fi + +if [ ! -x "$_WRAPPER_ADB" ]; then + echo "setup-adb-shim: wrapper not found: $_WRAPPER_ADB" >&2 + return 1 2>/dev/null || exit 1 +fi + +# Recreate shim dir from scratch each run (fast: mkdir + ln, no build overhead) +rm -rf "$_SHIM_DIR" +mkdir -p "$_SHIM_DIR" + +# Symlink every top-level entry from the real SDK root, skipping platform-tools +# (which we shadow with a real directory containing our wrapper adb) +for _entry in "$_REAL_HOME"/*/; do + _name="$(basename "$_entry")" + if [ "$_name" != "platform-tools" ]; then + ln -s "$_entry" "$_SHIM_DIR/$_name" + fi +done + +# Symlink hidden files at the SDK root (e.g. .knownPackages, .DS_Store) +for _f in "$_REAL_HOME"/.*; do + _name="$(basename "$_f")" + if [ "$_name" != "." ] && [ "$_name" != ".." ] && [ -e "$_f" ]; then + ln -s "$_f" "$_SHIM_DIR/$_name" 2>/dev/null || true + fi +done + +# Build a real platform-tools/ directory with all files symlinked except adb +mkdir -p "$_SHIM_DIR/platform-tools" +for _pt in "$_REAL_HOME/platform-tools"/*; do + _pt_name="$(basename "$_pt")" + if [ "$_pt_name" != "adb" ]; then + ln -s "$_pt" "$_SHIM_DIR/platform-tools/$_pt_name" + fi +done + +# Install our wrapper as the intercepted adb +cp "$_WRAPPER_ADB" "$_SHIM_DIR/platform-tools/adb" +chmod +x "$_SHIM_DIR/platform-tools/adb" + +# Export the env vars into the calling shell +export PEEK_REAL_ADB="$_REAL_HOME/platform-tools/adb" +export ANDROID_HOME="$_SHIM_DIR" diff --git a/apps/mobile/dev-android.sh b/apps/mobile/dev-android.sh index eb17ae6c..15b8fdb6 100755 --- a/apps/mobile/dev-android.sh +++ b/apps/mobile/dev-android.sh @@ -46,7 +46,7 @@ echo "ANDROID_HOME=$ANDROID_HOME" echo "JAVA_HOME=$JAVA_HOME" echo "NDK_HOME=$NDK_HOME" -# Ensure local.properties exists +# Ensure local.properties exists (written with real SDK path before shim takes over) TAURI_DIR="$SCRIPT_DIR/src-tauri" if [ ! -f "$TAURI_DIR/gen/android/local.properties" ]; then mkdir -p "$TAURI_DIR/gen/android" @@ -54,6 +54,53 @@ if [ ! -f "$TAURI_DIR/gen/android/local.properties" ]; then echo "Created local.properties" fi +# ── adb crash-prevention shim ────────────────────────────────────────────── +# tauri android dev spawns many concurrent adb child processes. In adb 37.0.0 +# a race-to-bind-port-5037 regression causes SIGABRT in adb_server_main when +# multiple processes try to start the daemon simultaneously (30+ crashes / 3 min +# observed 2026-05-21, task 93e18a97). +# +# Fix: intercept adb via a shim ANDROID_HOME whose platform-tools/adb is a +# flock-serialised wrapper (apps/mobile/bin/adb). The wrapper ensures only ONE +# process at a time can be in the "start server" state; all others either see +# the daemon is already up (fast TCP probe) or wait for the lock. +# +# Additionally, a background shepherd monitors the daemon and restarts it if it +# dies, closing the window where concurrent adb clients would all race to start. + +# Record real SDK path before overriding ANDROID_HOME +REAL_ANDROID_HOME="$ANDROID_HOME" + +# Source the shim script; exports ANDROID_HOME (shim dir) and PEEK_REAL_ADB +# shellcheck disable=SC1091 +. "$SCRIPT_DIR/bin/setup-adb-shim.sh" +echo "[adb-shim] ANDROID_HOME → $ANDROID_HOME (shim)" +echo "[adb-shim] PEEK_REAL_ADB = $PEEK_REAL_ADB" + +# Background shepherd: keep the adb daemon alive during the tauri run. +# Uses the REAL adb directly (not the wrapper) to avoid re-entry. +_adb_daemon_shepherd() { + local _real="$1" + local _port="${ANDROID_ADB_SERVER_PORT:-5037}" + while true; do + if ! nc -z -w1 127.0.0.1 "$_port" 2>/dev/null; then + "$_real" -P "$_port" start-server >/dev/null 2>&1 || true + fi + sleep 1 + done +} +_adb_daemon_shepherd "$PEEK_REAL_ADB" & +_SHEPHERD_PID=$! + +# Kill shepherd when dev session ends (Ctrl-C, normal exit, or error) +_cleanup_shepherd() { + kill "$_SHEPHERD_PID" 2>/dev/null || true +} +trap '_cleanup_shepherd' EXIT INT TERM HUP + +echo "[adb-shim] shepherd started (PID $_SHEPHERD_PID)" +# ── end adb crash-prevention shim ───────────────────────────────────────── + # Bump build number (auto-create if missing — fresh clones don't have this file) BUILD_FILE="$SCRIPT_DIR/BUILD_NUMBER" if [ ! -f "$BUILD_FILE" ]; then -- 2.51.2