TCP/IP reconnect

This commit is contained in:
2025-10-27 23:56:16 -04:00
parent ea2f98cfc3
commit 7d16f0fd20
2 changed files with 155 additions and 6 deletions

48
main.py
View File

@@ -6,6 +6,7 @@ import time
import logging import logging
from BetterADBSync import sync_with_options from BetterADBSync import sync_with_options
from tcpip import reconnect_as_tcpip
# debug vs standard logging # debug vs standard logging
DEBUG = False DEBUG = False
@@ -79,11 +80,24 @@ def traverse_tree(tree: dict, level: int = 0):
def main(): def main():
parser = argparse.ArgumentParser(description="Daemon for syncing files between a computer and an Android device.") 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 " parser.add_argument(
"syncing files. (Only runs once)") "--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() 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 # Read config file
if not os.path.exists("config.ini"): if not os.path.exists("config.ini"):
# Create a new config file # Create a new config file
@@ -121,20 +135,29 @@ def main():
logging.error("Could not start ADB server. Exiting...") logging.error("Could not start ADB server. Exiting...")
return return
# Disconnect all devices to start fresh
subprocess.run([adb_path, "disconnect"], stdout=subprocess.PIPE)
run = True run = True
first_run = True first_run = True
connected = False
while run: while run:
# Get the list of devices # Get the list of devices
result = subprocess.run([adb_path, "devices"], stdout=subprocess.PIPE) result = subprocess.run([adb_path, "devices"], stdout=subprocess.PIPE)
output = result.stdout.decode("utf-8") output = result.stdout.decode("utf-8")
devices = output.split(os.linesep)[1:-2] 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: if len(devices) == 0:
# Device is not yet connnected # Device is not yet connected
logging.info("No device connected. Retrying in 60 seconds.") device = config["Global"]["device"] # reset device to config value, in case it was TCP/IP before
time.sleep(60) first_run = True
if not connected:
logging.info("No device connected. Waiting silently until a device is connected.")
time.sleep(10)
continue continue
connected = True
if device and device not in devices: if device and device not in devices:
time.sleep(60) time.sleep(60)
continue continue
@@ -143,6 +166,19 @@ def main():
if first_run: if first_run:
logging.info(f"Device connected: {device}") 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(f"Found {len(config.sections()) - 1} path{'s' if len(config.sections()) > 2 else ''} to sync.")
logging.info('') logging.info('')

113
tcpip.py Normal file
View File

@@ -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