testsAndMisc/phone_focus_mode/enable_persistent_adb.sh
Krzysztof Rudnicki 287512f011 phone_focus_mode: reach the phone over adb without touching it
Android's Wireless debugging is deliberately ephemeral: the listener dies
when you leave the settings screen or the device sleeps, and it picks a
new random port each time. In one session it offered 35545, then 40401,
then 43781, and vanished entirely twice. Every deploy therefore started
with tapping through Settings and reading a port off the screen.

enable_persistent_adb.sh waits for any such transient window, then uses it
to pin adbd to port 5555 - the port deploy.sh has always assumed - and
installs a Magisk service.d boot script so the same happens on every boot.

The phone is on DHCP with no reservation, so the script identifies it by
MAC rather than trusting a hardcoded address: a ping sweep populates the
neighbour table and the address is looked up there. That beats a router
reservation because it needs no router access and works on any network.

One bug worth naming: the first version took the first `device` line from
`adb devices`. Pairing also leaves an mDNS _adb-tls-connect._tcp entry for
the same phone, so it reported port 5555 as reachable while nmap showed it
closed. It now matches the exact ip:port.

Verified: nmap showed 5555 closed then open across the switch; a run
against a deliberately wrong IP reported "phone moved" and connected; and
after `adb reboot` the script connected with no arguments and no
interaction, with /proc/uptime at 149s and service.adb.tcp.port reading
5555, so the boot script really ran. shellcheck clean.

Trade-off accepted by the user: adbd on a fixed port is reachable to
anything on the LAN, though it still requires an authorised key.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 13:33:45 +02:00

194 lines
6.4 KiB
Bash
Executable File

