| # Copyright 2023 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import inspect |
| import logging |
| import struct |
| from inspect import getframeinfo, stack |
| from typing import Any |
| |
| import fuchsia_controller_py as fc |
| |
| from ._fidl_common import ( |
| DomainError, |
| FidlMessage, |
| FidlMeta, |
| FrameworkError, |
| parse_ordinal, |
| parse_txid, |
| ) |
| from ._ipc import GlobalHandleWaker, HandleWaker |
| from ._registry import get_registered_method |
| |
| # Rather than make a long server UUID, this will be a monotonically increasing |
| # ID to differentiate servers for debugging purposes. |
| _SERVER_ID = 0 |
| _LOGGER = logging.getLogger("fidl.server") |
| |
| |
| class ServerError(Exception): |
| pass |
| |
| |
| class ServerBase( |
| metaclass=FidlMeta, |
| required_class_variables=[ |
| ("library", str), |
| ("method_map", dict), |
| ], |
| ): |
| """Base object for doing basic FIDL server tasks.""" |
| |
| _channel: fc.Channel | None |
| library: str |
| method_map: dict[int, Any] |
| |
| def __str__(self) -> str: |
| return f"server:{type(self).__name__}:{id(self)}" |
| |
| def __init__( |
| self, |
| channel: fc.Channel, |
| channel_waker: HandleWaker | None = None, |
| ) -> None: |
| global _SERVER_ID |
| self._channel = channel |
| self.id = _SERVER_ID |
| _SERVER_ID += 1 |
| if channel_waker is None: |
| self._channel_waker: HandleWaker = GlobalHandleWaker() |
| else: |
| self._channel_waker = channel_waker |
| caller = getframeinfo(stack()[1][0]) |
| _LOGGER.debug( |
| f"{self} instantiated from {caller.filename}:{caller.lineno}" |
| ) |
| |
| async def serve(self) -> None: |
| if self._channel is None: |
| raise ValueError("Channel is already closed") |
| |
| try: |
| with self._channel_waker.registration( |
| self._channel, name=str(self) |
| ): |
| while await self.handle_next_request(): |
| pass |
| finally: |
| # As long as we don't do something silly like catch a |
| # `GeneratorExit` exception or a `CancelledError` in the above |
| # code we should always execute this `finally` block. |
| _LOGGER.debug(f"{self} completed serving. Closing channel") |
| # Explicitly close the channel instead of deferring closure to the |
| # garbage collector to close it. The garbage collector may never |
| # close the channel since removing the last reference to an object |
| # only ever makes it eligible for garbage collection. One cannot |
| # treat these objects like scoped RAII objects in C++ or Rust. |
| # |
| # If another coroutine depends on this server functioning (like a |
| # client), then it'll hang forever. So, we must close the channel in |
| # order to make progress. |
| if self._channel is not None: |
| self._channel.close() |
| |
| async def handle_next_request(self) -> bool: |
| # TODO(b/299946378): Handle case where ordinal is unknown. |
| # TODO(b/303532690): When attempting to decode a method that is |
| # unrecognized, there should be a message sent declaring this is |
| # an unknown method. |
| try: |
| msg, txid, ordinal = await self._channel_read_and_parse() |
| except fc.FcTransportStatus as e: |
| if e.code() == fc.FcTransportStatus.FC_ERR_FDOMAIN: |
| _LOGGER.debug(f"{self} shutting down. PEER_CLOSED received") |
| return False |
| else: |
| _LOGGER.warn(f"{self} channel received error: {e}") |
| raise e |
| info = self.method_map[ordinal] |
| info.request_ident |
| method_name = info.name |
| method = getattr(self, method_name) |
| if msg is not None: |
| res = method(msg) |
| else: |
| res = method() |
| if inspect.isawaitable(res): |
| res = await res |
| if res is not None and not info.requires_response: |
| raise ServerError( |
| f"{self} method {info.name} received a " |
| + "response but is one-way method" |
| ) |
| if res is None and info.requires_response and not info.empty_response: |
| raise ServerError( |
| f"{self} method {info.name} returned " |
| + "None when a response was expected" |
| ) |
| if not info.requires_response: |
| return True |
| |
| if info.has_result: |
| _LOGGER.debug(f"{self} received method response {res}") |
| method_reg = get_registered_method(ordinal) |
| if method_reg is None: |
| raise RuntimeError(f"Unknown ordinal {ordinal}") |
| _, response_cls = method_reg |
| assert response_cls is not None |
| if type(res) is DomainError: |
| res = response_cls(err=res.error) |
| elif type(res) is FrameworkError: |
| res = response_cls(framework_err=res) |
| else: |
| if res is None: |
| res = response_cls(response=None) |
| else: |
| res = response_cls(response=res) |
| |
| if res is not None: |
| payload_bytes, handles = res.encode() |
| else: |
| payload_bytes, handles = b"", [] |
| |
| header = struct.pack("<IHBBQ", txid, 0x02, 0x00, 0x01, ordinal) |
| encoded_msg = header + payload_bytes |
| |
| if self._channel is None: |
| raise ValueError("Channel is already closed") |
| self._channel.write((encoded_msg, handles)) |
| return True |
| |
| async def _channel_read(self) -> FidlMessage: |
| while True: |
| if self._channel is None: |
| raise ValueError("Channel is already closed") |
| try: |
| return self._channel.read() |
| except fc.FcTransportStatus as e: |
| # Any number of spurious wakeups are possible. Stay in the loop if the error |
| # is FC_ERR_SHOULD_WAIT. |
| if e.code() == fc.FcTransportStatus.FC_ERR_SHOULD_WAIT: |
| _LOGGER.debug(f"{self} channel spurious wakeup") |
| await self._channel_waker.wait_ready(self._channel) |
| continue |
| _LOGGER.warning(f"{self} channel received error: {e}") |
| raise e |
| |
| async def _channel_read_and_parse(self) -> tuple[Any, int, int]: |
| raw_msg = await self._channel_read() |
| ordinal = parse_ordinal(raw_msg) |
| txid = parse_txid(raw_msg) |
| method = get_registered_method(ordinal) |
| if method is None: |
| raise RuntimeError(f"Unknown ordinal {ordinal}") |
| request_cls, _ = method |
| |
| handles = raw_msg[1] |
| verified_handles: list[int] = [0] * len(handles) |
| for i in range(len(handles)): |
| hdl = handles[i] |
| if isinstance(hdl, tuple): |
| verified_handles[i] = hdl[1] |
| else: |
| verified_handles[i] = hdl.take() |
| |
| if request_cls is not None: |
| result_obj = request_cls.decode(raw_msg[0][16:], verified_handles) |
| else: |
| result_obj = None |
| |
| return result_obj, txid, ordinal |
| |
| def _send_event(self, ordinal: int, library: str, msg_obj: Any) -> None: |
| if msg_obj is not None: |
| payload_bytes, handles = msg_obj.encode() |
| else: |
| payload_bytes, handles = b"", [] |
| |
| header = struct.pack("<IHBBQ", 0, 0x02, 0x00, 0x01, ordinal) |
| encoded_msg = header + payload_bytes |
| |
| if self._channel is None: |
| raise ValueError("Channel is already closed") |
| self._channel.write((encoded_msg, handles)) |