|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import os |
| 4 | +import sys |
| 5 | +from typing import TYPE_CHECKING |
| 6 | + |
| 7 | +import trio |
| 8 | +import trio.socket as tsocket |
| 9 | +from trio import TaskStatus |
| 10 | + |
| 11 | +if TYPE_CHECKING: |
| 12 | + from collections.abc import Awaitable, Callable |
| 13 | + |
| 14 | + |
| 15 | +try: |
| 16 | + from trio.socket import AF_UNIX |
| 17 | + |
| 18 | + HAS_UNIX = True |
| 19 | +except ImportError: |
| 20 | + HAS_UNIX = False |
| 21 | + |
| 22 | + |
| 23 | +# Default backlog size: |
| 24 | +# |
| 25 | +# Having the backlog too low can cause practical problems (a perfectly healthy |
| 26 | +# service that starts failing to accept connections if they arrive in a |
| 27 | +# burst). |
| 28 | +# |
| 29 | +# Having it too high doesn't really cause any problems. Like any buffer, you |
| 30 | +# want backlog queue to be zero usually, and it won't save you if you're |
| 31 | +# getting connection attempts faster than you can call accept() on an ongoing |
| 32 | +# basis. But unlike other buffers, this one doesn't really provide any |
| 33 | +# backpressure. If a connection gets stuck waiting in the backlog queue, then |
| 34 | +# from the peer's point of view the connection succeeded but then their |
| 35 | +# send/recv will stall until we get to it, possibly for a long time. OTOH if |
| 36 | +# there isn't room in the backlog queue, then their connect stalls, possibly |
| 37 | +# for a long time, which is pretty much the same thing. |
| 38 | +# |
| 39 | +# A large backlog can also use a bit more kernel memory, but this seems fairly |
| 40 | +# negligible these days. |
| 41 | +# |
| 42 | +# So this suggests we should make the backlog as large as possible. This also |
| 43 | +# matches what Golang does. However, they do it in a weird way, where they |
| 44 | +# have a bunch of code to sniff out the configured upper limit for backlog on |
| 45 | +# different operating systems. But on every system, passing in a too-large |
| 46 | +# backlog just causes it to be silently truncated to the configured maximum, |
| 47 | +# so this is unnecessary -- we can just pass in "infinity" and get the maximum |
| 48 | +# that way. (Verified on Windows, Linux, macOS using |
| 49 | +# https://github.com/python-trio/trio/wiki/notes-to-self#measure-listen-backlogpy |
| 50 | +def _compute_backlog(backlog: int | None) -> int: |
| 51 | + # Many systems (Linux, BSDs, ...) store the backlog in a uint16 and are |
| 52 | + # missing overflow protection, so we apply our own overflow protection. |
| 53 | + # https://github.com/golang/go/issues/5030 |
| 54 | + if not isinstance(backlog, int) and backlog is not None: |
| 55 | + raise TypeError(f"backlog must be an int or None, not {backlog!r}") |
| 56 | + if backlog is None: |
| 57 | + return 0xFFFF |
| 58 | + return min(backlog, 0xFFFF) |
| 59 | + |
| 60 | + |
| 61 | +async def open_unix_listener( |
| 62 | + path: str | bytes | os.PathLike[str] | os.PathLike[bytes], |
| 63 | + *, |
| 64 | + mode: int | None = None, # 0o666, |
| 65 | + backlog: int | None = None, |
| 66 | +) -> trio.UnixSocketListener: |
| 67 | + """Create :class:`SocketListener` objects to listen for connections. |
| 68 | + Opens a connection to the specified |
| 69 | + `Unix domain socket <https://en.wikipedia.org/wiki/Unix_domain_socket>`__. |
| 70 | +
|
| 71 | + You must have read/write permission on the specified file to connect. |
| 72 | +
|
| 73 | + Args: |
| 74 | +
|
| 75 | + path (str): Filename of UNIX socket to create and listen on. |
| 76 | + Absolute or relative paths may be used. |
| 77 | +
|
| 78 | + mode (int or None): The socket file permissions. |
| 79 | + UNIX permissions are usually specified in octal numbers. |
| 80 | + If you leave this as ``None``, Trio will not change the mode from |
| 81 | + the operating system's default. |
| 82 | +
|
| 83 | + backlog (int or None): The listen backlog to use. If you leave this as |
| 84 | + ``None`` then Trio will pick a good default. (Currently: whatever |
| 85 | + your system has configured as the maximum backlog.) |
| 86 | +
|
| 87 | + Returns: |
| 88 | + :class:`UnixSocketListener` |
| 89 | +
|
| 90 | + Raises: |
| 91 | + :class:`TypeError` if invalid arguments. |
| 92 | + :class:`RuntimeError`: If AF_UNIX sockets are not supported. |
| 93 | + """ |
| 94 | + if not HAS_UNIX: |
| 95 | + raise RuntimeError("Unix sockets are not supported on this platform") |
| 96 | + |
| 97 | + computed_backlog = _compute_backlog(backlog) |
| 98 | + |
| 99 | + fspath = await trio.Path(os.fsdecode(path)).absolute() |
| 100 | + |
| 101 | + folder = fspath.parent |
| 102 | + if not await folder.exists(): |
| 103 | + raise FileNotFoundError(f"Socket folder does not exist: {folder!r}") |
| 104 | + |
| 105 | + # much more simplified logic vs tcp sockets - one socket type and only one |
| 106 | + # possible location to connect to |
| 107 | + sock = tsocket.socket(AF_UNIX, tsocket.SOCK_STREAM) |
| 108 | + try: |
| 109 | + # See https://github.com/python-trio/trio/issues/39 |
| 110 | + if sys.platform != "win32": |
| 111 | + sock.setsockopt(tsocket.SOL_SOCKET, tsocket.SO_REUSEADDR, 1) |
| 112 | + |
| 113 | + await sock.bind(str(fspath)) |
| 114 | + |
| 115 | + sock.listen(computed_backlog) |
| 116 | + |
| 117 | + if mode is not None: |
| 118 | + await fspath.chmod(mode) |
| 119 | + |
| 120 | + return trio.UnixSocketListener(sock) |
| 121 | + except BaseException as exc: |
| 122 | + sock.close() |
| 123 | + try: |
| 124 | + os.unlink(str(fspath)) |
| 125 | + except BaseException as exc_2: |
| 126 | + raise exc_2 from exc |
| 127 | + raise |
| 128 | + |
| 129 | + |
| 130 | +async def serve_unix( |
| 131 | + handler: Callable[[trio.SocketStream], Awaitable[object]], |
| 132 | + path: str | bytes | os.PathLike[str] | os.PathLike[bytes], |
| 133 | + *, |
| 134 | + backlog: int | None = None, |
| 135 | + handler_nursery: trio.Nursery | None = None, |
| 136 | + task_status: TaskStatus[list[trio.UnixSocketListener]] = trio.TASK_STATUS_IGNORED, |
| 137 | +) -> None: |
| 138 | + """Listen for incoming UNIX connections, and for each one start a task |
| 139 | + running ``handler(stream)``. |
| 140 | + This is a thin convenience wrapper around :func:`open_unix_listener` and |
| 141 | + :func:`serve_listeners` – see them for full details. |
| 142 | + .. warning:: |
| 143 | + If ``handler`` raises an exception, then this function doesn't do |
| 144 | + anything special to catch it – so by default the exception will |
| 145 | + propagate out and crash your server. If you don't want this, then catch |
| 146 | + exceptions inside your ``handler``, or use a ``handler_nursery`` object |
| 147 | + that responds to exceptions in some other way. |
| 148 | + When used with ``nursery.start`` you get back the newly opened listeners. |
| 149 | + Args: |
| 150 | + handler: The handler to start for each incoming connection. Passed to |
| 151 | + :func:`serve_listeners`. |
| 152 | + path: The socket file name. |
| 153 | + Passed to :func:`open_unix_listener`. |
| 154 | + backlog: The listen backlog, or None to have a good default picked. |
| 155 | + Passed to :func:`open_tcp_listener`. |
| 156 | + handler_nursery: The nursery to start handlers in, or None to use an |
| 157 | + internal nursery. Passed to :func:`serve_listeners`. |
| 158 | + task_status: This function can be used with ``nursery.start``. |
| 159 | + Returns: |
| 160 | + This function only returns when cancelled. |
| 161 | + Raises: |
| 162 | + RuntimeError: If AF_UNIX sockets are not supported. |
| 163 | + """ |
| 164 | + if not HAS_UNIX: |
| 165 | + raise RuntimeError("Unix sockets are not supported on this platform") |
| 166 | + |
| 167 | + listener = await open_unix_listener(path, backlog=backlog) |
| 168 | + await trio.serve_listeners( |
| 169 | + handler, |
| 170 | + [listener], |
| 171 | + handler_nursery=handler_nursery, |
| 172 | + task_status=task_status, |
| 173 | + ) |
0 commit comments