From 7d16f0fd208250524747c2c1d819fefa035ad161 Mon Sep 17 00:00:00 2001 From: Nic Jones Date: Mon, 27 Oct 2025 23:56:16 -0400 Subject: [PATCH] TCP/IP reconnect --- main.py | 48 ++++++++++++++++++++--- tcpip.py | 113 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 6 deletions(-) create mode 100644 tcpip.py diff --git a/main.py b/main.py index 672200a..2b8169e 100755 --- a/main.py +++ b/main.py @@ -6,6 +6,7 @@ import time import logging from BetterADBSync import sync_with_options +from tcpip import reconnect_as_tcpip # debug vs standard logging DEBUG = False @@ -79,11 +80,24 @@ def traverse_tree(tree: dict, level: int = 0): def main(): parser = argparse.ArgumentParser(description="Daemon for syncing files between a computer and an Android device.") - parser.add_argument("--dry-run", action="store_true", help="Perform a dry run without actually " - "syncing files. (Only runs once)") + parser.add_argument( + "--dry-run", + action="store_true", + help="Perform a dry run without actually syncing files. (Only runs once)" + ) + parser.add_argument( + "--tcpip", + action="store_true", + help="Reconnect to device over TCP/IP before syncing. Linux only." + ) args = parser.parse_args() + if args.tcpip: + if os.name != 'posix': + logging.error("TCP/IP mode is only supported on Linux systems.") + return + # Read config file if not os.path.exists("config.ini"): # Create a new config file @@ -121,20 +135,29 @@ def main(): logging.error("Could not start ADB server. Exiting...") return + # Disconnect all devices to start fresh + subprocess.run([adb_path, "disconnect"], stdout=subprocess.PIPE) + run = True first_run = True + connected = False while run: # Get the list of devices result = subprocess.run([adb_path, "devices"], stdout=subprocess.PIPE) output = result.stdout.decode("utf-8") devices = output.split(os.linesep)[1:-2] - devices = [device.split("\t")[0] for device in devices] + devices = [device.split("\t")[0] for device in devices if "offline" not in device] if len(devices) == 0: - # Device is not yet connnected - logging.info("No device connected. Retrying in 60 seconds.") - time.sleep(60) + # Device is not yet connected + device = config["Global"]["device"] # reset device to config value, in case it was TCP/IP before + first_run = True + if not connected: + logging.info("No device connected. Waiting silently until a device is connected.") + time.sleep(10) continue + + connected = True if device and device not in devices: time.sleep(60) continue @@ -143,6 +166,19 @@ def main(): if first_run: logging.info(f"Device connected: {device}") + if args.tcpip: + # Reconnect over TCP/IP + logging.info("Reconnecting device over TCP/IP...") + try: + tcpip_device = reconnect_as_tcpip(device, adb_path) + except (RuntimeError, TimeoutError) as e: + logging.warning(f"Failed to reconnect device over TCP/IP: {e}") + logging.warning("Continuing with the current connection after 10 seconds.") + time.sleep(10) + else: + device = tcpip_device + logging.info(f"Device reconnected over TCP/IP at {tcpip_device}") + logging.info(f"Found {len(config.sections()) - 1} path{'s' if len(config.sections()) > 2 else ''} to sync.") logging.info('') diff --git a/tcpip.py b/tcpip.py new file mode 100644 index 0000000..412304b --- /dev/null +++ b/tcpip.py @@ -0,0 +1,113 @@ +import ipaddress +import subprocess +import time + +def get_available_subnets() -> list[ipaddress.IPv4Network | ipaddress.IPv6Network]: + """Uses the 'ip' command to get a list of available subnets on the system.""" + r = subprocess.run(['ip', 'route', 'show'], stdout=subprocess.PIPE) + output = r.stdout.decode('utf-8') + return [ipaddress.ip_network(line.split()[0]) for line in output.splitlines() if 'scope link' in line] + + +def get_device_ips(serial: str, adb_path='adb') -> list[ipaddress.IPv4Address]: + """Get all the IP addresses of the connected ADB device.""" + r = subprocess.run([ + adb_path, '-s', serial, + 'shell', f"ip -f inet addr show | grep inet" + ], stdout=subprocess.PIPE) + output = r.stdout.decode('utf-8') + ips = [] + for line in output.splitlines(): + parts = line.strip().split() + if len(parts) >= 2: + addr = ipaddress.ip_address(parts[1].split('/')[0]) + + if isinstance(addr, ipaddress.IPv6Address): # Skip IPv6 addresses + continue + + if addr.is_loopback or addr.is_link_local or not addr.is_private: # Skip unwanted addresses + continue + + ips.append(addr) + + return ips + + +def get_best_connections(serial: str, adb_path='adb') -> list[ipaddress.IPv4Address]: + """Get a list of the best IP addresses to connect to the device. + + They are ordered by preference, with the most preferred first. + The 'best' IPs are defined as those that are in the first available routed subnet on the host machine. + """ + device_ips = get_device_ips(serial, adb_path) + if not device_ips: + return [] + + available_subnets = get_available_subnets() + best_ips = [] + for subnet in available_subnets: + for ip in device_ips: + if ip in subnet: + best_ips.append(ip) + + return best_ips + + +def get_available_port(serial: str, adb_path='adb') -> int | None: + """Get an available TCP port on the device for ADB connection.""" + for i in range(5555, 5586): + r = subprocess.run([ + adb_path, '-s', serial, + 'shell', f"netstat -an | grep :{i} " + f"| grep LISTEN" + ], stdout=subprocess.PIPE) + output = r.stdout.decode('utf-8') + if not output.strip(): + return i + + return None + + +def reconnect_as_tcpip(serial: str, adb_path='adb') -> str: + """Reconnect the ADB device as TCP/IP. Returns the new connection string.""" + + # Find the best IP address to connect to + best_ips = get_best_connections(serial, adb_path) + if not best_ips: + raise RuntimeError("No suitable IP address found on device for TCP connection.") + + # Find an available port on the device + port = get_available_port(serial, adb_path) + if port is None: + raise RuntimeError("No available TCP port found on device for ADB connection.") + + # Restart ADB in TCP mode + r = subprocess.run([ + adb_path, '-s', serial, + 'tcpip', str(port) + ], stdout=subprocess.PIPE) + output = r.stdout.decode('utf-8') + if 'restarting in TCP mode' not in output: + # If this doesn't work the first time, usually a second attempt after a few seconds will work + time.sleep(2) + r = subprocess.run([ + adb_path, '-s', serial, + 'tcpip', str(port) + ], stdout=subprocess.PIPE) + output = r.stdout.decode('utf-8') + if 'restarting in TCP mode' not in output: + raise RuntimeError(f"Failed to restart device in TCP mode: {output.strip()}") + + # A slight delay is needed to allow the device to switch modes + time.sleep(2) + + # Connect to the device over TCP/IP + r = subprocess.run([ + adb_path, 'connect', f"{best_ips[0]}:{port}" + ], stdout=subprocess.PIPE, timeout=5) + output = r.stdout.decode('utf-8') + if 'connected to' not in output: + raise RuntimeError(f"Failed to connect to device over TCP/IP: {output.strip()}") + + connection_string = f"{best_ips[0]}:{port}" + return connection_string