carla-driver-interface 2.1.0__tar.gz → 2.2.0__tar.gz

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.
Files changed (33) hide show
  1. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/PKG-INFO +30 -1
  2. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/README.md +29 -0
  3. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/pyproject.toml +1 -1
  4. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/pyproject.toml.orig +1 -1
  5. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/src/carla_driver_interface/cli.py +8 -2
  6. carla_driver_interface-2.2.0/src/carla_driver_interface/inprocess.py +709 -0
  7. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/src/carla_driver_interface/server.py +29 -6
  8. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/LICENSE +0 -0
  9. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/src/carla_driver_interface/__init__.py +0 -0
  10. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/src/carla_driver_interface/_proto/__init__.py +0 -0
  11. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/src/carla_driver_interface/_proto/carla_driver_pb2.py +0 -0
  12. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/src/carla_driver_interface/_proto/carla_driver_pb2.pyi +0 -0
  13. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/src/carla_driver_interface/_proto/carla_driver_pb2_grpc.py +0 -0
  14. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/src/carla_driver_interface/_proto/common_pb2.py +0 -0
  15. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/src/carla_driver_interface/_proto/common_pb2.pyi +0 -0
  16. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/src/carla_driver_interface/_proto/egodriver_pb2.py +0 -0
  17. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/src/carla_driver_interface/_proto/egodriver_pb2.pyi +0 -0
  18. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/src/carla_driver_interface/_proto/egodriver_pb2_grpc.py +0 -0
  19. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/src/carla_driver_interface/_proto/sensorsim_pb2.py +0 -0
  20. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/src/carla_driver_interface/_proto/sensorsim_pb2.pyi +0 -0
  21. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/src/carla_driver_interface/alpasim.py +0 -0
  22. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/src/carla_driver_interface/contract.py +0 -0
  23. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/src/carla_driver_interface/driver.py +0 -0
  24. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/src/carla_driver_interface/geometry.py +0 -0
  25. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/src/carla_driver_interface/hdmap.py +0 -0
  26. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/src/carla_driver_interface/policies/__init__.py +0 -0
  27. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/src/carla_driver_interface/policies/constant_speed.py +0 -0
  28. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/src/carla_driver_interface/policies/route_follower.py +0 -0
  29. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/src/carla_driver_interface/polyline.py +0 -0
  30. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/src/carla_driver_interface/protocol.py +0 -0
  31. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/src/carla_driver_interface/py.typed +0 -0
  32. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/src/carla_driver_interface/service.py +0 -0
  33. {carla_driver_interface-2.1.0 → carla_driver_interface-2.2.0}/src/carla_driver_interface/testing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: carla-driver-interface
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: Both ends of alpasim's egodriver gRPC contract: serve a driving policy, or drive one without CARLA
5
5
  Author: Masaya Kataoka
6
6
  Author-email: Masaya Kataoka <ms.kataoka@gmail.com>
@@ -104,6 +104,35 @@ or, in a test, `FakeLoop("localhost:50051", [FakeCamera()])` from
104
104
  `LoopResult`). The ego follows the returned plan exactly, so what is tested is the policy
105
105
  and the wire, not a controller.
106
106
 
107
+ ## Serving on a Unix socket, or in the runtime's own process
108
+
109
+ A server binds a Unix domain socket when its host names one: `serve --host
110
+ unix:///run/policy.sock` (`build_server(policy, host="unix:///run/policy.sock")`; the
111
+ port is unused). A client dials the same address.
112
+
113
+ A runtime that runs the policy in its own process can skip the socket altogether:
114
+
115
+ ```python
116
+ from carla_driver_interface.inprocess import in_process_channel
117
+ from carla_driver_interface.protocol import EgodriverServiceStub
118
+
119
+ # A grpc.Channel; mode="alpasim", as on a server.
120
+ channel = in_process_channel(MyPolicy())
121
+ # The generated stubs, unchanged.
122
+ stub = EgodriverServiceStub(channel)
123
+ ```
124
+
125
+ `InProcessChannel` keeps the contract byte for byte: each request is serialized and parsed
126
+ by the servicer's own deserializer, each response likewise, so both sides see the bytes
127
+ the wire would carry. The servicer is the same `EgodriverServicer`, with a context that
128
+ does what grpc's does (`abort`, `set_code`/`set_details`, an exception as `UNKNOWN`,
129
+ invocation, initial and trailing metadata -- the contract revision is negotiated through
130
+ them), and a failed call is a `grpc.RpcError` with the code and details a server's would
131
+ carry. The test suite runs every wire test over TCP, a Unix socket and this channel and
132
+ compares what the client sees, errors included. Only the socket's own behaviour is
133
+ missing: a call runs on the calling thread, so a deadline is reported, as
134
+ `DEADLINE_EXCEEDED`, when the call returns rather than cutting it short.
135
+
107
136
  ## Running a policy in alpasim
108
137
 
109
138
  alpasim runs its driver as a container its wizard starts, and its runtime drives it over
@@ -88,6 +88,35 @@ or, in a test, `FakeLoop("localhost:50051", [FakeCamera()])` from
88
88
  `LoopResult`). The ego follows the returned plan exactly, so what is tested is the policy
89
89
  and the wire, not a controller.
