diff --git a/tests/ipc/test_server.py b/tests/ipc/test_server.py index b6d6b70d..3e929892 100644 --- a/tests/ipc/test_server.py +++ b/tests/ipc/test_server.py @@ -295,7 +295,10 @@ def test_ep_addr_reconciled_from_sockname( assert accept_addr[1] == 0 # ..and the ep learned the real one. assert ep.addr._port != 0 - assert ep.addr.unwrap() == tuple(sockname[:2]) + assert ep.addr.unwrap() == ( + 'tcp', + *sockname[:2], + ) case 'uds': # sock-file path is stable across the diff --git a/tractor/discovery/_addr.py b/tractor/discovery/_addr.py index ed080690..d0ca1861 100644 --- a/tractor/discovery/_addr.py +++ b/tractor/discovery/_addr.py @@ -113,6 +113,16 @@ UnwrappedAddress = TaggedAddress class Address(Protocol): proto_key: ClassVar[str] unwrapped_type: ClassVar[type] + # whether `.ipc._server.Endpoint.start_listener()` should + # reconcile a bound `.addr` against its listener's + # `socket.getsockname()`. + # + # XXX NOTE, that reconciliation exists ONLY to learn the + # kernel-*assigned* port from a `port=0` tcp bind; a backend + # whose `getsockname()` reports a categorically different thing + # than what was `.bind()`ed must opt out with `False`, else the + # ep's addr gets clobbered by an un-dialable one. + rebind_from_sockname: ClassVar[bool] # TODO, i feel like an `.is_bound()` is a better thing to # support? diff --git a/tractor/ipc/_server.py b/tractor/ipc/_server.py index f75bcae2..7b7a563c 100644 --- a/tractor/ipc/_server.py +++ b/tractor/ipc/_server.py @@ -684,7 +684,16 @@ class Endpoint(Struct): # NOTE, for handling the resolved non-0 port for # TCP/UDP network sockets. + # + # XXX, gated on the addr-type's opt-in since for some + # backends `getsockname()` does NOT answer "the addr you + # bound"; `tipc` reports a `TIPC_ADDR_ID` port-id instead + # of the published name-seq, so rebinding from it would + # replace a dialable service-name with an un-dialable + # (and un-reconstructable) port-id. if ( + self.addr.rebind_from_sockname + and (unwrapped := lstnr.socket.getsockname()) != self.addr.unwrap() diff --git a/tractor/ipc/_tcp.py b/tractor/ipc/_tcp.py index 40cb8e81..343d76f7 100644 --- a/tractor/ipc/_tcp.py +++ b/tractor/ipc/_tcp.py @@ -74,6 +74,10 @@ class TCPAddress( unwrapped_type: ClassVar[type] = tuple def_bindspace: ClassVar[str] = '127.0.0.1' + # XXX, REQUIRED here since a `port=0` bind means the kernel + # picks and `getsockname()` is the only way we learn it. + rebind_from_sockname: ClassVar[bool] = True + # ?TODO, actually validate ipv4/6 with stdlib's `ipaddress` @property def is_valid(self) -> bool: diff --git a/tractor/ipc/_uds.py b/tractor/ipc/_uds.py index cfba9519..c852ddef 100644 --- a/tractor/ipc/_uds.py +++ b/tractor/ipc/_uds.py @@ -151,6 +151,12 @@ class UDSAddress( unwrapped_type: ClassVar[type] = tuple def_bindspace: ClassVar[Path] = get_rt_dir() + # NOTE, `getsockname()` answers the sock-file path as a `str` + # which never `==` our 2-tuple `.unwrap()`, so the round-trip + # always fires; it's a no-op modulo `.maybe_pid` and is kept + # `True` to preserve pre-existing behaviour exactly. + rebind_from_sockname: ClassVar[bool] = True + @property def bindspace(self) -> Path: '''