@ajksunkang-aios/kgraph-linux-x64 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/kgraph-launcher +15 -3
- package/lib/kgraph/scripts/build-bundle.sh +17 -4
- package/lib/site-packages/outcome/__init__.py +20 -0
- package/lib/site-packages/outcome/_impl.py +239 -0
- package/lib/site-packages/outcome/_util.py +33 -0
- package/lib/site-packages/outcome/_version.py +7 -0
- package/lib/site-packages/outcome/py.typed +0 -0
- package/lib/site-packages/outcome-1.3.0.post0.dist-info/INSTALLER +1 -0
- package/lib/site-packages/outcome-1.3.0.post0.dist-info/LICENSE +3 -0
- package/lib/site-packages/outcome-1.3.0.post0.dist-info/LICENSE.APACHE2 +202 -0
- package/lib/site-packages/outcome-1.3.0.post0.dist-info/LICENSE.MIT +20 -0
- package/lib/site-packages/outcome-1.3.0.post0.dist-info/METADATA +63 -0
- package/lib/site-packages/outcome-1.3.0.post0.dist-info/RECORD +13 -0
- package/lib/site-packages/outcome-1.3.0.post0.dist-info/WHEEL +6 -0
- package/lib/site-packages/outcome-1.3.0.post0.dist-info/top_level.txt +1 -0
- package/lib/site-packages/sniffio/__init__.py +17 -0
- package/lib/site-packages/sniffio/_impl.py +95 -0
- package/lib/site-packages/sniffio/_tests/__init__.py +0 -0
- package/lib/site-packages/sniffio/_tests/test_sniffio.py +84 -0
- package/lib/site-packages/sniffio/_version.py +3 -0
- package/lib/site-packages/sniffio/py.typed +0 -0
- package/lib/site-packages/sniffio-1.3.1.dist-info/INSTALLER +1 -0
- package/lib/site-packages/sniffio-1.3.1.dist-info/LICENSE +3 -0
- package/lib/site-packages/sniffio-1.3.1.dist-info/LICENSE.APACHE2 +202 -0
- package/lib/site-packages/sniffio-1.3.1.dist-info/LICENSE.MIT +20 -0
- package/lib/site-packages/sniffio-1.3.1.dist-info/METADATA +104 -0
- package/lib/site-packages/sniffio-1.3.1.dist-info/RECORD +14 -0
- package/lib/site-packages/sniffio-1.3.1.dist-info/WHEEL +5 -0
- package/lib/site-packages/sniffio-1.3.1.dist-info/top_level.txt +1 -0
- package/lib/site-packages/sortedcontainers/__init__.py +74 -0
- package/lib/site-packages/sortedcontainers/sorteddict.py +812 -0
- package/lib/site-packages/sortedcontainers/sortedlist.py +2646 -0
- package/lib/site-packages/sortedcontainers/sortedset.py +733 -0
- package/lib/site-packages/sortedcontainers-2.4.0.dist-info/INSTALLER +1 -0
- package/lib/site-packages/sortedcontainers-2.4.0.dist-info/LICENSE +13 -0
- package/lib/site-packages/sortedcontainers-2.4.0.dist-info/METADATA +264 -0
- package/lib/site-packages/sortedcontainers-2.4.0.dist-info/RECORD +10 -0
- package/lib/site-packages/sortedcontainers-2.4.0.dist-info/WHEEL +6 -0
- package/lib/site-packages/sortedcontainers-2.4.0.dist-info/top_level.txt +1 -0
- package/lib/site-packages/trio/__init__.py +133 -0
- package/lib/site-packages/trio/__main__.py +3 -0
- package/lib/site-packages/trio/_abc.py +714 -0
- package/lib/site-packages/trio/_channel.py +610 -0
- package/lib/site-packages/trio/_core/__init__.py +94 -0
- package/lib/site-packages/trio/_core/_asyncgens.py +243 -0
- package/lib/site-packages/trio/_core/_concat_tb.py +26 -0
- package/lib/site-packages/trio/_core/_entry_queue.py +223 -0
- package/lib/site-packages/trio/_core/_exceptions.py +169 -0
- package/lib/site-packages/trio/_core/_generated_instrumentation.py +50 -0
- package/lib/site-packages/trio/_core/_generated_io_epoll.py +98 -0
- package/lib/site-packages/trio/_core/_generated_io_kqueue.py +153 -0
- package/lib/site-packages/trio/_core/_generated_io_windows.py +204 -0
- package/lib/site-packages/trio/_core/_generated_run.py +269 -0
- package/lib/site-packages/trio/_core/_generated_windows_ffi.py +10 -0
- package/lib/site-packages/trio/_core/_instrumentation.py +117 -0
- package/lib/site-packages/trio/_core/_io_common.py +31 -0
- package/lib/site-packages/trio/_core/_io_epoll.py +385 -0
- package/lib/site-packages/trio/_core/_io_kqueue.py +292 -0
- package/lib/site-packages/trio/_core/_io_windows.py +1036 -0
- package/lib/site-packages/trio/_core/_ki.py +271 -0
- package/lib/site-packages/trio/_core/_local.py +104 -0
- package/lib/site-packages/trio/_core/_mock_clock.py +165 -0
- package/lib/site-packages/trio/_core/_parking_lot.py +317 -0
- package/lib/site-packages/trio/_core/_run.py +3148 -0
- package/lib/site-packages/trio/_core/_run_context.py +15 -0
- package/lib/site-packages/trio/_core/_tests/__init__.py +0 -0
- package/lib/site-packages/trio/_core/_tests/test_asyncgen.py +339 -0
- package/lib/site-packages/trio/_core/_tests/test_cancelled.py +222 -0
- package/lib/site-packages/trio/_core/_tests/test_exceptiongroup_gc.py +103 -0
- package/lib/site-packages/trio/_core/_tests/test_guest_mode.py +755 -0
- package/lib/site-packages/trio/_core/_tests/test_instrumentation.py +315 -0
- package/lib/site-packages/trio/_core/_tests/test_io.py +522 -0
- package/lib/site-packages/trio/_core/_tests/test_ki.py +703 -0
- package/lib/site-packages/trio/_core/_tests/test_local.py +118 -0
- package/lib/site-packages/trio/_core/_tests/test_mock_clock.py +193 -0
- package/lib/site-packages/trio/_core/_tests/test_parking_lot.py +389 -0
- package/lib/site-packages/trio/_core/_tests/test_run.py +3024 -0
- package/lib/site-packages/trio/_core/_tests/test_thread_cache.py +227 -0
- package/lib/site-packages/trio/_core/_tests/test_tutil.py +13 -0
- package/lib/site-packages/trio/_core/_tests/test_unbounded_queue.py +154 -0
- package/lib/site-packages/trio/_core/_tests/test_windows.py +305 -0
- package/lib/site-packages/trio/_core/_tests/tutil.py +117 -0
- package/lib/site-packages/trio/_core/_tests/type_tests/nursery_start.py +79 -0
- package/lib/site-packages/trio/_core/_tests/type_tests/run.py +51 -0
- package/lib/site-packages/trio/_core/_thread_cache.py +317 -0
- package/lib/site-packages/trio/_core/_traps.py +318 -0
- package/lib/site-packages/trio/_core/_unbounded_queue.py +163 -0
- package/lib/site-packages/trio/_core/_wakeup_socketpair.py +75 -0
- package/lib/site-packages/trio/_core/_windows_cffi.py +313 -0
- package/lib/site-packages/trio/_deprecate.py +171 -0
- package/lib/site-packages/trio/_dtls.py +1380 -0
- package/lib/site-packages/trio/_file_io.py +513 -0
- package/lib/site-packages/trio/_highlevel_generic.py +125 -0
- package/lib/site-packages/trio/_highlevel_open_tcp_listeners.py +251 -0
- package/lib/site-packages/trio/_highlevel_open_tcp_stream.py +397 -0
- package/lib/site-packages/trio/_highlevel_open_unix_stream.py +65 -0
- package/lib/site-packages/trio/_highlevel_serve_listeners.py +148 -0
- package/lib/site-packages/trio/_highlevel_socket.py +423 -0
- package/lib/site-packages/trio/_highlevel_ssl_helpers.py +180 -0
- package/lib/site-packages/trio/_path.py +289 -0
- package/lib/site-packages/trio/_repl.py +159 -0
- package/lib/site-packages/trio/_signals.py +185 -0
- package/lib/site-packages/trio/_socket.py +1326 -0
- package/lib/site-packages/trio/_ssl.py +964 -0
- package/lib/site-packages/trio/_subprocess.py +1178 -0
- package/lib/site-packages/trio/_subprocess_platform/__init__.py +123 -0
- package/lib/site-packages/trio/_subprocess_platform/kqueue.py +48 -0
- package/lib/site-packages/trio/_subprocess_platform/waitid.py +113 -0
- package/lib/site-packages/trio/_subprocess_platform/windows.py +11 -0
- package/lib/site-packages/trio/_sync.py +908 -0
- package/lib/site-packages/trio/_tests/__init__.py +0 -0
- package/lib/site-packages/trio/_tests/astrill-codesigning-cert.cer +0 -0
- package/lib/site-packages/trio/_tests/check_type_completeness.py +247 -0
- package/lib/site-packages/trio/_tests/module_with_deprecations.py +22 -0
- package/lib/site-packages/trio/_tests/pytest_plugin.py +54 -0
- package/lib/site-packages/trio/_tests/test_abc.py +72 -0
- package/lib/site-packages/trio/_tests/test_channel.py +750 -0
- package/lib/site-packages/trio/_tests/test_contextvars.py +56 -0
- package/lib/site-packages/trio/_tests/test_deprecate.py +277 -0
- package/lib/site-packages/trio/_tests/test_deprecate_strict_exception_groups_false.py +64 -0
- package/lib/site-packages/trio/_tests/test_dtls.py +950 -0
- package/lib/site-packages/trio/_tests/test_exports.py +626 -0
- package/lib/site-packages/trio/_tests/test_fakenet.py +317 -0
- package/lib/site-packages/trio/_tests/test_file_io.py +269 -0
- package/lib/site-packages/trio/_tests/test_highlevel_generic.py +98 -0
- package/lib/site-packages/trio/_tests/test_highlevel_open_tcp_listeners.py +419 -0
- package/lib/site-packages/trio/_tests/test_highlevel_open_tcp_stream.py +693 -0
- package/lib/site-packages/trio/_tests/test_highlevel_open_unix_stream.py +86 -0
- package/lib/site-packages/trio/_tests/test_highlevel_serve_listeners.py +186 -0
- package/lib/site-packages/trio/_tests/test_highlevel_socket.py +336 -0
- package/lib/site-packages/trio/_tests/test_highlevel_ssl_helpers.py +169 -0
- package/lib/site-packages/trio/_tests/test_path.py +279 -0
- package/lib/site-packages/trio/_tests/test_repl.py +428 -0
- package/lib/site-packages/trio/_tests/test_scheduler_determinism.py +47 -0
- package/lib/site-packages/trio/_tests/test_signals.py +186 -0
- package/lib/site-packages/trio/_tests/test_socket.py +1253 -0
- package/lib/site-packages/trio/_tests/test_ssl.py +1371 -0
- package/lib/site-packages/trio/_tests/test_subprocess.py +767 -0
- package/lib/site-packages/trio/_tests/test_sync.py +735 -0
- package/lib/site-packages/trio/_tests/test_testing.py +682 -0
- package/lib/site-packages/trio/_tests/test_testing_raisesgroup.py +1128 -0
- package/lib/site-packages/trio/_tests/test_threads.py +1173 -0
- package/lib/site-packages/trio/_tests/test_timeouts.py +281 -0
- package/lib/site-packages/trio/_tests/test_tracing.py +88 -0
- package/lib/site-packages/trio/_tests/test_trio.py +8 -0
- package/lib/site-packages/trio/_tests/test_unix_pipes.py +288 -0
- package/lib/site-packages/trio/_tests/test_util.py +349 -0
- package/lib/site-packages/trio/_tests/test_wait_for_object.py +225 -0
- package/lib/site-packages/trio/_tests/test_windows_pipes.py +112 -0
- package/lib/site-packages/trio/_tests/tools/__init__.py +0 -0
- package/lib/site-packages/trio/_tests/tools/test_gen_exports.py +179 -0
- package/lib/site-packages/trio/_tests/tools/test_mypy_annotate.py +140 -0
- package/lib/site-packages/trio/_tests/tools/test_sync_requirements.py +80 -0
- package/lib/site-packages/trio/_tests/type_tests/check_wraps.py +9 -0
- package/lib/site-packages/trio/_tests/type_tests/open_memory_channel.py +4 -0
- package/lib/site-packages/trio/_tests/type_tests/path.py +140 -0
- package/lib/site-packages/trio/_tests/type_tests/subprocesses.py +23 -0
- package/lib/site-packages/trio/_tests/type_tests/task_status.py +29 -0
- package/lib/site-packages/trio/_threads.py +610 -0
- package/lib/site-packages/trio/_timeouts.py +197 -0
- package/lib/site-packages/trio/_tools/__init__.py +0 -0
- package/lib/site-packages/trio/_tools/gen_exports.py +401 -0
- package/lib/site-packages/trio/_tools/mypy_annotate.py +126 -0
- package/lib/site-packages/trio/_tools/sync_requirements.py +98 -0
- package/lib/site-packages/trio/_tools/windows_ffi_build.py +220 -0
- package/lib/site-packages/trio/_unix_pipes.py +197 -0
- package/lib/site-packages/trio/_util.py +385 -0
- package/lib/site-packages/trio/_version.py +3 -0
- package/lib/site-packages/trio/_wait_for_object.py +67 -0
- package/lib/site-packages/trio/_windows_pipes.py +144 -0
- package/lib/site-packages/trio/abc.py +23 -0
- package/lib/site-packages/trio/from_thread.py +13 -0
- package/lib/site-packages/trio/lowlevel.py +95 -0
- package/lib/site-packages/trio/py.typed +0 -0
- package/lib/site-packages/trio/socket.py +602 -0
- package/lib/site-packages/trio/testing/__init__.py +58 -0
- package/lib/site-packages/trio/testing/_check_streams.py +570 -0
- package/lib/site-packages/trio/testing/_checkpoints.py +69 -0
- package/lib/site-packages/trio/testing/_fake_net.py +584 -0
- package/lib/site-packages/trio/testing/_memory_streams.py +633 -0
- package/lib/site-packages/trio/testing/_network.py +36 -0
- package/lib/site-packages/trio/testing/_raises_group.py +1015 -0
- package/lib/site-packages/trio/testing/_sequencer.py +87 -0
- package/lib/site-packages/trio/testing/_trio_test.py +50 -0
- package/lib/site-packages/trio/to_thread.py +4 -0
- package/lib/site-packages/trio-0.33.0.dist-info/INSTALLER +1 -0
- package/lib/site-packages/trio-0.33.0.dist-info/METADATA +186 -0
- package/lib/site-packages/trio-0.33.0.dist-info/RECORD +156 -0
- package/lib/site-packages/trio-0.33.0.dist-info/REQUESTED +0 -0
- package/lib/site-packages/trio-0.33.0.dist-info/WHEEL +5 -0
- package/lib/site-packages/trio-0.33.0.dist-info/entry_points.txt +2 -0
- package/lib/site-packages/trio-0.33.0.dist-info/licenses/LICENSE +3 -0
- package/lib/site-packages/trio-0.33.0.dist-info/licenses/LICENSE.APACHE2 +202 -0
- package/lib/site-packages/trio-0.33.0.dist-info/licenses/LICENSE.MIT +22 -0
- package/lib/site-packages/trio-0.33.0.dist-info/top_level.txt +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import errno
|
|
4
|
+
import sys
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import trio
|
|
8
|
+
from trio import TaskStatus
|
|
9
|
+
|
|
10
|
+
from . import socket as tsocket
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Awaitable, Callable
|
|
14
|
+
|
|
15
|
+
if sys.version_info < (3, 11):
|
|
16
|
+
from exceptiongroup import ExceptionGroup
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Default backlog size:
|
|
20
|
+
#
|
|
21
|
+
# Having the backlog too low can cause practical problems (a perfectly healthy
|
|
22
|
+
# service that starts failing to accept connections if they arrive in a
|
|
23
|
+
# burst).
|
|
24
|
+
#
|
|
25
|
+
# Having it too high doesn't really cause any problems. Like any buffer, you
|
|
26
|
+
# want backlog queue to be zero usually, and it won't save you if you're
|
|
27
|
+
# getting connection attempts faster than you can call accept() on an ongoing
|
|
28
|
+
# basis. But unlike other buffers, this one doesn't really provide any
|
|
29
|
+
# backpressure. If a connection gets stuck waiting in the backlog queue, then
|
|
30
|
+
# from the peer's point of view the connection succeeded but then their
|
|
31
|
+
# send/recv will stall until we get to it, possibly for a long time. OTOH if
|
|
32
|
+
# there isn't room in the backlog queue, then their connect stalls, possibly
|
|
33
|
+
# for a long time, which is pretty much the same thing.
|
|
34
|
+
#
|
|
35
|
+
# A large backlog can also use a bit more kernel memory, but this seems fairly
|
|
36
|
+
# negligible these days.
|
|
37
|
+
#
|
|
38
|
+
# So this suggests we should make the backlog as large as possible. This also
|
|
39
|
+
# matches what Golang does. However, they do it in a weird way, where they
|
|
40
|
+
# have a bunch of code to sniff out the configured upper limit for backlog on
|
|
41
|
+
# different operating systems. But on every system, passing in a too-large
|
|
42
|
+
# backlog just causes it to be silently truncated to the configured maximum,
|
|
43
|
+
# so this is unnecessary -- we can just pass in "infinity" and get the maximum
|
|
44
|
+
# that way. (Verified on Windows, Linux, macOS using
|
|
45
|
+
# https://github.com/python-trio/trio/wiki/notes-to-self#measure-listen-backlogpy
|
|
46
|
+
def _compute_backlog(backlog: int | None) -> int:
|
|
47
|
+
# Many systems (Linux, BSDs, ...) store the backlog in a uint16 and are
|
|
48
|
+
# missing overflow protection, so we apply our own overflow protection.
|
|
49
|
+
# https://github.com/golang/go/issues/5030
|
|
50
|
+
if not isinstance(backlog, int) and backlog is not None:
|
|
51
|
+
raise TypeError(f"backlog must be an int or None, not {backlog!r}")
|
|
52
|
+
if backlog is None:
|
|
53
|
+
return 0xFFFF
|
|
54
|
+
return min(backlog, 0xFFFF)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def open_tcp_listeners(
|
|
58
|
+
port: int,
|
|
59
|
+
*,
|
|
60
|
+
host: str | bytes | None = None,
|
|
61
|
+
backlog: int | None = None,
|
|
62
|
+
) -> list[trio.SocketListener]:
|
|
63
|
+
"""Create :class:`SocketListener` objects to listen for TCP connections.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
|
|
67
|
+
port (int): The port to listen on.
|
|
68
|
+
|
|
69
|
+
If you use 0 as your port, then the kernel will automatically pick
|
|
70
|
+
an arbitrary open port. But be careful: if you use this feature when
|
|
71
|
+
binding to multiple IP addresses, then each IP address will get its
|
|
72
|
+
own random port, and the returned listeners will probably be
|
|
73
|
+
listening on different ports. In particular, this will happen if you
|
|
74
|
+
use ``host=None`` – which is the default – because in this case
|
|
75
|
+
:func:`open_tcp_listeners` will bind to both the IPv4 wildcard
|
|
76
|
+
address (``0.0.0.0``) and also the IPv6 wildcard address (``::``).
|
|
77
|
+
|
|
78
|
+
host (str, bytes, or None): The local interface to bind to. This is
|
|
79
|
+
passed to :func:`~socket.getaddrinfo` with the ``AI_PASSIVE`` flag
|
|
80
|
+
set.
|
|
81
|
+
|
|
82
|
+
If you want to bind to the wildcard address on both IPv4 and IPv6,
|
|
83
|
+
in order to accept connections on all available interfaces, then
|
|
84
|
+
pass ``None``. This is the default.
|
|
85
|
+
|
|
86
|
+
If you have a specific interface you want to bind to, pass its IP
|
|
87
|
+
address or hostname here. If a hostname resolves to multiple IP
|
|
88
|
+
addresses, this function will open one listener on each of them.
|
|
89
|
+
|
|
90
|
+
If you want to use only IPv4, or only IPv6, but want to accept on
|
|
91
|
+
all interfaces, pass the family-specific wildcard address:
|
|
92
|
+
``"0.0.0.0"`` for IPv4-only and ``"::"`` for IPv6-only.
|
|
93
|
+
|
|
94
|
+
backlog (int or None): The listen backlog to use. If you leave this as
|
|
95
|
+
``None`` then Trio will pick a good default. (Currently: whatever
|
|
96
|
+
your system has configured as the maximum backlog.)
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
list of :class:`SocketListener`
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
:class:`TypeError` if invalid arguments.
|
|
103
|
+
|
|
104
|
+
"""
|
|
105
|
+
# getaddrinfo sometimes allows port=None, sometimes not (depending on
|
|
106
|
+
# whether host=None). And on some systems it treats "" as 0, others it
|
|
107
|
+
# doesn't:
|
|
108
|
+
# http://klickverbot.at/blog/2012/01/getaddrinfo-edge-case-behavior-on-windows-linux-and-osx/
|
|
109
|
+
if not isinstance(port, int):
|
|
110
|
+
raise TypeError(f"port must be an int not {port!r}")
|
|
111
|
+
|
|
112
|
+
computed_backlog = _compute_backlog(backlog)
|
|
113
|
+
|
|
114
|
+
addresses = await tsocket.getaddrinfo(
|
|
115
|
+
host,
|
|
116
|
+
port,
|
|
117
|
+
type=tsocket.SOCK_STREAM,
|
|
118
|
+
flags=tsocket.AI_PASSIVE,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
listeners = []
|
|
122
|
+
unsupported_address_families = []
|
|
123
|
+
try:
|
|
124
|
+
for family, type_, proto, _, sockaddr in addresses:
|
|
125
|
+
try:
|
|
126
|
+
sock = tsocket.socket(family, type_, proto)
|
|
127
|
+
except OSError as ex:
|
|
128
|
+
if ex.errno == errno.EAFNOSUPPORT:
|
|
129
|
+
# If a system only supports IPv4, or only IPv6, it
|
|
130
|
+
# is still likely that getaddrinfo will return
|
|
131
|
+
# both an IPv4 and an IPv6 address. As long as at
|
|
132
|
+
# least one of the returned addresses can be
|
|
133
|
+
# turned into a socket, we won't complain about a
|
|
134
|
+
# failure to create the other.
|
|
135
|
+
unsupported_address_families.append(ex)
|
|
136
|
+
continue
|
|
137
|
+
else:
|
|
138
|
+
raise
|
|
139
|
+
try:
|
|
140
|
+
# See https://github.com/python-trio/trio/issues/39
|
|
141
|
+
if sys.platform != "win32":
|
|
142
|
+
sock.setsockopt(tsocket.SOL_SOCKET, tsocket.SO_REUSEADDR, 1)
|
|
143
|
+
|
|
144
|
+
if family == tsocket.AF_INET6:
|
|
145
|
+
sock.setsockopt(tsocket.IPPROTO_IPV6, tsocket.IPV6_V6ONLY, 1)
|
|
146
|
+
|
|
147
|
+
await sock.bind(sockaddr)
|
|
148
|
+
sock.listen(computed_backlog)
|
|
149
|
+
|
|
150
|
+
listeners.append(trio.SocketListener(sock))
|
|
151
|
+
except:
|
|
152
|
+
sock.close()
|
|
153
|
+
raise
|
|
154
|
+
except:
|
|
155
|
+
for listener in listeners:
|
|
156
|
+
listener.socket.close()
|
|
157
|
+
raise
|
|
158
|
+
|
|
159
|
+
if unsupported_address_families and not listeners:
|
|
160
|
+
msg = (
|
|
161
|
+
"This system doesn't support any of the kinds of "
|
|
162
|
+
"socket that that address could use"
|
|
163
|
+
)
|
|
164
|
+
raise OSError(errno.EAFNOSUPPORT, msg) from ExceptionGroup(
|
|
165
|
+
msg,
|
|
166
|
+
unsupported_address_families,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return listeners
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
async def serve_tcp(
|
|
173
|
+
handler: Callable[[trio.SocketStream], Awaitable[object]],
|
|
174
|
+
port: int,
|
|
175
|
+
*,
|
|
176
|
+
host: str | bytes | None = None,
|
|
177
|
+
backlog: int | None = None,
|
|
178
|
+
handler_nursery: trio.Nursery | None = None,
|
|
179
|
+
task_status: TaskStatus[list[trio.SocketListener]] = trio.TASK_STATUS_IGNORED,
|
|
180
|
+
) -> None:
|
|
181
|
+
"""Listen for incoming TCP connections, and for each one start a task
|
|
182
|
+
running ``handler(stream)``.
|
|
183
|
+
|
|
184
|
+
This is a thin convenience wrapper around :func:`open_tcp_listeners` and
|
|
185
|
+
:func:`serve_listeners` – see them for full details.
|
|
186
|
+
|
|
187
|
+
.. warning::
|
|
188
|
+
|
|
189
|
+
If ``handler`` raises an exception, then this function doesn't do
|
|
190
|
+
anything special to catch it – so by default the exception will
|
|
191
|
+
propagate out and crash your server. If you don't want this, then catch
|
|
192
|
+
exceptions inside your ``handler``, or use a ``handler_nursery`` object
|
|
193
|
+
that responds to exceptions in some other way.
|
|
194
|
+
|
|
195
|
+
When used with ``nursery.start`` you get back the newly opened listeners.
|
|
196
|
+
So, for example, if you want to start a server in your test suite and then
|
|
197
|
+
connect to it to check that it's working properly, you can use something
|
|
198
|
+
like::
|
|
199
|
+
|
|
200
|
+
from trio import SocketListener, SocketStream
|
|
201
|
+
from trio.testing import open_stream_to_socket_listener
|
|
202
|
+
|
|
203
|
+
async with trio.open_nursery() as nursery:
|
|
204
|
+
listeners: list[SocketListener] = await nursery.start(serve_tcp, handler, 0)
|
|
205
|
+
client_stream: SocketStream = await open_stream_to_socket_listener(listeners[0])
|
|
206
|
+
|
|
207
|
+
# Then send and receive data on 'client_stream', for example:
|
|
208
|
+
await client_stream.send_all(b"GET / HTTP/1.0\\r\\n\\r\\n")
|
|
209
|
+
|
|
210
|
+
This avoids several common pitfalls:
|
|
211
|
+
|
|
212
|
+
1. It lets the kernel pick a random open port, so your test suite doesn't
|
|
213
|
+
depend on any particular port being open.
|
|
214
|
+
|
|
215
|
+
2. It waits for the server to be accepting connections on that port before
|
|
216
|
+
``start`` returns, so there's no race condition where the incoming
|
|
217
|
+
connection arrives before the server is ready.
|
|
218
|
+
|
|
219
|
+
3. It uses the Listener object to find out which port was picked, so it
|
|
220
|
+
can connect to the right place.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
handler: The handler to start for each incoming connection. Passed to
|
|
224
|
+
:func:`serve_listeners`.
|
|
225
|
+
|
|
226
|
+
port: The port to listen on. Use 0 to let the kernel pick an open port.
|
|
227
|
+
Passed to :func:`open_tcp_listeners`.
|
|
228
|
+
|
|
229
|
+
host (str, bytes, or None): The host interface to listen on; use
|
|
230
|
+
``None`` to bind to the wildcard address. Passed to
|
|
231
|
+
:func:`open_tcp_listeners`.
|
|
232
|
+
|
|
233
|
+
backlog: The listen backlog, or None to have a good default picked.
|
|
234
|
+
Passed to :func:`open_tcp_listeners`.
|
|
235
|
+
|
|
236
|
+
handler_nursery: The nursery to start handlers in, or None to use an
|
|
237
|
+
internal nursery. Passed to :func:`serve_listeners`.
|
|
238
|
+
|
|
239
|
+
task_status: This function can be used with ``nursery.start``.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
This function only returns when cancelled.
|
|
243
|
+
|
|
244
|
+
"""
|
|
245
|
+
listeners = await trio.open_tcp_listeners(port, host=host, backlog=backlog)
|
|
246
|
+
await trio.serve_listeners(
|
|
247
|
+
handler,
|
|
248
|
+
listeners,
|
|
249
|
+
handler_nursery=handler_nursery,
|
|
250
|
+
task_status=task_status,
|
|
251
|
+
)
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from contextlib import contextmanager, suppress
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
import trio
|
|
8
|
+
from trio.socket import SOCK_STREAM, SocketType, getaddrinfo, socket
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Generator, MutableSequence
|
|
12
|
+
from socket import AddressFamily, SocketKind
|
|
13
|
+
|
|
14
|
+
from trio._socket import AddressFormat
|
|
15
|
+
|
|
16
|
+
if sys.version_info < (3, 11):
|
|
17
|
+
from exceptiongroup import BaseExceptionGroup, ExceptionGroup
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Implementation of RFC 6555 "Happy eyeballs"
|
|
21
|
+
# https://tools.ietf.org/html/rfc6555
|
|
22
|
+
#
|
|
23
|
+
# Basically, the problem here is that if we want to connect to some host, and
|
|
24
|
+
# DNS returns multiple IP addresses, then we don't know which of them will
|
|
25
|
+
# actually work -- it can happen that some of them are reachable, and some of
|
|
26
|
+
# them are not. One particularly common situation where this happens is on a
|
|
27
|
+
# host that thinks it has ipv6 connectivity, but really doesn't. But in
|
|
28
|
+
# principle this could happen for any kind of multi-home situation (e.g. the
|
|
29
|
+
# route to one mirror is down but another is up).
|
|
30
|
+
#
|
|
31
|
+
# The naive algorithm (e.g. the stdlib's socket.create_connection) would be to
|
|
32
|
+
# pick one of the IP addresses and try to connect; if that fails, try the
|
|
33
|
+
# next; etc. The problem with this is that TCP is stubborn, and if the first
|
|
34
|
+
# address is a blackhole then it might take a very long time (tens of seconds)
|
|
35
|
+
# before that connection attempt fails.
|
|
36
|
+
#
|
|
37
|
+
# That's where RFC 6555 comes in. It tells us that what we do is:
|
|
38
|
+
# - get the list of IPs from getaddrinfo, trusting the order it gives us (with
|
|
39
|
+
# one exception noted in section 5.4)
|
|
40
|
+
# - start a connection attempt to the first IP
|
|
41
|
+
# - when this fails OR if it's still going after DELAY seconds, then start a
|
|
42
|
+
# connection attempt to the second IP
|
|
43
|
+
# - when this fails OR if it's still going after another DELAY seconds, then
|
|
44
|
+
# start a connection attempt to the third IP
|
|
45
|
+
# - ... repeat until we run out of IPs.
|
|
46
|
+
#
|
|
47
|
+
# Our implementation is similarly straightforward: we spawn a chain of tasks,
|
|
48
|
+
# where each one (a) waits until the previous connection has failed or DELAY
|
|
49
|
+
# seconds have passed, (b) spawns the next task, (c) attempts to connect. As
|
|
50
|
+
# soon as any task crashes or succeeds, we cancel all the tasks and return.
|
|
51
|
+
#
|
|
52
|
+
# Note: this currently doesn't attempt to cache any results, so if you make
|
|
53
|
+
# multiple connections to the same host it'll re-run the happy-eyeballs
|
|
54
|
+
# algorithm each time. RFC 6555 is pretty confusing about whether this is
|
|
55
|
+
# allowed. Section 4 describes an algorithm that attempts ipv4 and ipv6
|
|
56
|
+
# simultaneously, and then says "The client MUST cache information regarding
|
|
57
|
+
# the outcome of each connection attempt, and it uses that information to
|
|
58
|
+
# avoid thrashing the network with subsequent attempts." Then section 4.2 says
|
|
59
|
+
# "implementations MUST prefer the first IP address family returned by the
|
|
60
|
+
# host's address preference policy, unless implementing a stateful
|
|
61
|
+
# algorithm". Here "stateful" means "one that caches information about
|
|
62
|
+
# previous attempts". So my reading of this is that IF you're starting ipv4
|
|
63
|
+
# and ipv6 at the same time then you MUST cache the result for ~ten minutes,
|
|
64
|
+
# but IF you're "preferring" one protocol by trying it first (like we are),
|
|
65
|
+
# then you don't need to cache.
|
|
66
|
+
#
|
|
67
|
+
# Caching is quite tricky: to get it right you need to do things like detect
|
|
68
|
+
# when the network interfaces are reconfigured, and if you get it wrong then
|
|
69
|
+
# connection attempts basically just don't work. So we don't even try.
|
|
70
|
+
|
|
71
|
+
# "Firefox and Chrome use 300 ms"
|
|
72
|
+
# https://tools.ietf.org/html/rfc6555#section-6
|
|
73
|
+
# Though
|
|
74
|
+
# https://www.researchgate.net/profile/Vaibhav_Bajpai3/publication/304568993_Measuring_the_Effects_of_Happy_Eyeballs/links/5773848e08ae6f328f6c284c/Measuring-the-Effects-of-Happy-Eyeballs.pdf
|
|
75
|
+
# claims that Firefox actually uses 0 ms, unless an about:config option is
|
|
76
|
+
# toggled and then it uses 250 ms.
|
|
77
|
+
DEFAULT_DELAY = 0.250
|
|
78
|
+
|
|
79
|
+
# How should we call getaddrinfo? In particular, should we use AI_ADDRCONFIG?
|
|
80
|
+
#
|
|
81
|
+
# The idea of AI_ADDRCONFIG is that it only returns addresses that might
|
|
82
|
+
# work. E.g., if getaddrinfo knows that you don't have any IPv6 connectivity,
|
|
83
|
+
# then it doesn't return any IPv6 addresses. And this is kinda nice, because
|
|
84
|
+
# it means maybe you can skip sending AAAA requests entirely. But in practice,
|
|
85
|
+
# it doesn't really work right.
|
|
86
|
+
#
|
|
87
|
+
# - on Linux/glibc, empirically, the default is to return all addresses, and
|
|
88
|
+
# with AI_ADDRCONFIG then it only returns IPv6 addresses if there is at least
|
|
89
|
+
# one non-loopback IPv6 address configured... but this can be a link-local
|
|
90
|
+
# address, so in practice I guess this is basically always configured if IPv6
|
|
91
|
+
# is enabled at all. OTOH if you pass in "::1" as the target address with
|
|
92
|
+
# AI_ADDRCONFIG and there's no *external* IPv6 address configured, you get an
|
|
93
|
+
# error. So AI_ADDRCONFIG mostly doesn't do anything, even when you would want
|
|
94
|
+
# it to, and when it does do something it might break things that would have
|
|
95
|
+
# worked.
|
|
96
|
+
#
|
|
97
|
+
# - on Windows 10, empirically, if no IPv6 address is configured then by
|
|
98
|
+
# default they are also suppressed from getaddrinfo (flags=0 and
|
|
99
|
+
# flags=AI_ADDRCONFIG seem to do the same thing). If you pass AI_ALL, then you
|
|
100
|
+
# get the full list.
|
|
101
|
+
# ...except for localhost! getaddrinfo("localhost", "80") gives me ::1, even
|
|
102
|
+
# though there's no ipv6 and other queries only return ipv4.
|
|
103
|
+
# If you pass in and IPv6 IP address as the target address, then that's always
|
|
104
|
+
# returned OK, even with AI_ADDRCONFIG set and no IPv6 configured.
|
|
105
|
+
#
|
|
106
|
+
# But I guess other versions of windows messed this up, judging from these bug
|
|
107
|
+
# reports:
|
|
108
|
+
# https://bugs.chromium.org/p/chromium/issues/detail?id=5234
|
|
109
|
+
# https://bugs.chromium.org/p/chromium/issues/detail?id=32522#c50
|
|
110
|
+
#
|
|
111
|
+
# So basically the options are either to use AI_ADDRCONFIG and then add some
|
|
112
|
+
# complicated special cases to work around its brokenness, or else don't use
|
|
113
|
+
# AI_ADDRCONFIG and accept that sometimes on legacy/misconfigured networks
|
|
114
|
+
# we'll waste 300 ms trying to connect to a blackholed destination.
|
|
115
|
+
#
|
|
116
|
+
# Twisted and Tornado always uses default flags. I think we'll do the same.
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@contextmanager
|
|
120
|
+
def close_all() -> Generator[set[SocketType], None, None]:
|
|
121
|
+
sockets_to_close: set[SocketType] = set()
|
|
122
|
+
try:
|
|
123
|
+
yield sockets_to_close
|
|
124
|
+
finally:
|
|
125
|
+
errs = []
|
|
126
|
+
for sock in sockets_to_close:
|
|
127
|
+
try:
|
|
128
|
+
sock.close()
|
|
129
|
+
except BaseException as exc:
|
|
130
|
+
errs.append(exc)
|
|
131
|
+
if len(errs) == 1:
|
|
132
|
+
raise errs[0]
|
|
133
|
+
elif errs:
|
|
134
|
+
raise BaseExceptionGroup("", errs)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def reorder_for_rfc_6555_section_5_4( # type: ignore[explicit-any]
|
|
138
|
+
targets: MutableSequence[tuple[AddressFamily, SocketKind, int, str, Any]],
|
|
139
|
+
) -> None:
|
|
140
|
+
# RFC 6555 section 5.4 says that if getaddrinfo returns multiple address
|
|
141
|
+
# families (e.g. IPv4 and IPv6), then you should make sure that your first
|
|
142
|
+
# and second attempts use different families:
|
|
143
|
+
#
|
|
144
|
+
# https://tools.ietf.org/html/rfc6555#section-5.4
|
|
145
|
+
#
|
|
146
|
+
# This function post-processes the results from getaddrinfo, in-place, to
|
|
147
|
+
# satisfy this requirement.
|
|
148
|
+
for i in range(1, len(targets)):
|
|
149
|
+
if targets[i][0] != targets[0][0]:
|
|
150
|
+
# Found the first entry with a different address family; move it
|
|
151
|
+
# so that it becomes the second item on the list.
|
|
152
|
+
if i != 1:
|
|
153
|
+
targets.insert(1, targets.pop(i))
|
|
154
|
+
break
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def format_host_port(host: str | bytes, port: int | str) -> str:
|
|
158
|
+
host = host.decode("ascii") if isinstance(host, bytes) else host
|
|
159
|
+
if ":" in host:
|
|
160
|
+
return f"[{host}]:{port}"
|
|
161
|
+
else:
|
|
162
|
+
return f"{host}:{port}"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# Twisted's HostnameEndpoint has a good set of configurations:
|
|
166
|
+
# https://twistedmatrix.com/documents/current/api/twisted.internet.endpoints.HostnameEndpoint.html
|
|
167
|
+
#
|
|
168
|
+
# - per-connection timeout
|
|
169
|
+
# this doesn't seem useful -- we let you set a timeout on the whole thing
|
|
170
|
+
# using Trio's normal mechanisms, and that seems like enough
|
|
171
|
+
# - delay between attempts
|
|
172
|
+
# - bind address (but not port!)
|
|
173
|
+
# they *don't* support multiple address bindings, like giving the ipv4 and
|
|
174
|
+
# ipv6 addresses of the host.
|
|
175
|
+
# I think maybe our semantics should be: we accept a list of bind addresses,
|
|
176
|
+
# and we bind to the first one that is compatible with the
|
|
177
|
+
# connection attempt we want to make, and if none are compatible then we
|
|
178
|
+
# don't try to connect to that target.
|
|
179
|
+
#
|
|
180
|
+
# XX TODO: implement bind address support
|
|
181
|
+
#
|
|
182
|
+
# Actually, the best option is probably to be explicit: {AF_INET: "...",
|
|
183
|
+
# AF_INET6: "..."}
|
|
184
|
+
# this might be simpler after
|
|
185
|
+
async def open_tcp_stream(
|
|
186
|
+
host: str | bytes,
|
|
187
|
+
port: int,
|
|
188
|
+
*,
|
|
189
|
+
happy_eyeballs_delay: float | None = DEFAULT_DELAY,
|
|
190
|
+
local_address: str | None = None,
|
|
191
|
+
) -> trio.SocketStream:
|
|
192
|
+
"""Connect to the given host and port over TCP.
|
|
193
|
+
|
|
194
|
+
If the given ``host`` has multiple IP addresses associated with it, then
|
|
195
|
+
we have a problem: which one do we use?
|
|
196
|
+
|
|
197
|
+
One approach would be to attempt to connect to the first one, and then if
|
|
198
|
+
that fails, attempt to connect to the second one ... until we've tried all
|
|
199
|
+
of them. But the problem with this is that if the first IP address is
|
|
200
|
+
unreachable (for example, because it's an IPv6 address and our network
|
|
201
|
+
discards IPv6 packets), then we might end up waiting tens of seconds for
|
|
202
|
+
the first connection attempt to timeout before we try the second address.
|
|
203
|
+
|
|
204
|
+
Another approach would be to attempt to connect to all of the addresses at
|
|
205
|
+
the same time, in parallel, and then use whichever connection succeeds
|
|
206
|
+
first, abandoning the others. This would be fast, but create a lot of
|
|
207
|
+
unnecessary load on the network and the remote server.
|
|
208
|
+
|
|
209
|
+
This function strikes a balance between these two extremes: it works its
|
|
210
|
+
way through the available addresses one at a time, like the first
|
|
211
|
+
approach; but, if ``happy_eyeballs_delay`` seconds have passed and it's
|
|
212
|
+
still waiting for an attempt to succeed or fail, then it gets impatient
|
|
213
|
+
and starts the next connection attempt in parallel. As soon as any one
|
|
214
|
+
connection attempt succeeds, all the other attempts are cancelled. This
|
|
215
|
+
avoids unnecessary load because most connections will succeed after just
|
|
216
|
+
one or two attempts, but if one of the addresses is unreachable then it
|
|
217
|
+
doesn't slow us down too much.
|
|
218
|
+
|
|
219
|
+
This is known as a "happy eyeballs" algorithm, and our particular variant
|
|
220
|
+
is modelled after how Chrome connects to webservers; see `RFC 6555
|
|
221
|
+
<https://tools.ietf.org/html/rfc6555>`__ for more details.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
host (str or bytes): The host to connect to. Can be an IPv4 address,
|
|
225
|
+
IPv6 address, or a hostname.
|
|
226
|
+
|
|
227
|
+
port (int): The port to connect to.
|
|
228
|
+
|
|
229
|
+
happy_eyeballs_delay (float or None): How many seconds to wait for each
|
|
230
|
+
connection attempt to succeed or fail before getting impatient and
|
|
231
|
+
starting another one in parallel. Set to `None` if you want
|
|
232
|
+
to limit to only one connection attempt at a time (like
|
|
233
|
+
:func:`socket.create_connection`). Default: 0.25 (250 ms).
|
|
234
|
+
|
|
235
|
+
local_address (None or str): The local IP address or hostname to use as
|
|
236
|
+
the source for outgoing connections. If ``None``, we let the OS pick
|
|
237
|
+
the source IP.
|
|
238
|
+
|
|
239
|
+
This is useful in some exotic networking configurations where your
|
|
240
|
+
host has multiple IP addresses, and you want to force the use of a
|
|
241
|
+
specific one.
|
|
242
|
+
|
|
243
|
+
Note that if you pass an IPv4 ``local_address``, then you won't be
|
|
244
|
+
able to connect to IPv6 hosts, and vice-versa. If you want to take
|
|
245
|
+
advantage of this to force the use of IPv4 or IPv6 without
|
|
246
|
+
specifying an exact source address, you can use the IPv4 wildcard
|
|
247
|
+
address ``local_address="0.0.0.0"``, or the IPv6 wildcard address
|
|
248
|
+
``local_address="::"``.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
SocketStream: a :class:`~trio.abc.Stream` connected to the given server.
|
|
252
|
+
|
|
253
|
+
Raises:
|
|
254
|
+
OSError: if the connection fails.
|
|
255
|
+
|
|
256
|
+
See also:
|
|
257
|
+
open_ssl_over_tcp_stream
|
|
258
|
+
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
# To keep our public API surface smaller, rule out some cases that
|
|
262
|
+
# getaddrinfo will accept in some circumstances, but that act weird or
|
|
263
|
+
# have non-portable behavior or are just plain not useful.
|
|
264
|
+
if not isinstance(host, (str, bytes)):
|
|
265
|
+
raise ValueError(f"host must be str or bytes, not {host!r}")
|
|
266
|
+
if not isinstance(port, int):
|
|
267
|
+
raise TypeError(f"port must be int, not {port!r}")
|
|
268
|
+
|
|
269
|
+
if happy_eyeballs_delay is None:
|
|
270
|
+
happy_eyeballs_delay = DEFAULT_DELAY
|
|
271
|
+
|
|
272
|
+
targets = await getaddrinfo(host, port, type=SOCK_STREAM)
|
|
273
|
+
|
|
274
|
+
# I don't think this can actually happen -- if there are no results,
|
|
275
|
+
# getaddrinfo should have raised OSError instead of returning an empty
|
|
276
|
+
# list. But let's be paranoid and handle it anyway:
|
|
277
|
+
if not targets:
|
|
278
|
+
msg = f"no results found for hostname lookup: {format_host_port(host, port)}"
|
|
279
|
+
raise OSError(msg)
|
|
280
|
+
|
|
281
|
+
reorder_for_rfc_6555_section_5_4(targets)
|
|
282
|
+
|
|
283
|
+
# This list records all the connection failures that we ignored.
|
|
284
|
+
oserrors: list[OSError] = []
|
|
285
|
+
|
|
286
|
+
# Keeps track of the socket that we're going to complete with,
|
|
287
|
+
# need to make sure this isn't automatically closed
|
|
288
|
+
winning_socket: SocketType | None = None
|
|
289
|
+
|
|
290
|
+
# Try connecting to the specified address. Possible outcomes:
|
|
291
|
+
# - success: record connected socket in winning_socket and cancel
|
|
292
|
+
# concurrent attempts
|
|
293
|
+
# - failure: record exception in oserrors, set attempt_failed allowing
|
|
294
|
+
# the next connection attempt to start early
|
|
295
|
+
# code needs to ensure sockets can be closed appropriately in the
|
|
296
|
+
# face of crash or cancellation
|
|
297
|
+
async def attempt_connect(
|
|
298
|
+
socket_args: tuple[AddressFamily, SocketKind, int],
|
|
299
|
+
sockaddr: AddressFormat,
|
|
300
|
+
attempt_failed: trio.Event,
|
|
301
|
+
) -> None:
|
|
302
|
+
nonlocal winning_socket
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
sock = socket(*socket_args)
|
|
306
|
+
open_sockets.add(sock)
|
|
307
|
+
|
|
308
|
+
if local_address is not None:
|
|
309
|
+
# TCP connections are identified by a 4-tuple:
|
|
310
|
+
#
|
|
311
|
+
# (local IP, local port, remote IP, remote port)
|
|
312
|
+
#
|
|
313
|
+
# So if a single local IP wants to make multiple connections
|
|
314
|
+
# to the same (remote IP, remote port) pair, then those
|
|
315
|
+
# connections have to use different local ports, or else TCP
|
|
316
|
+
# won't be able to tell them apart. OTOH, if you have multiple
|
|
317
|
+
# connections to different remote IP/ports, then those
|
|
318
|
+
# connections can share a local port.
|
|
319
|
+
#
|
|
320
|
+
# Normally, when you call bind(), the kernel will immediately
|
|
321
|
+
# assign a specific local port to your socket. At this point
|
|
322
|
+
# the kernel doesn't know which (remote IP, remote port)
|
|
323
|
+
# you're going to use, so it has to pick a local port that
|
|
324
|
+
# *no* other connection is using. That's the only way to
|
|
325
|
+
# guarantee that this local port will be usable later when we
|
|
326
|
+
# call connect(). (Alternatively, you can set SO_REUSEADDR to
|
|
327
|
+
# allow multiple nascent connections to share the same port,
|
|
328
|
+
# but then connect() might fail with EADDRNOTAVAIL if we get
|
|
329
|
+
# unlucky and our TCP 4-tuple ends up colliding with another
|
|
330
|
+
# unrelated connection.)
|
|
331
|
+
#
|
|
332
|
+
# So calling bind() before connect() works, but it disables
|
|
333
|
+
# sharing of local ports. This is inefficient: it makes you
|
|
334
|
+
# more likely to run out of local ports.
|
|
335
|
+
#
|
|
336
|
+
# But on some versions of Linux, we can re-enable sharing of
|
|
337
|
+
# local ports by setting a special flag. This flag tells
|
|
338
|
+
# bind() to only bind the IP, and not the port. That way,
|
|
339
|
+
# connect() is allowed to pick the the port, and it can do a
|
|
340
|
+
# better job of it because it knows the remote IP/port.
|
|
341
|
+
with suppress(OSError, AttributeError):
|
|
342
|
+
sock.setsockopt(
|
|
343
|
+
trio.socket.IPPROTO_IP,
|
|
344
|
+
trio.socket.IP_BIND_ADDRESS_NO_PORT,
|
|
345
|
+
1,
|
|
346
|
+
)
|
|
347
|
+
try:
|
|
348
|
+
await sock.bind((local_address, 0))
|
|
349
|
+
except OSError:
|
|
350
|
+
raise OSError(
|
|
351
|
+
f"local_address={local_address!r} is incompatible "
|
|
352
|
+
f"with remote address {sockaddr!r}",
|
|
353
|
+
) from None
|
|
354
|
+
|
|
355
|
+
await sock.connect(sockaddr)
|
|
356
|
+
|
|
357
|
+
# Success! Save the winning socket and cancel all outstanding
|
|
358
|
+
# connection attempts.
|
|
359
|
+
winning_socket = sock
|
|
360
|
+
nursery.cancel_scope.cancel(reason="successfully found a socket")
|
|
361
|
+
except OSError as exc:
|
|
362
|
+
# This connection attempt failed, but the next one might
|
|
363
|
+
# succeed. Save the error for later so we can report it if
|
|
364
|
+
# everything fails, and tell the next attempt that it should go
|
|
365
|
+
# ahead (if it hasn't already).
|
|
366
|
+
oserrors.append(exc)
|
|
367
|
+
attempt_failed.set()
|
|
368
|
+
|
|
369
|
+
with close_all() as open_sockets:
|
|
370
|
+
# nursery spawns a task for each connection attempt, will be
|
|
371
|
+
# cancelled by the task that gets a successful connection
|
|
372
|
+
async with trio.open_nursery() as nursery:
|
|
373
|
+
for address_family, socket_type, proto, _, addr in targets:
|
|
374
|
+
# create an event to indicate connection failure,
|
|
375
|
+
# allowing the next target to be tried early
|
|
376
|
+
attempt_failed = trio.Event()
|
|
377
|
+
|
|
378
|
+
nursery.start_soon(
|
|
379
|
+
attempt_connect,
|
|
380
|
+
(address_family, socket_type, proto),
|
|
381
|
+
addr,
|
|
382
|
+
attempt_failed,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
# give this attempt at most this time before moving on
|
|
386
|
+
with trio.move_on_after(happy_eyeballs_delay):
|
|
387
|
+
await attempt_failed.wait()
|
|
388
|
+
|
|
389
|
+
# nothing succeeded
|
|
390
|
+
if winning_socket is None:
|
|
391
|
+
assert len(oserrors) == len(targets)
|
|
392
|
+
msg = f"all attempts to connect to {format_host_port(host, port)} failed"
|
|
393
|
+
raise OSError(msg) from ExceptionGroup(msg, oserrors)
|
|
394
|
+
else:
|
|
395
|
+
stream = trio.SocketStream(winning_socket)
|
|
396
|
+
open_sockets.remove(winning_socket)
|
|
397
|
+
return stream
|