TCP/IP reconnect
This commit is contained in:
48
main.py
48
main.py
@@ -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
113
tcpip.py
Normal 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
|
||||||
Reference in New Issue
Block a user