pybricks-core 1.0.0b1__py3-none-any.whl

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.
pb_core/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 The Pybricks Authors
3
+
4
+ """Shared building blocks for the Pybricks tool suite.
5
+
6
+ This package holds everything both the service and the client need to agree on:
7
+ the JSON-RPC wire protocol, the per-platform IPC transport, logging setup and
8
+ platform paths. It deliberately depends on neither ``pb_service`` nor ``pb_client``.
9
+ """
10
+
11
+ __version__ = "1.0.0b1"
12
+
13
+ __all__ = ["__version__"]
pb_core/auth.py ADDED
@@ -0,0 +1,90 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 The Pybricks Authors
3
+
4
+ """Shared-secret authentication between the client and the service.
5
+
6
+ The service writes a random token to a file only the current user can read, and
7
+ every client presents it in the mandatory first call. This is the X11
8
+ magic-cookie scheme.
9
+
10
+ It is required on every transport rather than only on TCP. The default
11
+ discretionary access control list on a Windows named pipe grants Everyone read
12
+ access, so without a secret another local user could not issue commands but
13
+ could still watch a hub's output. It is not sufficient on its own for a service
14
+ reachable beyond loopback, where the token crosses the network in clear and
15
+ needs TLS or an SSH tunnel around it.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ import secrets
22
+ import sys
23
+ from pathlib import Path
24
+
25
+ from pb_core import paths
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ __all__ = ["TOKEN_BYTES", "load_token", "read_or_create_token"]
30
+
31
+ TOKEN_BYTES = 32
32
+
33
+
34
+ def read_or_create_token(path: Path | None = None) -> str:
35
+ """Return the service's token, creating one if there is none.
36
+
37
+ Called by the service at startup.
38
+
39
+ Args:
40
+ path: Override the cookie file location.
41
+
42
+ Returns:
43
+ The token as hexadecimal text.
44
+ """
45
+ cookie = path or paths.cookie_file()
46
+ paths.ensure_private_dir(cookie.parent)
47
+
48
+ if cookie.exists():
49
+ existing = cookie.read_text(encoding="ascii").strip()
50
+ if existing:
51
+ return existing
52
+
53
+ token = secrets.token_hex(TOKEN_BYTES)
54
+ # Create with restrictive permissions from the outset rather than widening
55
+ # then narrowing, which would leave a window where the token is readable.
56
+ descriptor = cookie.open("w", encoding="ascii")
57
+
58
+ try:
59
+ if sys.platform != "win32":
60
+ import os
61
+
62
+ os.fchmod(descriptor.fileno(), 0o600)
63
+
64
+ descriptor.write(token)
65
+ finally:
66
+ descriptor.close()
67
+
68
+ logger.debug("wrote a new authentication token to %s", cookie)
69
+ return token
70
+
71
+
72
+ def load_token(path: Path | None = None) -> str | None:
73
+ """Return the service's token if it can be read.
74
+
75
+ Called by clients before connecting.
76
+
77
+ Args:
78
+ path: Override the cookie file location.
79
+
80
+ Returns:
81
+ The token, or ``None`` if no service has ever run for this user.
82
+ """
83
+ cookie = path or paths.cookie_file()
84
+
85
+ try:
86
+ token = cookie.read_text(encoding="ascii").strip()
87
+ except OSError:
88
+ return None
89
+
90
+ return token or None
@@ -0,0 +1,66 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 The Pybricks Authors
3
+
4
+ """Transport between the client and the service.
5
+
6
+ The user never types an address: :func:`pb_core.ipc.address.resolve` picks the
7
+ right one for the platform. Everything above this package deals only in
8
+ :class:`anyio.abc.ByteStream`, so neither the service nor the client contains any
9
+ platform-specific code.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from contextlib import AbstractAsyncContextManager
15
+
16
+ from anyio.abc import ByteStream
17
+
18
+ from pb_core.ipc.address import ENDPOINT_ENV_VAR, Endpoint, default, resolve
19
+ from pb_core.ipc.backend import IpcBackend, IpcListener, StreamHandler, get_backend
20
+ from pb_core.ipc.errors import AddressInUseError, EndpointError
21
+
22
+ __all__ = [
23
+ "ENDPOINT_ENV_VAR",
24
+ "AddressInUseError",
25
+ "Endpoint",
26
+ "EndpointError",
27
+ "IpcBackend",
28
+ "IpcListener",
29
+ "StreamHandler",
30
+ "connect",
31
+ "create_listener",
32
+ "default",
33
+ "get_backend",
34
+ "resolve",
35
+ ]
36
+
37
+
38
+ def connect(endpoint: Endpoint) -> AbstractAsyncContextManager[ByteStream]:
39
+ """Connect to the service listening at ``endpoint``.
40
+
41
+ Args:
42
+ endpoint: Where the service is listening.
43
+
44
+ Returns:
45
+ An async context manager yielding the connected stream.
46
+
47
+ Raises:
48
+ ConnectionRefusedError: If nothing is listening. Callers treat this as
49
+ "the service is not running" and may start one.
50
+ """
51
+ return get_backend(endpoint).connect(endpoint)
52
+
53
+
54
+ def create_listener(endpoint: Endpoint) -> AbstractAsyncContextManager[IpcListener]:
55
+ """Listen for clients at ``endpoint``.
56
+
57
+ Args:
58
+ endpoint: Where to listen.
59
+
60
+ Returns:
61
+ An async context manager yielding the listener.
62
+
63
+ Raises:
64
+ AddressInUseError: If another service already holds the address.
65
+ """
66
+ return get_backend(endpoint).create_listener(endpoint)
pb_core/ipc/address.py ADDED
@@ -0,0 +1,151 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 The Pybricks Authors
3
+
4
+ """Endpoint addressing for the service.
5
+
6
+ An endpoint is written as a URI so that one string can name any transport:
7
+
8
+ * ``unix:///run/user/1000/pybricks/pb-service.sock`` -- AF_UNIX socket (Linux, macOS)
9
+ * ``pipe://pybricks-pb-service-david`` -- Windows named pipe
10
+ * ``tcp://127.0.0.1:47823`` -- TCP, for remote services and for tests
11
+ * ``memory://name`` -- in-process, used by the test suite
12
+
13
+ Users are not expected to type these. :func:`resolve` picks the right default
14
+ for the platform, and ``PB_ENDPOINT`` overrides it.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import os
20
+ import sys
21
+ from dataclasses import dataclass
22
+ from typing import Literal
23
+
24
+ from pb_core import paths
25
+ from pb_core.ipc.errors import EndpointError
26
+
27
+ __all__ = ["ENDPOINT_ENV_VAR", "Endpoint", "Scheme", "default", "resolve"]
28
+
29
+ ENDPOINT_ENV_VAR = "PB_ENDPOINT"
30
+
31
+ Scheme = Literal["unix", "pipe", "tcp", "memory"]
32
+
33
+
34
+ @dataclass(frozen=True, slots=True)
35
+ class Endpoint:
36
+ """A transport-independent address for the service."""
37
+
38
+ scheme: Scheme
39
+ target: str
40
+ """Socket path, pipe name, ``host:port``, or in-process key."""
41
+
42
+ @classmethod
43
+ def parse(cls, uri: str) -> Endpoint:
44
+ """Parse an endpoint URI.
45
+
46
+ Args:
47
+ uri: A string such as ``tcp://127.0.0.1:47823``.
48
+
49
+ Returns:
50
+ The parsed endpoint.
51
+
52
+ Raises:
53
+ EndpointError: If the URI is malformed or names an unknown
54
+ scheme. It is a :class:`ValueError`.
55
+ """
56
+ scheme, separator, rest = uri.partition("://")
57
+ if not separator:
58
+ raise EndpointError(f"endpoint is not a URI: {uri!r}")
59
+
60
+ if scheme not in ("unix", "pipe", "tcp", "memory"):
61
+ raise EndpointError(f"unknown endpoint scheme: {scheme!r}")
62
+
63
+ if not rest:
64
+ raise EndpointError(f"endpoint has no target: {uri!r}")
65
+
66
+ if scheme == "tcp":
67
+ host, colon, port = rest.rpartition(":")
68
+ if not colon or not host:
69
+ raise EndpointError(f"tcp endpoint needs host:port: {uri!r}")
70
+
71
+ try:
72
+ number = int(port)
73
+ except ValueError:
74
+ raise EndpointError(
75
+ f"tcp endpoint port is not a number: {uri!r}"
76
+ ) from None
77
+
78
+ if not 0 <= number <= 65535:
79
+ raise EndpointError(f"tcp endpoint port out of range: {uri!r}")
80
+
81
+ return cls(scheme, rest)
82
+
83
+ @property
84
+ def tcp_address(self) -> tuple[str, int]:
85
+ """Return ``(host, port)`` for a TCP endpoint.
86
+
87
+ Raises:
88
+ ValueError: If this is not a TCP endpoint.
89
+ """
90
+
91
+ if self.scheme != "tcp":
92
+ raise EndpointError(f"not a tcp endpoint: {self}")
93
+
94
+ host, _, port = self.target.rpartition(":")
95
+ return host, int(port)
96
+
97
+ @property
98
+ def is_loopback(self) -> bool:
99
+ """Whether a TCP endpoint is confined to this machine.
100
+
101
+ Non-loopback TCP carries the shared secret over a plaintext socket, so
102
+ callers should require TLS or an SSH tunnel for it.
103
+ """
104
+
105
+ if self.scheme != "tcp":
106
+ return True
107
+
108
+ host, _ = self.tcp_address
109
+ return host in ("127.0.0.1", "::1", "localhost")
110
+
111
+ def __str__(self) -> str:
112
+ """Render the endpoint back as a URI."""
113
+ return f"{self.scheme}://{self.target}"
114
+
115
+
116
+ def default() -> Endpoint:
117
+ """Return the platform's default endpoint.
118
+
119
+ Windows uses a named pipe because asyncio cannot serve AF_UNIX sockets on
120
+ the proactor event loop, which the service requires for other reasons.
121
+ """
122
+
123
+ if sys.platform == "win32":
124
+ return Endpoint(
125
+ "pipe", paths.windows_pipe_name().removeprefix(r"\.\pipe" + "\\")
126
+ )
127
+
128
+ return Endpoint("unix", str(paths.socket_path()))
129
+
130
+
131
+ def resolve(uri: str | None = None) -> Endpoint:
132
+ """Return the endpoint to use.
133
+
134
+ Precedence is the explicit argument, then ``PB_ENDPOINT``, then the
135
+ platform default.
136
+
137
+ Args:
138
+ uri: An explicit endpoint URI, if the caller has one.
139
+
140
+ Returns:
141
+ The endpoint to connect to or listen on.
142
+ """
143
+
144
+ if uri:
145
+ return Endpoint.parse(uri)
146
+
147
+ from_env = os.environ.get(ENDPOINT_ENV_VAR)
148
+ if from_env:
149
+ return Endpoint.parse(from_env)
150
+
151
+ return default()
@@ -0,0 +1,58 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 The Pybricks Authors
3
+
4
+ """Per-transport backends, selected by endpoint scheme."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import sys
9
+
10
+ from pb_core.ipc.address import Endpoint
11
+ from pb_core.ipc.backend.provider import IpcBackend, IpcListener, StreamHandler
12
+
13
+ __all__ = ["IpcBackend", "IpcListener", "StreamHandler", "get_backend"]
14
+
15
+
16
+ def get_backend(endpoint: Endpoint) -> IpcBackend:
17
+ """Return the backend that implements ``endpoint``.
18
+
19
+ Backends are imported lazily so that, for example, importing the client on
20
+ Linux never touches the Windows pipe module.
21
+
22
+ Args:
23
+ endpoint: The endpoint to be connected to or served.
24
+
25
+ Returns:
26
+ A backend for that endpoint's scheme.
27
+
28
+ Raises:
29
+ NotImplementedError: If the scheme is not supported on this platform.
30
+ """
31
+
32
+ if endpoint.scheme == "memory":
33
+ from pb_core.ipc.backend.memory import MemoryBackend
34
+
35
+ return MemoryBackend()
36
+
37
+ if endpoint.scheme == "tcp":
38
+ from pb_core.ipc.backend.tcp import TcpBackend
39
+
40
+ return TcpBackend()
41
+
42
+ if endpoint.scheme == "pipe":
43
+ if sys.platform != "win32":
44
+ raise NotImplementedError("named pipes are only available on Windows")
45
+
46
+ from pb_core.ipc.backend.windows import WindowsPipeBackend
47
+
48
+ return WindowsPipeBackend()
49
+
50
+ if sys.platform == "win32":
51
+ raise NotImplementedError(
52
+ "asyncio cannot serve AF_UNIX sockets on the Windows proactor loop; "
53
+ "use a pipe:// endpoint"
54
+ )
55
+
56
+ from pb_core.ipc.backend.posix import PosixBackend
57
+
58
+ return PosixBackend()
@@ -0,0 +1,31 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 The Pybricks Authors
3
+
4
+ """Adapter presenting an anyio listener through the :class:`IpcListener` API."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any, final
9
+
10
+ from anyio.abc import Listener, TaskGroup
11
+ from typing_extensions import override
12
+
13
+ from pb_core.ipc.backend.provider import IpcListener, StreamHandler, owned
14
+
15
+ __all__ = ["AnyioListener"]
16
+
17
+
18
+ @final
19
+ class AnyioListener(IpcListener):
20
+ """Wraps :class:`anyio.abc.Listener` so all backends share one interface."""
21
+
22
+ def __init__(self, inner: Listener[Any]) -> None:
23
+ self._inner = inner
24
+
25
+ @override
26
+ async def serve(self, handler: StreamHandler, task_group: TaskGroup) -> None:
27
+ await self._inner.serve(owned(handler), task_group)
28
+
29
+ @override
30
+ async def aclose(self) -> None:
31
+ await self._inner.aclose()
@@ -0,0 +1,163 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 The Pybricks Authors
3
+
4
+ """An in-process transport, used by the test suite.
5
+
6
+ Running the whole service and client stack in one process with no OS resources
7
+ makes the integration tests fast and identical on every platform, which leaves
8
+ the real per-platform backends needing only a conformance suite.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from collections.abc import AsyncGenerator
15
+ from contextlib import AbstractAsyncContextManager, asynccontextmanager, suppress
16
+ from typing import ClassVar, final
17
+
18
+ import anyio
19
+ from anyio.abc import ByteStream, TaskGroup
20
+ from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
21
+ from anyio.streams.stapled import StapledObjectStream
22
+ from typing_extensions import override
23
+
24
+ from pb_core.ipc.address import Endpoint
25
+ from pb_core.ipc.backend.provider import IpcBackend, IpcListener, StreamHandler, owned
26
+ from pb_core.ipc.errors import AddressInUseError
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ __all__ = ["MemoryBackend"]
31
+
32
+ _BUFFER = 64
33
+
34
+
35
+ @final
36
+ class _MemoryByteStream(ByteStream):
37
+ """Adapts an object stream of ``bytes`` to the byte-stream interface.
38
+
39
+ A byte stream must honour ``max_bytes``, so anything left over from a
40
+ received chunk is buffered until the next call.
41
+ """
42
+
43
+ def __init__(self, inner: StapledObjectStream[bytes]) -> None:
44
+ self._inner = inner
45
+ self._pending = b""
46
+
47
+ @override
48
+ async def receive(self, max_bytes: int = 65536) -> bytes:
49
+ if not self._pending:
50
+ self._pending = await self._inner.receive()
51
+
52
+ chunk, self._pending = self._pending[:max_bytes], self._pending[max_bytes:]
53
+ return chunk
54
+
55
+ @override
56
+ async def send(self, item: bytes) -> None:
57
+ await self._inner.send(item)
58
+
59
+ @override
60
+ async def send_eof(self) -> None:
61
+ await self._inner.send_eof()
62
+
63
+ @override
64
+ async def aclose(self) -> None:
65
+ await self._inner.aclose()
66
+
67
+
68
+ def _pair() -> tuple[ByteStream, ByteStream]:
69
+ """Create two byte streams connected to each other."""
70
+ a_tx, b_rx = anyio.create_memory_object_stream[bytes](_BUFFER)
71
+ b_tx, a_rx = anyio.create_memory_object_stream[bytes](_BUFFER)
72
+ return (
73
+ _MemoryByteStream(StapledObjectStream(a_tx, a_rx)),
74
+ _MemoryByteStream(StapledObjectStream(b_tx, b_rx)),
75
+ )
76
+
77
+
78
+ @final
79
+ class _MemoryListener(IpcListener):
80
+ """Accepts connections handed over through a queue."""
81
+
82
+ def __init__(
83
+ self,
84
+ key: str,
85
+ tx: MemoryObjectSendStream[ByteStream],
86
+ rx: MemoryObjectReceiveStream[ByteStream],
87
+ ) -> None:
88
+ self.key = key
89
+ self.tx = tx
90
+ self._rx = rx
91
+
92
+ @override
93
+ async def serve(self, handler: StreamHandler, task_group: TaskGroup) -> None:
94
+ wrapped = owned(handler)
95
+
96
+ async for stream in self._rx:
97
+ task_group.start_soon(wrapped, stream)
98
+
99
+ @override
100
+ async def aclose(self) -> None:
101
+ self.tx.close()
102
+
103
+ # Close anything accepted but never served, or it leaks on shutdown.
104
+ with suppress(anyio.WouldBlock, anyio.EndOfStream, anyio.ClosedResourceError):
105
+ while True:
106
+ await self._rx.receive_nowait().aclose()
107
+
108
+ self._rx.close()
109
+
110
+
111
+ @final
112
+ class MemoryBackend(IpcBackend):
113
+ """A process-global registry of in-process listeners keyed by name."""
114
+
115
+ _listeners: ClassVar[dict[str, _MemoryListener]] = {}
116
+
117
+ @override
118
+ def connect(self, endpoint: Endpoint) -> AbstractAsyncContextManager[ByteStream]:
119
+ return self._do_connect(endpoint)
120
+
121
+ @asynccontextmanager
122
+ async def _do_connect(self, endpoint: Endpoint) -> AsyncGenerator[ByteStream, None]:
123
+ listener = self._listeners.get(endpoint.target)
124
+ if listener is None:
125
+ raise ConnectionRefusedError(f"no listener at {endpoint}")
126
+
127
+ client, server = _pair()
128
+
129
+ try:
130
+ listener.tx.send_nowait(server)
131
+ except (anyio.WouldBlock, anyio.ClosedResourceError, anyio.BrokenResourceError):
132
+ await client.aclose()
133
+ await server.aclose()
134
+ raise ConnectionRefusedError(
135
+ f"listener at {endpoint} is not accepting"
136
+ ) from None
137
+
138
+ logger.debug("memory: connected to %s", endpoint)
139
+
140
+ async with client:
141
+ yield client
142
+
143
+ @override
144
+ def create_listener(
145
+ self, endpoint: Endpoint
146
+ ) -> AbstractAsyncContextManager[IpcListener]:
147
+ return self._do_listen(endpoint)
148
+
149
+ @asynccontextmanager
150
+ async def _do_listen(self, endpoint: Endpoint) -> AsyncGenerator[IpcListener, None]:
151
+ if endpoint.target in self._listeners:
152
+ raise AddressInUseError(f"address already in use: {endpoint}")
153
+
154
+ tx, rx = anyio.create_memory_object_stream[ByteStream](_BUFFER)
155
+ listener = _MemoryListener(endpoint.target, tx, rx)
156
+ self._listeners[endpoint.target] = listener
157
+ logger.debug("memory: listening on %s", endpoint)
158
+
159
+ try:
160
+ yield listener
161
+ finally:
162
+ del self._listeners[endpoint.target]
163
+ await listener.aclose()
@@ -0,0 +1,118 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 The Pybricks Authors
3
+
4
+ """AF_UNIX transport for Linux and macOS.
5
+
6
+ anyio supports UNIX sockets natively, so this backend is mostly about getting
7
+ the filesystem details right: a private parent directory, mode ``0600`` on the
8
+ socket itself, and safe removal of a socket left behind by a crashed service.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from collections.abc import AsyncGenerator
15
+ from contextlib import AbstractAsyncContextManager, asynccontextmanager, suppress
16
+ from pathlib import Path
17
+ from typing import final
18
+
19
+ import anyio
20
+ import anyio.to_thread
21
+ from anyio.abc import ByteStream
22
+ from typing_extensions import override
23
+
24
+ from pb_core import paths
25
+ from pb_core.ipc.address import Endpoint
26
+ from pb_core.ipc.backend._anyio_listener import AnyioListener
27
+ from pb_core.ipc.backend.provider import IpcBackend, IpcListener
28
+ from pb_core.ipc.errors import AddressInUseError, is_address_in_use
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ __all__ = ["PosixBackend"]
33
+
34
+
35
+ @final
36
+ class PosixBackend(IpcBackend):
37
+ """AF_UNIX sockets."""
38
+
39
+ @override
40
+ def connect(self, endpoint: Endpoint) -> AbstractAsyncContextManager[ByteStream]:
41
+ return self._connect(endpoint)
42
+
43
+ @asynccontextmanager
44
+ async def _connect(self, endpoint: Endpoint) -> AsyncGenerator[ByteStream, None]:
45
+ try:
46
+ stream = await anyio.connect_unix(endpoint.target)
47
+ except FileNotFoundError:
48
+ # No socket file: the service is not running. Callers may start one.
49
+ raise ConnectionRefusedError(f"no service at {endpoint}") from None
50
+
51
+ logger.debug("unix: connected to %s", endpoint)
52
+
53
+ async with stream:
54
+ yield stream
55
+
56
+ @override
57
+ def create_listener(
58
+ self, endpoint: Endpoint
59
+ ) -> AbstractAsyncContextManager[IpcListener]:
60
+ return self._listen(endpoint)
61
+
62
+ @asynccontextmanager
63
+ async def _listen(self, endpoint: Endpoint) -> AsyncGenerator[IpcListener, None]:
64
+ socket = anyio.Path(endpoint.target)
65
+ # Blocking stat/mkdir, so off the event loop thread.
66
+ await anyio.to_thread.run_sync(paths.ensure_private_dir, Path(socket.parent))
67
+
68
+ # A socket file on its own says nothing about whether anyone is behind
69
+ # it, and binding would replace a live service's socket with ours --
70
+ # leaving it running and unreachable. So ask: a connection that is
71
+ # accepted is proof, and anything else means the file is debris from a
72
+ # crash and can go. The service also holds the single-instance lock by
73
+ # this point, which is the real guard; this is what makes the backend
74
+ # honest on its own, and what makes "address already in use" mean the
75
+ # same thing on every transport.
76
+ if await _is_live(endpoint):
77
+ raise AddressInUseError(f"address already in use: {endpoint}")
78
+
79
+ with suppress(FileNotFoundError):
80
+ await socket.unlink()
81
+
82
+ try:
83
+ inner = await anyio.create_unix_listener(socket, mode=0o600)
84
+ except OSError as exc:
85
+ if is_address_in_use(exc):
86
+ raise AddressInUseError(f"address already in use: {endpoint}") from exc
87
+
88
+ raise
89
+
90
+ logger.info("unix: listening on %s", endpoint)
91
+ listener = AnyioListener(inner)
92
+
93
+ try:
94
+ yield listener
95
+ finally:
96
+ await listener.aclose()
97
+
98
+ with suppress(FileNotFoundError):
99
+ await socket.unlink()
100
+
101
+
102
+ async def _is_live(endpoint: Endpoint) -> bool:
103
+ """Whether something is already accepting connections on this socket.
104
+
105
+ Args:
106
+ endpoint: The socket to try.
107
+
108
+ Returns:
109
+ True if a connection was accepted, which only a running service can do.
110
+ """
111
+
112
+ try:
113
+ stream = await anyio.connect_unix(endpoint.target)
114
+ except (FileNotFoundError, ConnectionRefusedError, OSError):
115
+ return False
116
+
117
+ await stream.aclose()
118
+ return True