| # 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() |