90
90
 
91
+ ## Serving on a Unix socket, or in the runtime's own process
92
+
93
+ A server binds a Unix domain socket when its host names one: `serve --host
94
+ unix:///run/policy.sock` (`build_server(policy, host="unix:///run/policy.sock")`; the
95
+ port is unused). A client dials the same address.
96
+
97
+ A runtime that runs the policy in its own process can skip the socket altogether:
98
+
99
+ ```python
100
+ from carla_driver_interface.inprocess import in_process_channel
101
+ from carla_driver_interface.protocol import EgodriverServiceStub
102
+
103
+ # A grpc.Channel; mode="alpasim", as on a server.
104
+ channel = in_process_channel(MyPolicy())
105
+ # The generated stubs, unchanged.
106
+ stub = EgodriverServiceStub(channel)
107
+ ```
108
+
109
+ `InProcessChannel` keeps the contract byte for byte: each request is serialized and parsed
110
+ by the servicer's own deserializer, each response likewise, so both sides see the bytes
111
+ the wire would carry. The servicer is the same `EgodriverServicer`, with a context that
112
+ does what grpc's does (`abort`, `set_code`/`set_details`, an exception as `UNKNOWN`,
113
+ invocation, initial and trailing metadata -- the contract revision is negotiated through
114
+ them), and a failed call is a `grpc.RpcError` with the code and details a server's would
115
+ carry. The test suite runs every wire test over TCP, a Unix socket and this channel and
116
+ compares what the client sees, errors included. Only the socket's own behaviour is
117
+ missing: a call runs on the calling thread, so a deadline is reported, as
118
+ `DEADLINE_EXCEEDED`, when the call returns rather than cutting it short.
119
+
91
120
  ## Running a policy in alpasim
92
121
 
93
122
  alpasim runs its driver as a container its wizard starts, and its runtime drives it over
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "carla-driver-interface"
3
- version = "2.1.0"
3
+ version = "2.2.0"
4
4
  description = "Both ends of alpasim's egodriver gRPC contract: serve a driving policy, or drive one without CARLA"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "carla-driver-interface"
3
- version = "2.1.0"
3
+ version = "2.2.0"
4
4
  description = "Both ends of alpasim's egodriver gRPC contract: serve a driving policy, or drive one without CARLA"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -56,7 +56,11 @@ def main(argv: Optional[List[str]] = None) -> int:
56
56
 
57
57
  serve = commands.add_parser("serve", help="serve a policy over gRPC")
58
58
  _add_policy_arguments(serve, os.environ.get(POLICY_ENV, "route_follower"))
59
- serve.add_argument("--host", default="0.0.0.0")
59
+ serve.add_argument(
60
+ "--host",
61
+ default="0.0.0.0",
62
+ help="address to bind, or a Unix socket (unix:///path/policy.sock; --port unused)",
63
+ )
60
64
  serve.add_argument("--port", type=int, default=50051)
61
65
  serve.add_argument("--cruise-speed", type=float, default=None, help="m/s")
