From fee572b3fcca061c3d6c38d01c0a3a5b273ef881 Mon Sep 17 00:00:00 2001 From: Simon Holesch Date: Sun, 18 Aug 2024 23:26:14 +0200 Subject: [PATCH] USB/IP: Run Blocking Operations in Thread Some sysfs file writes block for more than 100 ms, which causes an asyncio warning. Run those blocking writes in threads, so the event loop isn't blocked. --- not_my_board/_usbip.py | 23 ++++++++++++++++------- not_my_board/_util/__init__.py | 1 + not_my_board/_util/_asyncio.py | 8 ++++++-- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/not_my_board/_usbip.py b/not_my_board/_usbip.py index 135d82f..1d0dd9c 100644 --- a/not_my_board/_usbip.py +++ b/not_my_board/_usbip.py @@ -29,7 +29,7 @@ def __init__(self, devices): async def _context_stack(self, stack): for _, device in self._devices.items(): - stack.callback(device.restore_default_usb_driver) + stack.push_async_callback(device.restore_default_usb_driver) await stack.enter_async_context(util.background_task(_refresh_task(device))) async def handle_client(self, reader, writer): @@ -143,12 +143,15 @@ def refresh(self): async def _context_stack(self, stack): await stack.enter_async_context(self._lock) await self.available() - stack.callback(self.stop_export) + stack.push_async_callback(self.stop_export) - def stop_export(self): + async def stop_export(self): if self._is_exported: try: - (self._sysfs_path / "usbip_sockfd").write_text("-1\n") + # This can block for ~ 250 ms. Run it in a thread. + await util.run_in_thread( + (self._sysfs_path / "usbip_sockfd").write_text, "-1\n" + ) except (OSError, FileNotFoundError): # client might have disconnected or device disappeared pass @@ -192,7 +195,10 @@ async def _ensure_usbip_host_driver(self): logger.info( 'Unbinding USB device %s from driver "%s"', self._busid, driver_name ) - (driver_path / "unbind").write_text(self._busid) + # Unbinding can take more than 100 ms. Run in Thread. + await util.run_in_thread( + (driver_path / "unbind").write_text, self._busid + ) await self._bind_usbip_host_driver() elif self._sysfs_path.exists(): await self._bind_usbip_host_driver() @@ -205,7 +211,7 @@ async def _bind_usbip_host_driver(self): (usbip_host_driver / "match_busid").write_text(f"add {self._busid}") (usbip_host_driver / "bind").write_text(self._busid) - def restore_default_usb_driver(self): + async def restore_default_usb_driver(self): driver_path = self._sysfs_path / "driver" if driver_path.exists(): driver_name = driver_path.resolve().name @@ -213,7 +219,10 @@ def restore_default_usb_driver(self): logger.info( 'Unbinding USB device %s from driver "%s"', self._busid, driver_name ) - (driver_path / "unbind").write_text(self._busid) + # Unbinding can take more than 100 ms. Run in Thread. + await util.run_in_thread( + (driver_path / "unbind").write_text, self._busid + ) self._bind_default_usb_driver() elif self._sysfs_path.exists(): self._bind_default_usb_driver() diff --git a/not_my_board/_util/__init__.py b/not_my_board/_util/__init__.py index 1b4065d..cb6da6e 100644 --- a/not_my_board/_util/__init__.py +++ b/not_my_board/_util/__init__.py @@ -10,6 +10,7 @@ relay_streams, run, run_concurrently, + run_in_thread, ) from ._logging import configure_logging from ._matching import find_matching diff --git a/not_my_board/_util/_asyncio.py b/not_my_board/_util/_asyncio.py index ac46336..84e4fb0 100644 --- a/not_my_board/_util/_asyncio.py +++ b/not_my_board/_util/_asyncio.py @@ -247,9 +247,13 @@ async def __aexit__(self, exc_type, exc, tb): async def flock(f): """File lock as a context manager""" - loop = asyncio.get_running_loop() try: - await loop.run_in_executor(None, fcntl.flock, f.fileno(), fcntl.LOCK_EX) + await run_in_thread(fcntl.flock, f.fileno(), fcntl.LOCK_EX) yield finally: fcntl.flock(f, fcntl.LOCK_UN) + + +async def run_in_thread(func, *args): + loop = asyncio.get_running_loop() + return await loop.run_in_executor(None, func, *args)