Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,32 @@
from datetime import datetime
from typing import List, Dict, Any, Optional

# ---------------------------------------------------------------------------
# Monkey-patch asyncio.BaseEventLoop._check_closed to suppress "Event loop is
# closed" RuntimeError during late cleanup. This is a last-resort defense —
# the primary fix is neuter_async_httpx_del() + _suppress_closed_loop_errors.
# Without this patch, any __del__ or close() that runs after the loop has
# stopped will raise synchronously from call_soon() before exception handlers
# can intercept it.
# ---------------------------------------------------------------------------
try:
import asyncio
_orig_check_closed = asyncio.AbstractEventLoop._check_closed if hasattr(asyncio.AbstractEventLoop, '_check_closed') else None
if _orig_check_closed is None:
from asyncio import BaseEventLoop
_orig_check_closed = BaseEventLoop._check_closed

def _silence_check_closed(self):
try:
_orig_check_closed(self)
except RuntimeError as e:
if "Event loop is closed" in str(e):
return # silently suppress
raise
BaseEventLoop._check_closed = _silence_check_closed

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

call_soon() uses _check_closed() as its guard before enqueueing work. Replacing it globally means late cleanup can queue callbacks on a closed loop instead of failing, so this is not a safe output-only suppression point; please identify and fix the specific remaining finalizer path.

except Exception:
pass

logger = logging.getLogger(__name__)

# Suppress startup messages for clean CLI experience
Expand Down