62
66
  serve.add_argument(
@@ -76,7 +80,9 @@ def main(argv: Optional[List[str]] = None) -> int:
76
80
  demo = commands.add_parser(
77
81
  "demo", help="drive a policy server through the CARLA-free fake loop"
78
82
  )
79
- demo.add_argument("--driver", default="localhost:50051", help="policy host:port")
83
+ demo.add_argument(
84
+ "--driver", default="localhost:50051", help="policy host:port, or unix:///path"
85
+ )
80
86
  demo.add_argument("--steps", type=int, default=100)
81
87
  demo.add_argument("--step", type=float, default=0.1, help="seconds per drive call")
82
88
  demo.add_argument("--camera-width", type=int, default=FakeCamera.width)
@@ -0,0 +1,709 @@
1
+ """The egodriver contract without a socket: a gRPC channel that calls the servicer.
2
+
3
+ A runtime that serves its policy from its own process (autoware_carla_scenario's
4
+ ``driver.policy``) still pays for TCP: every drive's megabytes of LiDAR are copied
5
+ through the loopback and HTTP/2 framing twice. :class:`InProcessChannel` is a
6
+ :class:`grpc.Channel` that skips the transport and nothing else:
7
+
8
+ * the stubs are the generated ones (``EgodriverServiceStub``,
9
+ ``CarlaVehicleServiceStub``), and the servicer registers on the channel with the
10
+ generated ``add_*_to_server`` exactly as on a server, so method names and
11
+ (de)serializers are the generated code's;
12
+ * every request is serialized to bytes and parsed from them by the servicer's
13
+ deserializer, and every response likewise -- the bytes each side sees are the
14
+ bytes it would see on the wire, never a shared message object;
15
+ * the servicer gets a :class:`grpc.ServicerContext` with the semantics it can
16
+ observe on a real server: ``abort`` ends the call with its code and details,
17
+ an exception becomes ``UNKNOWN`` ("Exception calling application: ..."),
18
+ ``set_code`` / ``set_details`` fail a call that returns, invocation metadata,
19
+ ``send_initial_metadata`` (once) and trailing metadata reach the other side
20
+ (the contract revision is negotiated through them,
21
+ :mod:`carla_driver_interface.contract`);
22
+ * the client sees a failed call as a :class:`grpc.RpcError` that is also a
23
+ :class:`grpc.Call` (``code()``, ``details()``, both metadata), as grpc raises
24
+ it; an unknown method is ``UNIMPLEMENTED`` ("Method not found!"), an
25
+ oversized request ``RESOURCE_EXHAUSTED``, a call on a closed channel a
26
+ ``ValueError``.
27
+
28
+ What differs is only what a socket adds: the servicer runs on the calling
29
+ thread, so a deadline cannot cut a call short -- a call that overran its
30
+ ``timeout`` fails with ``DEADLINE_EXCEEDED`` when it returns, its effects done,
31
+ as a server that finishes after the client gave up has done them too.
32
+
33
+ Use :func:`in_process_channel` for a policy, as :func:`~carla_driver_interface.server.build_server`
34
+ serves one::
35
+
36
+ channel = in_process_channel(policy)
37
+ stub = EgodriverServiceStub(channel)
38
+ """
39
+
40
+ from __future__ import annotations
41
+
42
+ import collections
43
+ import logging
44
+ import threading
45
+ import time
46
+ from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple
47
+
48
+ import grpc
49
+
50
+ from .driver import BaseDriver
51
+ from .protocol import (
52
+ MAX_MESSAGE_BYTES,
53
+ add_CarlaVehicleServiceServicer_to_server,
54
+ add_EgodriverServiceServicer_to_server,
55
+ channel_options,
56
+ )
57
+ from .service import EgodriverServicer, ServiceMode
58
+
59
+ logger = logging.getLogger(__name__)
60
+
61
+ __all__ = ["InProcessChannel", "InProcessRpcError", "in_process_channel"]
62
+
63
+ #: What :meth:`grpc.ServicerContext.peer` answers on this channel.
64
+ PEER = "in-process"
65
+
66
+ Metadata = Tuple[Tuple[str, Any], ...]
67
+
68
+
69
+ class _Metadatum(collections.namedtuple("_Metadatum", ("key", "value"))):
70
+ """One metadata entry, as grpc hands them out (``key``, ``value``, a pair)."""
71
+
72
+
73
+ def _metadata(entries: Optional[Iterable[Tuple[str, Any]]]) -> Metadata:
74
+ """*entries* as grpc delivers them: checked as grpc checks them, then frozen.
75
+
76
+ Raises:
77
+ TypeError: A value grpc refuses before sending (not ``str``/``bytes``,
78
+ or ``str`` under a ``-bin`` key).
79
+ InProcessRpcError: ``INTERNAL`` for a key HTTP/2 cannot carry.
80
+ """
81
+ out = []
82
+ for key, value in entries or ():
83
+ if key.endswith("-bin"):
84
+ if not isinstance(value, bytes):
85
+ raise TypeError(
86
+ f'Binary metadata key="{key}" expected bytes, got {type(value)}'
87
+ )
88
+ elif not isinstance(value, (str, bytes)):
89
+ raise TypeError(f"Expected str, not {type(value)}")
90
+ if not _legal_key(key):
91
+ raise InProcessRpcError(grpc.StatusCode.INTERNAL, "Invalid metadata")
92
+ if isinstance(value, bytes) and not key.endswith("-bin"):
93
+ value = value.decode("ascii")
94
+ out.append(_Metadatum(key, value))
95
+ return tuple(out)
96
+
97
+
98
+ _KEY_CHARS = frozenset("abcdefghijklmnopqrstuvwxyz0123456789-_.")
99
+
100
+
101
+ def _legal_key(key: str) -> bool:
102
+ # HTTP/2 header names, lower case; grpc reserves the ``grpc-`` prefix.
103
+ return bool(key) and set(key) <= _KEY_CHARS and not key.startswith("grpc-")
104
+
105
+
106
+ class InProcessRpcError(grpc.RpcError, grpc.Call, grpc.Future):
107
+ """A failed call, as grpc raises one: an :class:`grpc.RpcError`, a :class:`grpc.Call`
108
+ and a done :class:`grpc.Future` (an interceptor's continuation returns it as the
109
+ call's outcome)."""
110
+
111
+ def __init__(
112
+ self,
113
+ code: grpc.StatusCode,
114
+ details: str,
115
+ initial_metadata: Metadata = (),
116
+ trailing_metadata: Metadata = (),
117
+ ) -> None:
118
+ super().__init__(details)
119
+ self._code = code
120
+ self._details = details
121
+ self._initial_metadata = initial_metadata
122
+ self._trailing_metadata = trailing_metadata
123
+
124
+ def code(self) -> grpc.StatusCode:
125
+ return self._code
126
+
127
+ def details(self) -> str:
128
+ return self._details
129
+
130
+ def initial_metadata(self) -> Metadata:
131
+ return self._initial_metadata
132
+
133
+ def trailing_metadata(self) -> Metadata:
134
+ return self._trailing_metadata
135
+
136
+ def debug_error_string(self) -> str:
137
+ return f"in-process call failed: {self._code.name}: {self._details}"
138
+
139
+ def is_active(self) -> bool:
140
+ return False
141
+
142
+ def time_remaining(self) -> Optional[float]:
143
+ return None
144
+
145
+ def cancel(self) -> bool:
146
+ return False
147
+
148
+ def add_callback(self, callback: Callable[[], None]) -> bool:
149
+ return False
150
+
151
+ # -- grpc.Future, already done -------------------------------------------------
152
+
153
+ def cancelled(self) -> bool:
154
+ return False
155
+
156
+ def running(self) -> bool:
157
+ return False
158
+
159
+ def done(self) -> bool:
160
+ return True
161
+
162
+ def result(self, timeout: Optional[float] = None) -> Any:
163
+ raise self
164
+
165
+ def exception(self, timeout: Optional[float] = None) -> Optional[Exception]:
166
+ return self
167
+
168
+ def traceback(self, timeout: Optional[float] = None) -> Any:
169
+ return self.__traceback__
170
+
171
+ def add_done_callback(self, fn: Callable[[Any], None]) -> None:
172
+ fn(self)
173
+
174
+ def __str__(self) -> str:
175
+ return (
176
+ f"<{type(self).__name__} of RPC that terminated with:\n"
177
+ f'\tstatus = {self._code}\n\tdetails = "{self._details}"\n>'
178
+ )
179
+
180
+ __repr__ = __str__
181
+
182
+
183
+ class _Aborted(Exception):
184
+ """Raised by :meth:`_ServicerContext.abort`; grpc raises a bare ``Exception``."""
185
+
186
+
187
+ class _ServicerContext(grpc.ServicerContext):
188
+ """The server half of one call: what a servicer can see of grpc's own context."""
189
+
190
+ def __init__(self, metadata: Metadata, deadline: Optional[float]) -> None:
191
+ self._invocation_metadata = metadata
192
+ self._deadline = deadline
193
+ self._lock = threading.Lock()
194
+ self.initial_metadata: Optional[Metadata] = None
195
+ self.trailing: Optional[Metadata] = None
196
+ self.status_code: Optional[grpc.StatusCode] = None
197
+ self.status_details: Optional[str] = None
198
+ self.aborted = False
199
+ self._callbacks: List[Callable[[], None]] = []
200
+ self._active = True
201
+
202
+ # -- grpc.RpcContext -----------------------------------------------------
203
+
204
+ def is_active(self) -> bool:
205
+ return self._active and (self._deadline is None or time.time() < self._deadline)
206
+
207
+ def time_remaining(self) -> Optional[float]:
208
+ if self._deadline is None:
209
+ return None
210
+ return max(self._deadline - time.time(), 0.0)
211
+
212
+ def cancel(self) -> None:
213
+ with self._lock:
214
+ self.status_code = grpc.StatusCode.CANCELLED
215
+ self.status_details = "Cancelled"
216
+ self.aborted = True
217
+ raise _Aborted()
218
+
219
+ def add_callback(self, callback: Callable[[], None]) -> bool:
220
+ with self._lock:
221
+ if not self._active:
222
+ return False
223
+ self._callbacks.append(callback)
224
+ return True
225
+
226
+ # -- grpc.ServicerContext --------------------------------------------------
227
+
228
+ def invocation_metadata(self) -> Metadata:
229
+ return self._invocation_metadata
230
+
231
+ def peer(self) -> str:
232
+ return PEER
233
+
234
+ def peer_identities(self) -> Optional[Iterable[bytes]]:
235
+ return None
236
+
237
+ def peer_identity_key(self) -> Optional[str]:
238
+ return None
239
+
240
+ def auth_context(self) -> Dict[str, Iterable[bytes]]:
241
+ return {}
242
+
243
+ def set_compression(self, compression: grpc.Compression) -> None:
244
+ pass
245
+
246
+ def disable_next_message_compression(self) -> None:
247
+ pass
248
+
249
+ def send_initial_metadata(
250
+ self, initial_metadata: Iterable[Tuple[str, Any]]
251
+ ) -> None:
252
+ with self._lock:
253
+ if self.initial_metadata is not None:
254
+ raise ValueError("Initial metadata no longer allowed!")
255
+ self.initial_metadata = _metadata(initial_metadata)
256
+
257
+ def set_trailing_metadata(
258
+ self, trailing_metadata: Iterable[Tuple[str, Any]]
259
+ ) -> None:
260
+ with self._lock:
261
+ self.trailing = _metadata(trailing_metadata)
262
+
263
+ def trailing_metadata(self) -> Optional[Metadata]:
264
+ return self.trailing
265
+
266
+ def abort(self, code: grpc.StatusCode, details: str) -> None:
267
+ if code == grpc.StatusCode.OK:
268
+ logger.error("abort() called with StatusCode.OK; returning UNKNOWN")
269
+ code, details = grpc.StatusCode.UNKNOWN, ""
270
+ with self._lock:
271
+ self.status_code = code
272
+ self.status_details = details
273
+ self.aborted = True
274
+ raise _Aborted()
275
+
276
+ def abort_with_status(self, status: grpc.Status) -> None:
277
+ self.trailing = _metadata(status.trailing_metadata)
278
+ self.abort(status.code, status.details)
279
+
280
+ def set_code(self, code: grpc.StatusCode) -> None:
281
+ with self._lock:
282
+ self.status_code = code
283
+
284
+ def code(self) -> Optional[grpc.StatusCode]:
285
+ return self.status_code
286
+
287
+ def set_details(self, details: str) -> None:
288
+ with self._lock:
289
+ self.status_details = details
290
+
291
+ def details(self) -> Optional[str]:
292
+ return self.status_details
293
+
294
+ # -- the call's end ----------------------------------------------------------
295
+
296
+ def finish(self) -> None:
297
+ with self._lock:
298
+ self._active = False
299
+ callbacks, self._callbacks = self._callbacks, []
300
+ for callback in callbacks:
301
+ try:
302
+ callback()
303
+ except Exception: # noqa: BLE001 - grpc logs and carries on too
304
+ logger.exception("a call's termination callback raised")
305
+
306
+ def failure(self, code: grpc.StatusCode, details: str) -> InProcessRpcError:
307
+ """The client's error for a call that ended here; the context's own status wins."""
308
+ return InProcessRpcError(
309
+ self.status_code or code,
310
+ details if self.status_details is None else self.status_details,
311
+ self.initial_metadata or (),
312
+ self.trailing or (),
313
+ )
314
+
315
+
316
+ class _Call(grpc.Call):
317
+ """A completed call's outcome as ``with_call`` hands it back."""
318
+
319
+ def __init__(self, initial_metadata: Metadata, trailing_metadata: Metadata) -> None:
320
+ self._initial_metadata = initial_metadata
321
+ self._trailing_metadata = trailing_metadata
322
+
323
+ def initial_metadata(self) -> Metadata:
324
+ return self._initial_metadata
325
+
326
+ def trailing_metadata(self) -> Metadata:
327
+ return self._trailing_metadata
328
+
329
+ def code(self) -> grpc.StatusCode:
330
+ return grpc.StatusCode.OK
331
+
332
+ def details(self) -> str:
333
+ return ""
334
+
335
+ def is_active(self) -> bool:
336
+ return False
337
+
338
+ def time_remaining(self) -> Optional[float]:
339
+ return None
340
+
341
+ def cancel(self) -> bool:
342
+ return False
343
+
344
+ def add_callback(self, callback: Callable[[], None]) -> bool:
345
+ return False
346
+
347
+
348
+ class _Future(grpc.Future, grpc.Call):
349
+ """``future()``'s answer: already done, since the call ran on this thread."""
350
+
351
+ def __init__(
352
+ self, response: Any, call: Optional[_Call], error: Optional[InProcessRpcError]
353
+ ):
354
+ self._response = response
355
+ self._call = call
356
+ self._error = error
357
+
358
+ def cancel(self) -> bool:
359
+ return False
360
+
361
+ def cancelled(self) -> bool:
362
+ return False
363
+
364
+ def running(self) -> bool:
365
+ return False
366
+
367
+ def done(self) -> bool:
368
+ return True
369
+
370
+ def result(self, timeout: Optional[float] = None) -> Any:
371
+ if self._error is not None:
372
+ raise self._error
373
+ return self._response
374
+
375
+ def exception(self, timeout: Optional[float] = None) -> Optional[Exception]:
376
+ return self._error
377
+
378
+ def traceback(self, timeout: Optional[float] = None) -> Any:
379
+ return None if self._error is None else self._error.__traceback__
380
+
381
+ def add_done_callback(self, fn: Callable[[Any], None]) -> None:
382
+ fn(self)
383
+
384
+ def _outcome(self) -> grpc.Call:
385
+ return self._error if self._error is not None else self._call # type: ignore[return-value]
386
+
387
+ def initial_metadata(self) -> Metadata:
388
+ return self._outcome().initial_metadata()
389
+
390
+ def trailing_metadata(self) -> Metadata:
391
+ return self._outcome().trailing_metadata()
392
+
393
+ def code(self) -> grpc.StatusCode:
394
+ return self._outcome().code()
395
+
396
+ def details(self) -> str:
397
+ return self._outcome().details()
398
+
399
+ def is_active(self) -> bool:
400
+ return False
401
+
402
+ def time_remaining(self) -> Optional[float]:
403
+ return None
404
+
405
+ def add_callback(self, callback: Callable[[], None]) -> bool:
406
+ return False
407
+
408
+
409
+ class _UnaryUnary(grpc.UnaryUnaryMultiCallable):
410
+ def __init__(
411
+ self,
412
+ channel: "InProcessChannel",
413
+ method: str,
414
+ request_serializer: Optional[Callable[[Any], bytes]],
415
+ response_deserializer: Optional[Callable[[bytes], Any]],
416
+ ) -> None:
417
+ self._channel = channel
418
+ self._method = method
419
+ self._serialize = request_serializer
420
+ self._deserialize = response_deserializer
421
+
422
+ def __call__(
423
+ self,
424
+ request: Any,
425
+ timeout: Optional[float] = None,
426
+ metadata: Optional[Sequence[Tuple[str, Any]]] = None,
427
+ credentials: Optional[grpc.CallCredentials] = None,
428
+ wait_for_ready: Optional[bool] = None,
429
+ compression: Optional[grpc.Compression] = None,
430
+ ) -> Any:
431
+ response, _ = self.with_call(request, timeout, metadata)
432
+ return response
433
+
434
+ def with_call(
435
+ self,
436
+ request: Any,
437
+ timeout: Optional[float] = None,
438
+ metadata: Optional[Sequence[Tuple[str, Any]]] = None,
439
+ credentials: Optional[grpc.CallCredentials] = None,
440
+ wait_for_ready: Optional[bool] = None,
441
+ compression: Optional[grpc.Compression] = None,
442
+ ) -> Tuple[Any, grpc.Call]:
443
+ return self._channel._invoke(
444
+ self._method, request, self._serialize, self._deserialize, timeout, metadata
445
+ )
446
+
447
+ def future(
448
+ self,
449
+ request: Any,
450
+ timeout: Optional[float] = None,
451
+ metadata: Optional[Sequence[Tuple[str, Any]]] = None,
452
+ credentials: Optional[grpc.CallCredentials] = None,
453
+ wait_for_ready: Optional[bool] = None,
454
+ compression: Optional[grpc.Compression] = None,
455
+ ) -> _Future:
456
+ try:
457
+ response, call = self.with_call(request, timeout, metadata)
458
+ except InProcessRpcError as error:
459
+ return _Future(None, None, error)
460
+ return _Future(response, call, None) # type: ignore[arg-type]
461
+
462
+
463
+ class _HandlerCallDetails(
464
+ collections.namedtuple("_HandlerCallDetails", ("method", "invocation_metadata")),
465
+ grpc.HandlerCallDetails,
466
+ ):
467
+ pass
468
+
469
+
470
+ def _option(options: Sequence[Tuple[str, Any]], name: str, default: int) -> int:
471
+ value = dict(options).get(name, default)
472
+ return int(value) if int(value) >= 0 else 2**63
473
+
474
+
475
+ class InProcessChannel(grpc.Channel):
476
+ """A :class:`grpc.Channel` whose unary calls run the registered servicers here.
477
+
478
+ It is its own server too: a servicer registers on it with the generated
479
+ ``add_*_to_server(servicer, channel)``, as on a :class:`grpc.Server`.
480
+
481
+ Args:
482
+ options: Channel options; only the message size limits are read
483
+ (``grpc.max_send_message_length``, ``grpc.max_receive_message_length``),
484
+ :func:`~carla_driver_interface.protocol.channel_options` by default,
485
+ as both ends of this contract use.
486
+ """
487
+
488
+ def __init__(self, options: Optional[Sequence[Tuple[str, Any]]] = None) -> None:
489
+ options = channel_options() if options is None else list(options)
490
+ self._max_send = _option(
491
+ options, "grpc.max_send_message_length", MAX_MESSAGE_BYTES
492
+ )
493
+ self._max_receive = _option(
494
+ options, "grpc.max_receive_message_length", MAX_MESSAGE_BYTES
495
+ )
496
+ self._generic_handlers: List[grpc.GenericRpcHandler] = []
497
+ self._registered: Dict[str, grpc.RpcMethodHandler] = {}
498
+ self._closed = False
499
+
500
+ # -- the server half ---------------------------------------------------------
501
+
502
+ def add_generic_rpc_handlers(
503
+ self, generic_rpc_handlers: Iterable[grpc.GenericRpcHandler]
504
+ ) -> None:
505
+ """Register handlers, as :meth:`grpc.Server.add_generic_rpc_handlers` does."""
506
+ self._generic_handlers.extend(generic_rpc_handlers)
507
+
508
+ def add_registered_method_handlers(
509
+ self, service_name: str, method_handlers: Dict[str, grpc.RpcMethodHandler]
510
+ ) -> None:
511
+ """Register handlers by method, as newer generated code also does on a server."""
512
+ for name, handler in method_handlers.items():
513
+ self._registered[f"/{service_name}/{name}"] = handler
514
+
515
+ def _handler(
516
+ self, method: str, metadata: Metadata
517
+ ) -> Optional[grpc.RpcMethodHandler]:
518
+ if method in self._registered:
519
+ return self._registered[method]
520
+ details = _HandlerCallDetails(method, metadata)
521
+ for generic in self._generic_handlers:
522
+ handler = generic.service(details)
523
+ if handler is not None:
524
+ return handler
525
+ return None
526
+
527
+ # -- one call ----------------------------------------------------------------
528
+
529
+ def _invoke(
530
+ self,
531
+ method: str,
532
+ request: Any,
533
+ serialize: Optional[Callable[[Any], bytes]],
534
+ deserialize: Optional[Callable[[bytes], Any]],
535
+ timeout: Optional[float],
536
+ metadata: Optional[Sequence[Tuple[str, Any]]],
537
+ ) -> Tuple[Any, grpc.Call]:
538
+ if self._closed:
539
+ raise ValueError("Cannot invoke RPC on closed channel!")
540
+ invocation = _metadata(metadata)
541
+ started = time.time()
542
+ # grpc's Python client reads timeout=0 as no deadline at all.
543
+ deadline = started + timeout if timeout else None
544
+ if timeout is not None and timeout < 0:
545
+ raise InProcessRpcError(
546
+ grpc.StatusCode.DEADLINE_EXCEEDED, "Deadline Exceeded"
547
+ )
548
+ try:
549
+ request_bytes = request if serialize is None else serialize(request)
550
+ except Exception: # noqa: BLE001 - grpc reports any serializer failure
551
+ raise InProcessRpcError(
552
+ grpc.StatusCode.INTERNAL, "Exception serializing request!"
553
+ ) from None
554
+ if len(request_bytes) > self._max_send:
555
+ raise InProcessRpcError(
556
+ grpc.StatusCode.RESOURCE_EXHAUSTED,
557
+ f"CLIENT: Sent message larger than max ({len(request_bytes)} vs. "
558
+ f"{self._max_send})",
559
+ )
560
+ handler = self._handler(method, invocation)
561
+ if handler is None:
562
+ raise InProcessRpcError(grpc.StatusCode.UNIMPLEMENTED, "Method not found!")
563
+ if handler.unary_unary is None:
564
+ raise NotImplementedError(
565
+ f"{method} is not unary-unary; the in-process channel serves unary calls only"
566
+ )
567
+
568
+ try:
569
+ response_bytes = self._serve(handler, request_bytes, invocation, deadline)
570
+ except InProcessRpcError:
571
+ # Over the wire the client has given up by the time a late abort is
572
+ # sent, so it sees DEADLINE_EXCEEDED, not the servicer's status.
573
+ if deadline is not None and time.time() > deadline:
574
+ raise InProcessRpcError(
575
+ grpc.StatusCode.DEADLINE_EXCEEDED, "Deadline Exceeded"
576
+ ) from None
577
+ raise
578
+ if deadline is not None and time.time() > deadline:
579
+ # The servicer ran to the end on this thread; the client gave up.
580
+ raise InProcessRpcError(
581
+ grpc.StatusCode.DEADLINE_EXCEEDED, "Deadline Exceeded"
582
+ )
583
+ context, payload = response_bytes
584
+ initial = context.initial_metadata or ()
585
+ trailing = context.trailing or ()
586
+ if len(payload) > self._max_receive:
587
+ raise InProcessRpcError(
588
+ grpc.StatusCode.RESOURCE_EXHAUSTED,
589
+ f"CLIENT: Received message larger than max ({len(payload)} vs. "
590
+ f"{self._max_receive})",
591
+ initial,
592
+ trailing,
593
+ )
594
+ try:
595
+ response = payload if deserialize is None else deserialize(payload)
596
+ except Exception: # noqa: BLE001
597
+ raise InProcessRpcError(
598
+ grpc.StatusCode.INTERNAL,
599
+ "Exception deserializing response!",
600
+ initial,
601
+ trailing,
602
+ ) from None
603
+ return response, _Call(initial, trailing)
604
+
605
+ def _serve(
606
+ self,
607
+ handler: grpc.RpcMethodHandler,
608
+ request_bytes: bytes,
609
+ invocation: Metadata,
610
+ deadline: Optional[float],
611
+ ) -> Tuple[_ServicerContext, bytes]:
612
+ """The server's half: parse, call, serialize; an error ends it as grpc's would."""
613
+ context = _ServicerContext(invocation, deadline)
614
+ try:
615
+ try:
616
+ request = (
617
+ request_bytes
618
+ if handler.request_deserializer is None
619
+ else handler.request_deserializer(request_bytes)
620
+ )
621
+ except Exception: # noqa: BLE001
622
+ raise context.failure(
623
+ grpc.StatusCode.INTERNAL, "Exception deserializing request!"
624
+ ) from None
625
+ try:
626
+ response = handler.unary_unary(request, context)
627
+ except Exception as exception: # noqa: BLE001 - as grpc's server
628
+ if context.aborted:
629
+ raise context.failure(
630
+ grpc.StatusCode.UNKNOWN, "RPC Aborted"
631
+ ) from None
632
+ details = f"Exception calling application: {exception}"
633
+ logger.exception(details)
634
+ raise context.failure(grpc.StatusCode.UNKNOWN, details) from None
635
+ if context.aborted:
636
+ # A servicer that swallowed its own abort still aborted.
637
+ raise context.failure(grpc.StatusCode.UNKNOWN, "RPC Aborted")
638
+ try:
639
+ payload = (
640
+ response
641
+ if handler.response_serializer is None
642
+ else handler.response_serializer(response)
643
+ )
644
+ except Exception: # noqa: BLE001
645
+ payload = None
646
+ if payload is None:
647
+ raise context.failure(
648
+ grpc.StatusCode.INTERNAL, "Failed to serialize response!"
649
+ )
650
+ if context.status_code not in (None, grpc.StatusCode.OK):
651
+ raise context.failure(context.status_code, "")
652
+ return context, payload
653
+ finally:
654
+ context.finish()
655
+
656
+ # -- grpc.Channel ------------------------------------------------------------
657
+
658
+ def unary_unary(
659
+ self,
660
+ method: str,
661
+ request_serializer: Optional[Callable[[Any], bytes]] = None,
662
+ response_deserializer: Optional[Callable[[bytes], Any]] = None,
663
+ _registered_method: Optional[bool] = False,
664
+ ) -> grpc.UnaryUnaryMultiCallable:
665
+ return _UnaryUnary(self, method, request_serializer, response_deserializer)
666
+
667
+ def unary_stream(self, method: str, *args: Any, **kwargs: Any) -> Any:
668
+ raise NotImplementedError("the in-process channel serves unary calls only")
669
+
670
+ def stream_unary(self, method: str, *args: Any, **kwargs: Any) -> Any:
671
+ raise NotImplementedError("the in-process channel serves unary calls only")
672
+
673
+ def stream_stream(self, method: str, *args: Any, **kwargs: Any) -> Any:
674
+ raise NotImplementedError("the in-process channel serves unary calls only")
675
+
676
+ def subscribe(
677
+ self,
678
+ callback: Callable[[grpc.ChannelConnectivity], None],
679
+ try_to_connect: bool = False,
680
+ ) -> None:
681
+ callback(grpc.ChannelConnectivity.READY)
682
+
683
+ def unsubscribe(self, callback: Callable[[grpc.ChannelConnectivity], None]) -> None:
684
+ pass
685
+
686
+ def close(self) -> None:
687
+ self._closed = True
688
+
689
+ def __enter__(self) -> "InProcessChannel":
690
+ return self
691
+
692
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
693
+ self.close()
694
+
695
+
696
+ def in_process_channel(
697
+ driver: BaseDriver, mode: ServiceMode = "carla"
698
+ ) -> InProcessChannel:
699
+ """A channel serving ``driver`` in this process, as :func:`~carla_driver_interface.server.build_server` serves it.
700
+
701
+ The same :class:`~carla_driver_interface.service.EgodriverServicer`, registered
702
+ for both services a server carries (``egodriver.EgodriverService`` and
703
+ ``carla_driver.v0.CarlaVehicleService``).
704
+ """
705
+ channel = InProcessChannel(channel_options())
706
+ servicer = EgodriverServicer(driver, mode)
707
+ add_EgodriverServiceServicer_to_server(servicer, channel)
708
+ add_CarlaVehicleServiceServicer_to_server(servicer, channel)
709
+ return channel
@@ -21,7 +21,17 @@ from .service import EgodriverServicer, ServiceMode
21
21
 
22
22
  logger = logging.getLogger(__name__)
23
23
 
24
- __all__ = ["build_server", "run_server", "serving"]
24
+ __all__ = ["UNIX_SCHEMES", "bind_address", "build_server", "run_server", "serving"]
25
+
26
+ #: gRPC address schemes naming a Unix domain socket rather than a TCP host.
27
+ UNIX_SCHEMES = ("unix:", "unix-abstract:")
28
+
29
+
30
+ def bind_address(host: str, port: int) -> str:
31
+ """The address :func:`build_server` binds: ``host:port``, or ``host`` itself
32
+ when it names a Unix domain socket (``unix:///run/policy.sock``,
33
+ ``unix:relative.sock``, ``unix-abstract:name``), which has no port."""
34
+ return host if host.startswith(UNIX_SCHEMES) else f"{host}:{port}"
25
35
 
26
36
 
27
37
  def build_server(
@@ -36,6 +46,10 @@ def build_server(
36
46
  Returns the server and the bound port, which differs from ``port`` when ``port``
37
47
  is 0 -- useful for tests. ``mode`` names the runtime on the other end
38
48
  (:class:`~carla_driver_interface.service.EgodriverServicer`).
49
+
50
+ ``host`` may instead be a Unix domain socket address (``unix:///path``, see
51
+ :func:`bind_address`); ``port`` is then unused and the bound port is 0. A
52
+ client dials the same address.
39
53
  """
40
54
  server = grpc.server(
41
55
  futures.ThreadPoolExecutor(max_workers=max_workers), options=channel_options()
@@ -44,10 +58,15 @@ def build_server(
44
58
  add_EgodriverServiceServicer_to_server(servicer, server)
45
59
  # The vehicle command the egodriver contract has no field for.
46
60
  add_CarlaVehicleServiceServicer_to_server(servicer, server)
47
- bound_port = server.add_insecure_port(f"{host}:{port}")
61
+ address = bind_address(host, port)
62
+ try:
63
+ bound_port = server.add_insecure_port(address)
64
+ except RuntimeError:
65
+ bound_port = 0
48
66
  if bound_port == 0:
49
- raise RuntimeError(f"failed to bind {host}:{port}")
50
- return server, bound_port
67
+ raise RuntimeError(f"failed to bind {address}")
68
+ # A Unix socket has no port; grpc answers 1 for one that bound.
69
+ return server, 0 if host.startswith(UNIX_SCHEMES) else bound_port
51
70
 
52
71
 
53
72
  @contextmanager
@@ -58,13 +77,17 @@ def serving(
58
77
  max_workers: int = 8,
59
78
  mode: ServiceMode = "carla",
60
79
  ) -> Iterator[int]:
61
- """Run a server for ``driver`` for the duration of the block; yields its port."""
80
+ """Run a server for ``driver`` for the duration of the block; yields its port
81
+ (0 on a Unix socket, :func:`build_server`)."""
62
82
  server, bound_port = build_server(
63
83
  driver, port=port, host=host, max_workers=max_workers, mode=mode
64
84
  )
65
85
  server.start()
66
86
  logger.info(
67
- "policy %r serving on %s:%d (%s mode)", driver.name, host, bound_port, mode
87
+ "policy %r serving on %s (%s mode)",
88
+ driver.name,
89
+ bind_address(host, bound_port),
90
+ mode,
68
91
  )
69
92
  try:
70
93
  yield bound_port