| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273 |
- """Asyncio event-loop exception handlers used at app startup.
- Currently houses a single Windows-specific filter for the noisy
- ``_ProactorBasePipeTransport._call_connection_lost`` ``WinError 10054``
- that fires every time a printer / MQTT broker / camera RSTs a TCP socket
- instead of closing it cleanly. See ``install_proactor_reset_filter`` for
- the why and the failure mode it suppresses.
- """
- from __future__ import annotations
- import asyncio
- import logging
- import sys
- from typing import Any
- logger = logging.getLogger(__name__)
- def _is_proactor_connection_reset(context: dict[str, Any]) -> bool:
- """True if `context` describes the Windows Proactor cleanup-RST noise.
- asyncio's default exception handler is invoked in two distinct cases
- we care about — generic uncaught task exceptions, and the specific
- `_call_connection_lost` cleanup path — and we only want to suppress
- the latter. Match on three signals together so a real
- `ConnectionResetError` raised inside an application task still
- surfaces normally:
- 1. The exception is `ConnectionResetError` (or a subclass).
- 2. asyncio's own message string mentions `_call_connection_lost`
- (the Proactor-cleanup callback is the only place Python emits
- this exact phrase).
- 3. We're actually on Windows, where the Proactor is in use.
- """
- if sys.platform != "win32":
- return False
- exc = context.get("exception")
- if not isinstance(exc, ConnectionResetError):
- return False
- message = context.get("message", "")
- return "_call_connection_lost" in message
- def _proactor_reset_filter(loop: asyncio.AbstractEventLoop, context: dict[str, Any]) -> None:
- """Custom event-loop exception handler.
- Handles the Proactor-cleanup `ConnectionResetError` by logging it at
- DEBUG instead of ERROR, and delegates everything else to asyncio's
- default handler so unrelated bugs are still visible.
- """
- if _is_proactor_connection_reset(context):
- logger.debug(
- "asyncio Proactor: peer reset socket during cleanup (WinError 10054); "
- "ignored — application-layer reconnect handles the disconnect"
- )
- return
- loop.default_exception_handler(context)
- def install_proactor_reset_filter(loop: asyncio.AbstractEventLoop | None = None) -> bool:
- """Install the filter on `loop` (or the running loop if omitted).
- Returns True when the filter was installed (Windows only), False on
- every other platform — so callers can branch on the return value if
- they want to log the install / skip.
- """
- if sys.platform != "win32":
- return False
- if loop is None:
- loop = asyncio.get_running_loop()
- loop.set_exception_handler(_proactor_reset_filter)
- return True
|