#!/bin/bash
# ============================================================================
# Make adb-over-Wi-Fi permanent on the phone (port 5555).
#
# Android's "Wireless debugging" is deliberately ephemeral: the listener dies
# when you leave the settings screen or the device sleeps, and it picks a new
# random port every time it comes back. That is why every deploy so far has
# needed someone to tap through the phone first.
#
# This script waits for ANY transient wireless-debugging window, then uses it
# to switch adbd to a fixed port 5555 and installs a Magisk boot script so the
# same thing happens on every boot. After one successful run the phone is
# reachable at $PHONE_IP:5555 forever, with no tapping.
#
# Requires: root (Magisk) on the device, and the host already paired.
#
# Usage:
# ./enable_persistent_adb.sh [PHONE_IP] [KNOWN_PORT]
# PHONE_IP defaults to 192.168.1.64.
# KNOWN_PORT, if given, is tried first before falling back to a port scan.
# ============================================================================
set -uo pipefail
PHONE_IP="${1:-192.168.1.64}"
readonly KNOWN_PORT="${2:-}"
readonly FIXED_PORT=5555
# The phone is on DHCP, so its address can move. Its MAC cannot, so that is
# what we actually identify it by; PHONE_IP is only a first guess.
readonly PHONE_MAC="00:08:22:68:81:fc"
# Android allocates the wireless-debugging port from the ephemeral range.
readonly SCAN_RANGE="30000-65535"
# Give up rather than spin forever if nobody ever wakes the phone.
readonly DEADLINE_SECONDS=1800
readonly BOOT_SCRIPT=/data/adb/service.d/99-adb-tcp-5555.sh
log() { printf '[persistent-adb] %s\n' "$1"; }
require() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "Error: $1 is required but not installed" >&2
exit 1
fi
}
# Echo the serial iff THIS endpoint is connected and not offline.
#
# Deliberately matches the exact ip:port rather than the first `device` line.
# After pairing, adb also auto-connects an mDNS `_adb-tls-connect._tcp` entry
# for the same phone; taking the first line made `try_connect 5555` report
# success while port 5555 was in fact closed.
connected_serial() {
local endpoint="$1"
adb devices 2>/dev/null |
awk -v want="$endpoint" '$1 == want && $2 == "device" {print $1; exit}'
}
# Try to reach adbd. Returns 0 and sets REPLY to the serial when connected.
try_connect() {
local port="$1" endpoint
endpoint="${PHONE_IP}:${port}"
adb connect "$endpoint" >/dev/null 2>&1 || return 1
# `connect` succeeds optimistically; the device can still be offline.
sleep 2
REPLY="$(connected_serial "$endpoint")"
[[ -n "$REPLY" ]]
}
# Locate the phone on the current subnet by MAC and echo its IP.
#
# DHCP can hand the phone a different address at any time, which would break a
# hardcoded PHONE_IP. A ping sweep populates the neighbour table, then we look
# the MAC up in it. Prefer this over a router DHCP reservation because it needs
# no router access and keeps working on any network the laptop joins.
discover_ip_by_mac() {
local subnet
subnet="$(ip -4 route show dev "$(ip -4 route show default |
awk '/default/ {print $5; exit}')" 2>/dev/null |
awk '/proto kernel/ {print $1; exit}')"
[[ -n "$subnet" ]] || return 1
nmap -sn -T4 "$subnet" >/dev/null 2>&1
ip neigh show 2>/dev/null |
awk -v mac="$PHONE_MAC" 'tolower($5) == tolower(mac) {print $1; exit}'
}
# Find the current ephemeral wireless-debugging port, if one is listening.
scan_for_port() {
nmap -p "$SCAN_RANGE" --open -T4 -Pn --min-rate 3000 "$PHONE_IP" 2>/dev/null |
awk -F/ '/^[0-9]+\/tcp[[:space:]]+open/ {print $1}' | head -1
}
# Switch adbd to the fixed port and persist it across reboots.
make_permanent() {
local serial="$1"
if ! adb -s "$serial" shell 'su -c id' 2>/dev/null | grep -q 'uid=0'; then
echo "Error: no root on the device; cannot install the boot script." >&2
echo " adb tcpip $FIXED_PORT alone would not survive a reboot." >&2
return 1
fi
log "installing Magisk boot script at $BOOT_SCRIPT"
# service.d runs late in boot, after the network is up, which is what adbd
# needs. Restarting adbd is what actually makes it re-read the port.
adb -s "$serial" shell "su -c 'cat > $BOOT_SCRIPT'" <<EOF
#!/system/bin/sh
# Installed by phone_focus_mode/enable_persistent_adb.sh.
# Pin adb-over-Wi-Fi to a fixed port so the laptop can always connect without
# anyone tapping through Settings -> Wireless debugging first.
setprop service.adb.tcp.port $FIXED_PORT
stop adbd
start adbd
EOF
adb -s "$serial" shell "su -c 'chmod 755 $BOOT_SCRIPT'"
log "switching adbd to port $FIXED_PORT now"
adb -s "$serial" shell "su -c 'setprop service.adb.tcp.port $FIXED_PORT; stop adbd; start adbd'"
# adbd restarting drops every current connection, including this one.
sleep 4
}
main() {
require adb
require nmap
log "waiting for a wireless-debugging window on $PHONE_IP (up to $((DEADLINE_SECONDS / 60))m)"
log "wake the phone and open Settings -> Developer options -> Wireless debugging"
local deadline=$((SECONDS + DEADLINE_SECONDS))
local serial=""
while ((SECONDS < deadline)); do
# Cheapest first: the fixed port may already be live from a prior run.
if try_connect "$FIXED_PORT"; then
serial="$REPLY"
log "already reachable on $FIXED_PORT - nothing to do"
printf '%s\n' "$serial"
return 0
fi
# Not at the expected address — DHCP may have moved it. Re-find the
# phone by MAC before concluding that adbd is down.
local moved
moved="$(discover_ip_by_mac)"
if [[ -n "$moved" && "$moved" != "$PHONE_IP" ]]; then
log "phone moved: $PHONE_IP -> $moved"
PHONE_IP="$moved"
if try_connect "$FIXED_PORT"; then
serial="$REPLY"
log "reachable on $FIXED_PORT at the new address"
printf '%s\n' "$serial"
return 0
fi
fi
if [[ -n "$KNOWN_PORT" ]] && try_connect "$KNOWN_PORT"; then
serial="$REPLY"
break
fi
local found
found="$(scan_for_port)"
if [[ -n "$found" ]] && try_connect "$found"; then
serial="$REPLY"
break
fi
sleep 5
done
if [[ -z "$serial" ]]; then
echo "Error: no wireless-debugging window appeared before the deadline." >&2
return 1
fi
log "connected as $serial"
make_permanent "$serial" || return 1
if try_connect "$FIXED_PORT"; then
log "SUCCESS: phone is now reachable at ${PHONE_IP}:${FIXED_PORT}"
log "it will come back on this port after every reboot"
printf '%s\n' "${PHONE_IP}:${FIXED_PORT}"
return 0
fi
echo "Error: adbd did not come back on $FIXED_PORT after the switch." >&2
return 1
}
main "$@"