I am writing a Python wrapper around Appium server. Appium accepts command-line parameter for a local port to bind to. Unfortunately, Appium cannot autoselect a free port for itself, so it either binds to explicitly specified port, or fails with EADDRINUSE
. Even when telling it to bind to port 0
, it will start successfully, but won’t display what port it had bound to.
If I find a free port myself in the Python wrapper, there is no guarantee that some other process won’t bind to the same port meanwhile I am passing it to Appium. And if I don’t release it first myself, Appium won’t be able to bind to it, so I have to.
I know this is unlikely to ever happen in practice, but what would be the “right way” to “reserve” a local port number before passing it to another process in a cross-platform way (Linux, macOS, Windows)?
Advertisement
Answer
Thanks to @rodrigo suggestion in comments, I have ended up with this code:
import platform import re import subprocess from typing import Set if platform.system() == 'Windows': def _get_ports(pid): sp = subprocess.run(['netstat', '-anop', 'TCP'], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, check=True) rx_socket = re.compile(br'''(?x) ^ s* TCP s+ 127.0.0.1 : (?P<port>d{1,5}) s+ .*? s+ LISTENING s+ (?P<pid>d+) s* $''') for line in sp.stdout.splitlines(): rxm = rx_socket.match(line) if rxm is None: continue sock_port, sock_pid = map(int, rxm.groups()) if sock_pid == pid: yield sock_port else: def _get_ports(pid): sp = subprocess.run(['lsof', '-anlPFn', '+w', f'-p{pid}', '-i4TCP@127.0.0.1', '-sTCP:LISTEN'], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, check=True) for line in sp.stdout.splitlines(): if line.startswith(b'n'): host, port = line.rsplit(b':', 1) port = int(port) yield port def get_ports(pid: int) -> Set[int]: """Get set of local-bound listening TCPv4 ports for given process. :param pid: process ID to inspect :returns: set of ports """ return set(_get_ports(pid)) print(get_ports(12345))
It works on Linux, macOS and Windows, and finds out all locally-bound TCPv4 ports for given process that are in LISTEN state. It also skips all kinds of host/port/username reverse look-ups to make it faster, and does not require elevated privileges.
So, finally, the idea is to just let Appium (or anything else) start on 0.0.0.0:0
, it will bind itself to the first available port, as provided by OS, and then inspect what ports is it now listening on. No race conditions.