blob: 52057b32d36a34b3b5fd714b00f16db1abeae157 [file]
# 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))