Add `MsgpackTIPCStream`, the `AF_TIPC` `MsgTransport`
Wire `.connect_to()` (dial by service name), `.connected()` and `.get_stream_addrs()` on top of `MsgpackTransport` so `trio.SocketStream` + the existing `<I`-prefix framing carry `msgpack` msgs over TIPC unchanged. XXX both ends of a connected TIPC sock answer `TIPC_ADDR_ID` port-ids and a port-id carries NO service name, so, - the *dialling* side re-asserts the name it actually dialled over `._raddr` (same move as `MsgpackUDSStream`s peer-pid re-assign), - the *accepting* side keeps a `TIPC_NAME_UNKNOWN` sentinel plus the observed `(node, ref)`. It doesn't need more — the `Aid` from `._do_handshake()` already carries the peer's logical identity. Also normalize dial failures: TIPC answers an unpublished-name lookup with `EHOSTUNREACH`, which python maps to a **bare** `OSError` and NOT a `ConnectionError` subtype the way `ECONNREFUSED` maps to `ConnectionRefusedError`. The discovery-ping path needs the `ConnectionError` shape, so the `_reraise_as_connerr()` wrap is load-bearing, not polish. XXX ALSO tolerate a dead peer in `.get_stream_addrs()`! Unlike tcp/uds — where the kernel keeps answering the peer addr until *we* close — TIPC answers `ENOTCONN` once the peer is gone. Since `MsgpackTransport.__init__()` calls `.get_stream_addrs()` (via `Channel.from_stream()`) BEFORE the handshake, an unguarded `OSError` there escapes `handle_stream_from_peer()`s handshake tolerance (contract §4) and tears down the WHOLE actor. Any connect-then-drop peer — a port scan, a liveness probe, a cancelled dial — was a remote actor-kill. A dead peer must cost us an addr, not the runtime. Deats, - `TIPC_IMPORTANCE` exposed as a `.connect_to()` kwarg — TIPC can rank a conn's traffic under congestion, which no other backend can do. Defaulted to the kernel default for now; wiring the parent<->child chan to `HIGH` is a follow-up. - `TIPC_DEST_DROPPABLE = 0` so undeliverable msgs surface as errors instead of being silently dropped. (this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))wkt/pr493_review
parent
e3089ba356
commit
8c0ae140cd
|
|
@ -12,20 +12,27 @@ from socket import (
|
||||||
SOCK_STREAM,
|
SOCK_STREAM,
|
||||||
SOL_SOCKET,
|
SOL_SOCKET,
|
||||||
SO_ACCEPTCONN,
|
SO_ACCEPTCONN,
|
||||||
|
SOL_TIPC,
|
||||||
)
|
)
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
|
from tractor.msg.types import Aid
|
||||||
from tractor.ipc import _tipc
|
from tractor.ipc import _tipc
|
||||||
from tractor.ipc._tipc import (
|
from tractor.ipc._tipc import (
|
||||||
AF_TIPC,
|
AF_TIPC,
|
||||||
TIPC_ADDR_ID,
|
TIPC_ADDR_ID,
|
||||||
TIPC_ADDR_NAME,
|
TIPC_ADDR_NAME,
|
||||||
TIPC_CLUSTER_SCOPE,
|
TIPC_CLUSTER_SCOPE,
|
||||||
|
TIPC_DEST_DROPPABLE,
|
||||||
|
TIPC_HIGH_IMPORTANCE,
|
||||||
|
TIPC_IMPORTANCE,
|
||||||
|
TIPC_NAME_UNKNOWN,
|
||||||
TIPC_NODE_SCOPE,
|
TIPC_NODE_SCOPE,
|
||||||
TIPC_ZONE_SCOPE,
|
TIPC_ZONE_SCOPE,
|
||||||
TRACTOR_STYPE,
|
TRACTOR_STYPE,
|
||||||
|
MsgpackTIPCStream,
|
||||||
TIPCAddress,
|
TIPCAddress,
|
||||||
instance_from_seed,
|
instance_from_seed,
|
||||||
is_tipc_available,
|
is_tipc_available,
|
||||||
|
|
@ -333,6 +340,222 @@ def test_getsockname_is_a_port_id_not_the_bound_name():
|
||||||
trio.run(main)
|
trio.run(main)
|
||||||
|
|
||||||
|
|
||||||
|
@requires_tipc
|
||||||
|
def test_msgpack_roundtrip_over_service_name():
|
||||||
|
'''
|
||||||
|
Two `trio` tasks in ONE proc exchange `msgpack`-framed msgs
|
||||||
|
over a TIPC service name — no `tractor` runtime involved.
|
||||||
|
|
||||||
|
Also pins the `(laddr, raddr)` story of plan 01 §3.4a: the
|
||||||
|
dialling side knows the name it dialled, the accepting side
|
||||||
|
only ever learns a port-id.
|
||||||
|
|
||||||
|
'''
|
||||||
|
async def main():
|
||||||
|
addr: TIPCAddress = TIPCAddress.get_random()
|
||||||
|
lstnr = await start_listener(addr=addr)
|
||||||
|
|
||||||
|
ping = Aid(name='doggy', uuid='abc123', pid=1)
|
||||||
|
pong = Aid(name='kitty', uuid='def456', pid=2)
|
||||||
|
srv_got: list = []
|
||||||
|
|
||||||
|
async def _serve():
|
||||||
|
stream = await lstnr.accept()
|
||||||
|
tpt = MsgpackTIPCStream(stream)
|
||||||
|
|
||||||
|
# accepting side can NOT know the peer's service name
|
||||||
|
assert not tpt.raddr.is_valid
|
||||||
|
assert tpt.raddr._instance == TIPC_NAME_UNKNOWN
|
||||||
|
assert tpt.raddr.maybe_ref is not None
|
||||||
|
|
||||||
|
srv_got.append(await tpt.recv())
|
||||||
|
await tpt.send(pong)
|
||||||
|
await stream.aclose()
|
||||||
|
|
||||||
|
async with trio.open_nursery() as tn:
|
||||||
|
tn.start_soon(_serve)
|
||||||
|
await trio.sleep(0.05)
|
||||||
|
|
||||||
|
cli: MsgpackTIPCStream = await MsgpackTIPCStream.connect_to(
|
||||||
|
destaddr=addr,
|
||||||
|
)
|
||||||
|
assert cli.connected()
|
||||||
|
|
||||||
|
# dialling side DOES know the name, and it round-trips
|
||||||
|
assert cli.raddr.is_valid
|
||||||
|
assert cli.raddr.unwrap() == addr.unwrap()
|
||||||
|
assert cli.raddr.maybe_ref is not None
|
||||||
|
|
||||||
|
await cli.send(ping)
|
||||||
|
assert await cli.recv() == pong
|
||||||
|
await cli.stream.aclose()
|
||||||
|
|
||||||
|
assert srv_got == [ping]
|
||||||
|
lstnr.socket.close()
|
||||||
|
|
||||||
|
trio.run(main)
|
||||||
|
|
||||||
|
|
||||||
|
@requires_tipc
|
||||||
|
def test_dial_unpublished_name_is_connerr():
|
||||||
|
'''
|
||||||
|
Contract §4: a dead/absent peer must normalize to
|
||||||
|
`ConnectionError` — the discovery-ping path depends on it.
|
||||||
|
|
||||||
|
XXX TIPC answers an unpublished-name lookup with
|
||||||
|
`EHOSTUNREACH`, which python maps to a **bare** `OSError` (NOT
|
||||||
|
a `ConnectionError` subtype the way `ECONNREFUSED` maps to
|
||||||
|
`ConnectionRefusedError`), so the normalization is load-bearing
|
||||||
|
rather than cosmetic.
|
||||||
|
|
||||||
|
'''
|
||||||
|
async def main():
|
||||||
|
# nothing has ever `.bind()`ed this one
|
||||||
|
nowhere = TIPCAddress(
|
||||||
|
_stype=TRACTOR_STYPE,
|
||||||
|
_instance=0xDEADBEEF,
|
||||||
|
)
|
||||||
|
with trio.fail_after(5):
|
||||||
|
await MsgpackTIPCStream.connect_to(destaddr=nowhere)
|
||||||
|
|
||||||
|
with pytest.raises(ConnectionError) as excinfo:
|
||||||
|
trio.run(main)
|
||||||
|
|
||||||
|
src_exc = excinfo.value.__cause__
|
||||||
|
assert src_exc.errno == errno.EHOSTUNREACH
|
||||||
|
assert 'No TIPC publisher' in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
|
@requires_tipc
|
||||||
|
def test_importance_sockopt_roundtrips():
|
||||||
|
'''
|
||||||
|
The `TIPC_IMPORTANCE` QoS knob (plan 01 §3.3) is settable and
|
||||||
|
readable back — TIPC can rank a conn's traffic under
|
||||||
|
congestion, which no other backend can do.
|
||||||
|
|
||||||
|
'''
|
||||||
|
async def main():
|
||||||
|
addr: TIPCAddress = TIPCAddress.get_random()
|
||||||
|
lstnr = await start_listener(addr=addr)
|
||||||
|
|
||||||
|
# XXX hold the accepted conn open for the duration; a
|
||||||
|
# peer that closes first makes `getpeername()` (called
|
||||||
|
# from `MsgpackTransport.__init__`) raise `ENOTCONN`.
|
||||||
|
done = trio.Event()
|
||||||
|
|
||||||
|
async def _accept():
|
||||||
|
stream = await lstnr.accept()
|
||||||
|
await done.wait()
|
||||||
|
await stream.aclose()
|
||||||
|
|
||||||
|
async with trio.open_nursery() as tn:
|
||||||
|
tn.start_soon(_accept)
|
||||||
|
await trio.sleep(0.05)
|
||||||
|
cli = await MsgpackTIPCStream.connect_to(
|
||||||
|
destaddr=addr,
|
||||||
|
importance=TIPC_HIGH_IMPORTANCE,
|
||||||
|
)
|
||||||
|
sock = cli.stream.socket
|
||||||
|
assert sock.getsockopt(
|
||||||
|
SOL_TIPC,
|
||||||
|
TIPC_IMPORTANCE,
|
||||||
|
) == TIPC_HIGH_IMPORTANCE
|
||||||
|
assert sock.getsockopt(
|
||||||
|
SOL_TIPC,
|
||||||
|
TIPC_DEST_DROPPABLE,
|
||||||
|
) == 0
|
||||||
|
await cli.stream.aclose()
|
||||||
|
done.set()
|
||||||
|
|
||||||
|
lstnr.socket.close()
|
||||||
|
|
||||||
|
trio.run(main)
|
||||||
|
|
||||||
|
|
||||||
|
@requires_tipc
|
||||||
|
def test_dropped_peer_does_not_kill_the_listener():
|
||||||
|
'''
|
||||||
|
A peer that connects then drops BEFORE we read must cost us
|
||||||
|
an addr, not the runtime.
|
||||||
|
|
||||||
|
XXX unlike tcp/uds — where the kernel keeps answering the
|
||||||
|
peer addr until *we* close — TIPC answers `ENOTCONN` on
|
||||||
|
`getpeername()` once the peer is gone. Since
|
||||||
|
`MsgpackTransport.__init__()` calls `.get_stream_addrs()`
|
||||||
|
(via `Channel.from_stream()`) BEFORE the handshake, an
|
||||||
|
unguarded `OSError` there escapes
|
||||||
|
`handle_stream_from_peer()`s handshake tolerance (contract
|
||||||
|
§4) and tears down the whole actor.
|
||||||
|
|
||||||
|
Real-world triggers: a port scan, a liveness probe (our own
|
||||||
|
`tests/discovery/conftest.py::daemon` readiness poll does
|
||||||
|
exactly this!), or a cancelled dial.
|
||||||
|
|
||||||
|
'''
|
||||||
|
async def main():
|
||||||
|
addr: TIPCAddress = TIPCAddress.get_random()
|
||||||
|
lstnr = await start_listener(addr=addr)
|
||||||
|
|
||||||
|
tpts: list = []
|
||||||
|
|
||||||
|
async def _accept():
|
||||||
|
stream = await lstnr.accept()
|
||||||
|
# MUST NOT raise even though the peer is already gone
|
||||||
|
tpts.append(MsgpackTIPCStream(stream))
|
||||||
|
|
||||||
|
async with trio.open_nursery() as tn:
|
||||||
|
tn.start_soon(_accept)
|
||||||
|
await trio.sleep(0.05)
|
||||||
|
|
||||||
|
# connect-then-immediately-drop
|
||||||
|
sock = _tipc.trio_socket.socket(AF_TIPC, SOCK_STREAM)
|
||||||
|
await sock.connect((
|
||||||
|
TIPC_ADDR_NAME,
|
||||||
|
addr._stype,
|
||||||
|
addr._instance,
|
||||||
|
0,
|
||||||
|
addr._scope,
|
||||||
|
))
|
||||||
|
sock.close()
|
||||||
|
await trio.sleep(0.2)
|
||||||
|
|
||||||
|
# NOTE the assertion that matters is simply that
|
||||||
|
# `MsgpackTIPCStream()` above did NOT raise; whether
|
||||||
|
# `getpeername()` still answers is a kernel-side race on
|
||||||
|
# the disconnect indication, so don't pin `.maybe_ref`.
|
||||||
|
assert len(tpts) == 1
|
||||||
|
raddr: TIPCAddress = tpts[0].raddr
|
||||||
|
assert not raddr.is_valid
|
||||||
|
# ..and it still reprs cleanly for the con-status logs
|
||||||
|
assert 'unknown-service' in repr(raddr)
|
||||||
|
|
||||||
|
lstnr.socket.close()
|
||||||
|
|
||||||
|
trio.run(main)
|
||||||
|
|
||||||
|
|
||||||
|
def test_observed_addr_tolerates_a_dead_peer():
|
||||||
|
'''
|
||||||
|
The deterministic half of the above: `_maybe_sockaddr()`
|
||||||
|
swallows the `ENOTCONN` and `_observed_addr()` still yields
|
||||||
|
a usable (name-less, port-id-less) addr.
|
||||||
|
|
||||||
|
'''
|
||||||
|
def _enotconn():
|
||||||
|
raise OSError(
|
||||||
|
errno.ENOTCONN,
|
||||||
|
'Transport endpoint is not connected',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert _tipc._maybe_sockaddr(_enotconn) is None
|
||||||
|
|
||||||
|
addr: TIPCAddress = _tipc._observed_addr(None)
|
||||||
|
assert not addr.is_valid
|
||||||
|
assert addr.maybe_node is None
|
||||||
|
assert addr.maybe_ref is None
|
||||||
|
assert 'unknown-service' in repr(addr)
|
||||||
|
|
||||||
|
|
||||||
@requires_tipc
|
@requires_tipc
|
||||||
def test_duplicate_name_bind_does_not_raise():
|
def test_duplicate_name_bind_does_not_raise():
|
||||||
'''
|
'''
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ import os
|
||||||
import socket
|
import socket
|
||||||
from socket import SOCK_STREAM
|
from socket import SOCK_STREAM
|
||||||
from typing import (
|
from typing import (
|
||||||
|
Callable,
|
||||||
ClassVar,
|
ClassVar,
|
||||||
Type,
|
Type,
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
|
|
@ -60,12 +61,19 @@ from typing import (
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import msgspec
|
import msgspec
|
||||||
|
import trio
|
||||||
from trio import (
|
from trio import (
|
||||||
socket as trio_socket,
|
socket as trio_socket,
|
||||||
SocketListener,
|
SocketListener,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from multiaddr import Multiaddr
|
||||||
|
from tractor.msg import MsgCodec
|
||||||
from tractor.log import get_logger
|
from tractor.log import get_logger
|
||||||
|
from tractor.discovery._multiaddr import mk_maddr
|
||||||
|
from tractor.ipc._transport import (
|
||||||
|
MsgpackTransport,
|
||||||
|
)
|
||||||
from tractor.runtime._state import (
|
from tractor.runtime._state import (
|
||||||
current_actor,
|
current_actor,
|
||||||
is_root_process,
|
is_root_process,
|
||||||
|
|
@ -94,6 +102,10 @@ try:
|
||||||
TIPC_ADDR_NAME,
|
TIPC_ADDR_NAME,
|
||||||
TIPC_ADDR_NAMESEQ,
|
TIPC_ADDR_NAMESEQ,
|
||||||
TIPC_CLUSTER_SCOPE,
|
TIPC_CLUSTER_SCOPE,
|
||||||
|
TIPC_DEST_DROPPABLE,
|
||||||
|
TIPC_HIGH_IMPORTANCE,
|
||||||
|
TIPC_IMPORTANCE,
|
||||||
|
TIPC_LOW_IMPORTANCE,
|
||||||
TIPC_NODE_SCOPE,
|
TIPC_NODE_SCOPE,
|
||||||
TIPC_ZONE_SCOPE,
|
TIPC_ZONE_SCOPE,
|
||||||
)
|
)
|
||||||
|
|
@ -106,6 +118,10 @@ except ImportError:
|
||||||
TIPC_ZONE_SCOPE: int = 1
|
TIPC_ZONE_SCOPE: int = 1
|
||||||
TIPC_CLUSTER_SCOPE: int = 2
|
TIPC_CLUSTER_SCOPE: int = 2
|
||||||
TIPC_NODE_SCOPE: int = 3
|
TIPC_NODE_SCOPE: int = 3
|
||||||
|
TIPC_LOW_IMPORTANCE: int = 0
|
||||||
|
TIPC_HIGH_IMPORTANCE: int = 2
|
||||||
|
TIPC_IMPORTANCE: int = 127
|
||||||
|
TIPC_DEST_DROPPABLE: int = 129
|
||||||
|
|
||||||
|
|
||||||
# `tractor`'s reserved TIPC service-class ("type"), spelling out
|
# `tractor`'s reserved TIPC service-class ("type"), spelling out
|
||||||
|
|
@ -124,9 +140,19 @@ _tipc_reserved_stypes: range = range(0, 64)
|
||||||
|
|
||||||
# sentinel for "this addr was *observed* off a `TIPC_ADDR_ID`, so
|
# sentinel for "this addr was *observed* off a `TIPC_ADDR_ID`, so
|
||||||
# the peer's service-name is unknowable from the socket alone".
|
# the peer's service-name is unknowable from the socket alone".
|
||||||
# See plan 01 §3.4.
|
# See `MsgpackTIPCStream.get_stream_addrs()` and plan 01 §3.4.
|
||||||
TIPC_NAME_UNKNOWN: int = -1
|
TIPC_NAME_UNKNOWN: int = -1
|
||||||
|
|
||||||
|
# XXX, the kernel default (`TIPC_LOW_IMPORTANCE`), i.e. today this
|
||||||
|
# is a no-op knob preserving stock behaviour.
|
||||||
|
#
|
||||||
|
# ?TODO, TIPC can rank a connection's traffic under congestion —
|
||||||
|
# something no other backend can do — so the parent<->child
|
||||||
|
# *supervision* chan deserves `TIPC_HIGH_IMPORTANCE` while bulk app
|
||||||
|
# streams stay low. Wiring `_runtime.py`'s parent-chan path to pass
|
||||||
|
# it is deliberately a follow-up; see plan 01 §3.3 + §10.
|
||||||
|
TRACTOR_DEF_IMPORTANCE: int = TIPC_LOW_IMPORTANCE
|
||||||
|
|
||||||
_scope_names: dict[int, str] = {
|
_scope_names: dict[int, str] = {
|
||||||
TIPC_ZONE_SCOPE: 'zone',
|
TIPC_ZONE_SCOPE: 'zone',
|
||||||
TIPC_CLUSTER_SCOPE: 'cluster',
|
TIPC_CLUSTER_SCOPE: 'cluster',
|
||||||
|
|
@ -548,3 +574,204 @@ async def start_listener(
|
||||||
# entry to unlink and the kernel withdraws the published name on
|
# entry to unlink and the kernel withdraws the published name on
|
||||||
# socket close. Per contract §1.2 absence means "closing is
|
# socket close. Per contract §1.2 absence means "closing is
|
||||||
# implicit".
|
# implicit".
|
||||||
|
|
||||||
|
|
||||||
|
@cm
|
||||||
|
def _close_on_error(sock):
|
||||||
|
'''
|
||||||
|
Close `sock` if the wrapped block raises.
|
||||||
|
|
||||||
|
Equivalent to `trio._highlevel_open_unix_stream.close_on_error`
|
||||||
|
but inlined so this (linux-cluster) backend doesn't import a
|
||||||
|
*unix-domain* private module.
|
||||||
|
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
yield sock
|
||||||
|
except BaseException:
|
||||||
|
sock.close()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
class MsgpackTIPCStream(MsgpackTransport):
|
||||||
|
'''
|
||||||
|
A `trio.SocketStream` around an `AF_TIPC` service-name
|
||||||
|
connection delivering `msgpack` encoded msgs via the `msgspec`
|
||||||
|
codec lib.
|
||||||
|
|
||||||
|
'''
|
||||||
|
address_type = TIPCAddress
|
||||||
|
layer_key: int = 4
|
||||||
|
|
||||||
|
@property
|
||||||
|
def maddr(self) -> Multiaddr|str:
|
||||||
|
if not self.raddr:
|
||||||
|
return '<unknown-peer>'
|
||||||
|
|
||||||
|
return mk_maddr(self.raddr)
|
||||||
|
|
||||||
|
def connected(self) -> bool:
|
||||||
|
return self.stream.socket.fileno() != -1
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def connect_to(
|
||||||
|
cls,
|
||||||
|
destaddr: TIPCAddress,
|
||||||
|
prefix_size: int = 4,
|
||||||
|
codec: MsgCodec|None = None,
|
||||||
|
importance: int = TRACTOR_DEF_IMPORTANCE,
|
||||||
|
**kwargs,
|
||||||
|
) -> MsgpackTIPCStream:
|
||||||
|
'''
|
||||||
|
Dial `destaddr` **by service name**.
|
||||||
|
|
||||||
|
NOTE, the `.connect()` here *is* the discovery lookup — the
|
||||||
|
kernel resolves the published name-table entry for us, so
|
||||||
|
there's no registrar hop on this path.
|
||||||
|
|
||||||
|
'''
|
||||||
|
sock = trio_socket.socket(
|
||||||
|
AF_TIPC,
|
||||||
|
SOCK_STREAM,
|
||||||
|
)
|
||||||
|
with _close_on_error(sock):
|
||||||
|
sock.setsockopt(
|
||||||
|
SOL_TIPC,
|
||||||
|
TIPC_IMPORTANCE,
|
||||||
|
importance,
|
||||||
|
)
|
||||||
|
# NOTE, surface undeliverable msgs as errors rather
|
||||||
|
# than let the kernel silently drop them.
|
||||||
|
sock.setsockopt(
|
||||||
|
SOL_TIPC,
|
||||||
|
TIPC_DEST_DROPPABLE,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
with _reraise_as_connerr(
|
||||||
|
src_excs=(OSError,),
|
||||||
|
addr=destaddr,
|
||||||
|
):
|
||||||
|
await sock.connect((
|
||||||
|
TIPC_ADDR_NAME,
|
||||||
|
destaddr._stype,
|
||||||
|
destaddr._instance,
|
||||||
|
0, # domain: 0 == "anywhere in scope"
|
||||||
|
destaddr._scope,
|
||||||
|
))
|
||||||
|
|
||||||
|
tpt_stream = MsgpackTIPCStream(
|
||||||
|
trio.SocketStream(sock),
|
||||||
|
prefix_size=prefix_size,
|
||||||
|
codec=codec,
|
||||||
|
)
|
||||||
|
# XXX, the dialling side is the ONLY side that knows the
|
||||||
|
# peer's *service name* (a port-id can't be reversed into
|
||||||
|
# one), so re-assert it over the observed-only `._raddr`
|
||||||
|
# that `.get_stream_addrs()` just derived.
|
||||||
|
#
|
||||||
|
# Same move as `MsgpackUDSStream.connect_to()`s peer-pid
|
||||||
|
# re-assign.
|
||||||
|
tpt_stream._raddr = destaddr.with_port_id(
|
||||||
|
*_port_id(sock.getpeername()),
|
||||||
|
)
|
||||||
|
return tpt_stream
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_stream_addrs(
|
||||||
|
cls,
|
||||||
|
stream: trio.SocketStream,
|
||||||
|
) -> tuple[
|
||||||
|
TIPCAddress,
|
||||||
|
TIPCAddress,
|
||||||
|
]:
|
||||||
|
'''
|
||||||
|
Derive `(laddr, raddr)` from a connected TIPC socket.
|
||||||
|
|
||||||
|
XXX, BOTH ends answer `TIPC_ADDR_ID` port-ids and a port-id
|
||||||
|
carries NO service-name, so neither addr is dialable here;
|
||||||
|
they're name-`TIPC_NAME_UNKNOWN` and carry only the
|
||||||
|
observed `(node, ref)`.
|
||||||
|
|
||||||
|
That's fine and deliberate (plan 01 §3.4a):
|
||||||
|
|
||||||
|
- the *dialling* side overrides `._raddr` with the name it
|
||||||
|
actually dialled (see `.connect_to()`),
|
||||||
|
- the *accepting* side genuinely cannot know the peer's
|
||||||
|
name from the socket — but it doesn't need to, since the
|
||||||
|
`Aid` from `Channel._do_handshake()` already carries the
|
||||||
|
peer's logical identity.
|
||||||
|
|
||||||
|
'''
|
||||||
|
sock = stream.socket
|
||||||
|
return (
|
||||||
|
_observed_addr(_maybe_sockaddr(sock.getsockname)),
|
||||||
|
_observed_addr(_maybe_sockaddr(sock.getpeername)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _maybe_sockaddr(
|
||||||
|
getter: Callable[[], tuple],
|
||||||
|
) -> tuple|None:
|
||||||
|
'''
|
||||||
|
Call a `sock.getsockname`/`.getpeername` tolerantly.
|
||||||
|
|
||||||
|
XXX REQUIRED for TIPC: unlike tcp/uds — where the kernel keeps
|
||||||
|
answering the peer addr until *we* close — a TIPC socket whose
|
||||||
|
peer has already gone answers `ENOTCONN`. That happens for any
|
||||||
|
connect-then-immediately-drop peer: a port scan, a liveness
|
||||||
|
probe, a cancelled dial.
|
||||||
|
|
||||||
|
Since `MsgpackTransport.__init__()` calls `.get_stream_addrs()`
|
||||||
|
(via `Channel.from_stream()`) BEFORE the handshake, letting the
|
||||||
|
`OSError` fly would escape `handle_stream_from_peer()`s
|
||||||
|
handshake tolerance (contract §4) and tear down the whole
|
||||||
|
actor. A dead peer must cost us an addr, not the runtime.
|
||||||
|
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
return getter()
|
||||||
|
except OSError as oserr:
|
||||||
|
log.transport(
|
||||||
|
f'TIPC peer already gone, no port-id available\n'
|
||||||
|
f'from src: {oserr!r}\n'
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _port_id(
|
||||||
|
sockaddr: tuple[int, int, int, int, int],
|
||||||
|
) -> tuple[int, int]:
|
||||||
|
'''
|
||||||
|
Unpack the `(node, ref)` of a `TIPC_ADDR_ID` 5-tuple as
|
||||||
|
delivered by `getsockname()`/`getpeername()`.
|
||||||
|
|
||||||
|
Layout is `(addrtype, node, ref, 0, scope)`; see
|
||||||
|
`makesockaddr()`s `AF_TIPC` case in CPython's `socketmodule.c`.
|
||||||
|
|
||||||
|
'''
|
||||||
|
_, node, ref, *_ = sockaddr
|
||||||
|
return (node, ref)
|
||||||
|
|
||||||
|
|
||||||
|
def _observed_addr(
|
||||||
|
sockaddr: tuple[int, int, int, int, int]|None,
|
||||||
|
) -> TIPCAddress:
|
||||||
|
'''
|
||||||
|
Wrap a `TIPC_ADDR_ID` port-id as a name-less `TIPCAddress`
|
||||||
|
usable for logging/`repr` only.
|
||||||
|
|
||||||
|
A `None` `sockaddr` (peer already gone, see
|
||||||
|
`_maybe_sockaddr()`) yields the same addr sans port-id.
|
||||||
|
|
||||||
|
'''
|
||||||
|
node: int|None = None
|
||||||
|
ref: int|None = None
|
||||||
|
if sockaddr is not None:
|
||||||
|
node, ref = _port_id(sockaddr)
|
||||||
|
|
||||||
|
return TIPCAddress(
|
||||||
|
_stype=TIPC_NAME_UNKNOWN,
|
||||||
|
_instance=TIPC_NAME_UNKNOWN,
|
||||||
|
maybe_node=node,
|
||||||
|
maybe_ref=ref,
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue