6.4 KiB
Use a paired Google ADB connection
The Python client remains the default. If the TV offers encrypted Wireless debugging,
adb_shell cannot speak its TLS protocol. The optional Google ADB backend lets the existing
tools use that connection without opening a plaintext ADB port.
Install Google's platform-tools and the usual tools/requirements.txt dependencies first.
On the TV, enable Wireless debugging. Pair with the address, port and code from Pair device
with pairing code, then connect to the separate address and port on the main Wireless
debugging screen:
adb pair <tv-ip>:<pairing-port>
adb connect <tv-ip>:<connection-port>
export TV_ADB_BINARY=/absolute/path/to/adb
export TV_HOST=<tv-ip>:<connection-port>
tools/venv/bin/python -I tools/tv.py --probe
Google ADB manages pairing and authentication; the tools neither read its private key nor
implement TLS. Keep the main screen's current connection port: it can change when debugging
is toggled or the TV restarts. TV_HOST and TV_PORT have their usual meaning. IPv6 endpoints
use [address]:port. The executable may also be a name on PATH, such as adb.
The backend requires an already connected, authorized transport. Every operation selects
that exact endpoint with adb -s; it never falls back to another TV or runs adb tcpip
to change the TV's debugging mode. Its close() leaves the shared Google ADB connection
and server running, so other tools using them keep working.
The setting is read by tools/tv.py, so every tool that reaches the TV through it uses the
backend: the scripts tools/disable.sh, tools/restore.sh, tools/theme.sh and
launcher/build-and-install.sh, and the Python tools, tools/check.py (the check after a
firmware update), tools/privacy.py, tools/bench.py, tools/trace.py,
tools/theme_restart.py, tools/b64push.py, tools/recovery.py and tools/manage.py.
Their existing safety checks still run, and on a set whose only network ADB is Wireless
debugging this is how the check after an update runs without opening the plaintext port with
adb tcpip 5555. Back up first and follow PROMPT.md before changing a TV. For example:
tools/venv/bin/python -I tools/tv.py --screenshot shots/before.png
When finished, switch Wireless debugging and any other enabled debugging modes off on the
TV and revoke the authorizations. Check the noted connection port with
python3 tools/adb-closed.py <tv-ip> <connection-port> before rebooting, then check that
debugging remains off after the reboot. Pairing codes, keys and screenshots stay private.
macOS: a stock adb that cannot reach the LAN
On macOS 15 (Sequoia) the adb command starts its own server daemon (adb fork-server server) and
leaves the work to it. Local Network privacy grants the permission to the app that ran the command -
Terminal, iTerm2, VS Code - and that daemon outlives the command, which the report reads as the
grant not reaching it: adb connect <tv-ip>:5555 answers failed to connect to '<tv-ip>:5555': No route to host (65) while tools/tv.py in the same window reaches the TV. Reported on macOS Sequoia
in #3336, which names no TV model.
The Python client needs nothing here, since it opens its own socket from the window that holds the
permission. The stock binary is what TV_ADB_BINARY runs for a Wireless debugging pairing, and what
adb install and adb push use. The relay below runs in that window instead: it listens on
loopback, which the Local Network check does not cover, and forwards to the TV from the process that
does hold the permission. Save it as relay.py:
#!/usr/bin/env python3
"""Forward 127.0.0.1:<local port> to <tv ip>:<tv port>, for a stock adb blocked on macOS."""
import socket
import sys
import threading
def pipe(src, dst):
try:
while data := src.recv(65536):
dst.sendall(data)
except OSError:
pass
finally:
for sock in (src, dst):
sock.close()
def main():
tv_host, tv_port, local_port = sys.argv[1], int(sys.argv[2]), int(sys.argv[3])
server = socket.socket()
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("127.0.0.1", local_port))
server.listen(8)
print(f"relaying 127.0.0.1:{local_port} -> {tv_host}:{tv_port}", flush=True)
while True:
client, _ = server.accept()
try:
upstream = socket.create_connection((tv_host, tv_port), timeout=10)
upstream.settimeout(None) # the connect timeout must not cut an idle session off later
except OSError as e:
print(f"cannot reach {tv_host}:{tv_port}: {e} (is the TV on, and network debugging still on?)",
flush=True)
client.close()
continue
threading.Thread(target=pipe, args=(client, upstream), daemon=True).start()
threading.Thread(target=pipe, args=(upstream, client), daemon=True).start()
main()
An attempt that cannot reach the TV, refused or timed out, is printed and the relay goes back to
waiting for the next client, the same boundary tools/relay.py keeps around its connect. Without it
the error ends the program, so a TV that was in standby for one adb command leaves every later
command refused on the loopback port until the relay is started again.
Start it for the port adb has to reach, and give both adb and the tools the loopback address:
python3 relay.py <tv-ip> 5555 15555
adb connect 127.0.0.1:15555
export TV_ADB_BINARY=adb
export TV_HOST=127.0.0.1:15555
tools/venv/bin/python -I tools/tv.py --probe
A pairing goes through a second copy on the pairing port while the first one carries the connection,
for example python3 relay.py <tv-ip> 37123 37123 and then adb pair 127.0.0.1:37123, since the two
ports are different. Two things to keep in mind, as it bridges rather than fixes the denial:
- It has to stay up for as long as
adbis used. Stop it and the connection it carries reads as a TV that went away;adb kill-serverreconnects through it only while it listens. - What it forwards is the plaintext port, though only from
127.0.0.1, so nothing else on the network reaches the TV through it. The Python client needs no relay at all, and a window that is itself denied by Local Network privacy still fails: that is the macOS bullet in docs/TROUBLESHOOTING.md, not this one.