blob: 3b8610e937bd291e461ff2cb400746ab90c2d934 [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.
"""Module for handling encoding and decoding FIDL messages, as well as for handling async I/O."""
import asyncio
import logging
import socket
import traceback
from abc import ABC, abstractmethod
from types import TracebackType
from typing import Dict, Self
import fuchsia_controller_py as fc
logger = logging.getLogger(__name__)
HANDLE_READY_QUEUES: Dict[int, asyncio.Queue[int]] = {}
HANDLE_REFCOUNTS: Dict[int, int] = {}
def enqueue_ready_zx_handle_from_fd(
fd: int, handle_ready_queues: Dict[int, asyncio.Queue[int]]
) -> None:
"""Reads zx_handle that is ready for reading, and enqueues it in the appropriate ready queue."""
s = socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM)
handle_no = int.from_bytes(s.recv(4), "little")
queue = handle_ready_queues.get(handle_no)
if not queue:
logger.debug(f"Dropping notification for: {handle_no}")
return
queue.put_nowait(handle_no)
class HandleRegistration:
"""A scoped object for handles.
This is intended to be used with the `with` keyword to ensure that handle registration is
properly cleaned up.
"""
def __init__(
self, waker: "HandleWaker", h: fc.BaseHandle, *, name: str
) -> None:
self.waker = waker
self.handle = h
self.name = name
def __enter__(self) -> Self:
self.waker._register(self.handle, name=self.name)
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
self.waker._unregister(self.handle)
class HandleWaker(ABC):
"""Base class for a waker used with potentially blocking handles."""
@abstractmethod
def _register(self, channel: fc.BaseHandle, *, name: str) -> None:
"""Registers a handle to receive wake notifications."""
@abstractmethod
def _unregister(self, channel: fc.BaseHandle) -> None:
"""Unregisters a handle, meaning it is not possible to wait for it to be ready."""
def registration(
self, channel: fc.BaseHandle, *, name: str
) -> HandleRegistration:
"""Returns a scoped registration object for a handle."""
return HandleRegistration(self, channel, name=name)
@abstractmethod
def post_ready(self, channel: fc.BaseHandle) -> None:
"""Notifies the waker that a channel is ready."""
@abstractmethod
async def wait_ready(self, channel: fc.BaseHandle) -> int:
"""Waits for a channel to be ready asynchronously."""
class GlobalHandleWaker(HandleWaker):
"""A class for handling notifications on a readable handle.
As this is a global channel waker, this hooks into global state. This is the default waker used
with all client and server code, as well as async wrapper code around readable handles.
"""
def __init__(self) -> None:
self._handle_ready_queues = HANDLE_READY_QUEUES
self._handle_refcounts = HANDLE_REFCOUNTS
def _register(self, h: fc.BaseHandle, *, name: str) -> None:
h_id = h.as_int()
if h_id not in self._handle_ready_queues:
self._handle_ready_queues[h_id] = asyncio.Queue()
self._handle_refcounts[h_id] = 0
self._handle_refcounts[h_id] += 1
notification_fd = fc.connect_handle_notifier()
# This try call is simply here in case registration occurs in outside
# an async event loop.
try:
# Calling this multiple times only overwrites the reader.
# In the event that the loop is destroyed this will be removed automatically.
asyncio.get_running_loop().add_reader(
notification_fd,
enqueue_ready_zx_handle_from_fd,
notification_fd,
self._handle_ready_queues,
)
except RuntimeError as e:
logger.debug(
"Registered reader in non-async context. Printing exception for debugging."
)
logger.debug("[[ TRACE BEGIN ]]")
for line in map(lambda x: x.strip(), traceback.format_exception(e)):
logger.debug(f"-- {line}")
logger.debug("[[ TRACE END ]]")
def _unregister(self, h: fc.BaseHandle) -> None:
h_id = h.as_int()
if h_id in self._handle_ready_queues:
self._handle_refcounts[h_id] -= 1
if self._handle_refcounts[h_id] <= 0:
self._handle_ready_queues.pop(h_id)
self._handle_refcounts.pop(h_id)
def post_ready(self, h: fc.BaseHandle) -> None:
logger.debug(f"Re-notifying for channel: {h.as_int()}")
self._handle_ready_queues[h.as_int()].put_nowait(h.as_int())
async def wait_ready(self, h: fc.BaseHandle) -> int:
res = await self._handle_ready_queues[h.as_int()].get()
self._handle_ready_queues[h.as_int()].task_done()
return res
def _reset_for_testing(self) -> None:
"""This is a method intended only to be used for testing.
This implementation of handle notifications doesn't consistently get cleaned up
when channels are garbage collected. There is likely a clearer root-cause
for this that can be prevented, but for the time being this is a hacked-together
fix for using `unittest.IsolatedAsyncioTestCase` with various FIDL server
implementations and background tasks. Each test uses its own new/isolated
loop to prevent pollution from other tasks, so resetting state will at least
ensure that any asyncio.Queue objects in our state are removed and don't cross from
one loop into another.
"""
self._handle_ready_queues.clear()
self._handle_refcounts.clear()