8000 fix: suppress event loop errors during subprocess transport cleanup by yeisonvargasf · Pull Request #731 · pyupio/safety · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

fix: suppress event loop errors during subprocess transport cleanup #731

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 5, 2025
Merged
Show file tree
Hide file tree
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
85 changes: 67 additions & 18 deletions safety/asyncio_patch.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,88 @@
import sys
import logging


logger = logging.getLogger(__name__)


def apply_asyncio_patch():
"""
Apply a patch to asyncio's proactor events on Windows when running Python 3.8 or 3.9.

This patch is needed because of a bug in Python 3.8 and 3.9 that causes a
RuntimeError to be raised when the event loop is closed while there are still
open file descriptors. This bug was fixed in Python 3.10.
Apply a patch to asyncio's exception handling for subprocesses.

The bug manifests itself when using the proactor event loop on Windows, which
is the default event loop on Windows. The bug causes the event loop to be
closed while there are still open file descriptors, which causes a RuntimeError
to be raised.
There are some issues with asyncio's exception handling for subprocesses,
which causes a RuntimeError to be raised when the event loop was already closed.

This patch catches the RuntimeError and ignores it, which allows the event loop
to be closed properly.

See https://bugs.python.org/issue39232 and https://github.com/python/cpython/issues/92841
for more information.
Similar issues:
- https://bugs.python.org/issue39232
- https://github.com/python/cpython/issues/92841
"""

if sys.platform == "win32" and (3, 8, 0) <= sys.version_info < (3, 11, 0):
import asyncio.base_subprocess

original_subprocess_del = asyncio.base_subprocess.BaseSubprocessTransport.__del__

def patched_subprocess_del(self):
try:
original_subprocess_del(self)
except (RuntimeError, ValueError, OSError) as e:
if isinstance(e, RuntimeError) and str(e) != "Event loop is closed":
raise
if isinstance(e, ValueError) and str(e) != "I/O operation on closed pipe":
raise
if isinstance(e, OSError) and "[WinError 6]" not in str(e):
raise
logger.debug(f"Patched {original_subprocess_del}")

asyncio.base_subprocess.BaseSubprocessTransport.__del__ = patched_subprocess_del

if sys.platform == "win32":
import asyncio.proactor_events as proactor_events

original_del = proactor_events._ProactorBasePipeTransport.__del__
original_pipe_del = proactor_events._ProactorBasePipeTransport.__del__

def patched_pipe_del(self):
try:
original_pipe_del(self)
except (RuntimeError, ValueError) as e:
if isinstance(e, RuntimeError) and str(e) != "Event loop is closed":
raise
if (
isinstance(e, ValueError)
and str(e) != "I/O operation on closed pipe"
):
raise
logger.debug(f"Patched {original_pipe_del}")

original_repr = proactor_events._ProactorBasePipeTransport.__repr__

def patched_repr(self):
try:
return original_repr(self)
except ValueError as e:
if str(e) != "I/O operation on closed pipe":
raise
logger.debug(f"Patched {original_repr}")
return f"<{self.__class__} [closed]>"

proactor_events._ProactorBasePipeTransport.__del__ = patched_pipe_del
proactor_events._ProactorBasePipeTransport.__repr__ = patched_repr

import subprocess

original_popen_del = subprocess.Popen.__del__

def patched_del(self):
def patched_popen_del(self):
try:
original_del(self)
except RuntimeError as e:
if str(e) != "Event loop is closed":
original_popen_del(self)
except OSError as e:
if "[WinError 6]" not in str(e):
raise
logger.debug(f"Patched {original_popen_del}")

proactor_events._ProactorBasePipeTransport.__del__ = patched_del
subprocess.Popen.__del__ = patched_popen_del


apply_asyncio_patch()
17 changes: 12 additions & 5 deletions safety/tool/tool_inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
from safety_schemas.models.events.payloads import ToolStatus, AliasConfig, IndexConfig
from safety_schemas.models.events.types import ToolType

import logging

logger = logging.getLogger(__name__)


class ToolInspector:
"""
Expand Down Expand Up @@ -189,7 +193,7 @@ async def _find_executable_paths(self, tool_type: ToolType) -> Set[str]:
self._found_paths[tool_type] = paths
return paths

def _kill_process(self, proc):
async def _kill_process(self, proc):
"""
Helper method to kill a process safely.
"""
Expand All @@ -198,9 +202,9 @@ def _kill_process(self, proc):

try:
proc.kill()
await asyncio.wait_for(proc.wait(), timeout=1.0)
except Exception:
# Ignore any errors during kill
pass
logger.exception("Error killing process")

async def _check_tool(self, tool_type: ToolType, path: str) -> Optional[ToolStatus]:
"""
Expand All @@ -209,6 +213,7 @@ async def _check_tool(self, tool_type: ToolType, path: str) -> Optional[ToolStat
proc = None
try:
version_arg = self.VERSION_ARGS[tool_type]

proc = await asyncio.create_subprocess_exec(
path,
version_arg,
Expand Down Expand Up @@ -245,7 +250,7 @@ async def _check_tool(self, tool_type: ToolType, path: str) -> Optional[ToolStat
)
except (asyncio.TimeoutError, TimeoutError):
if proc:
self._kill_process(proc)
await self._kill_process(proc)
# Clear references to help garbage collection
proc = None

Expand All @@ -257,9 +262,11 @@ async def _check_tool(self, tool_type: ToolType, path: str) -> Optional[ToolStat
reachable=False,
)
except Exception:
logger.exception("Error checking tool")

# Any other error means the tool is not reachable
if proc:
self._kill_process(proc)
await self._kill_process(proc)
# Clear reference to help garbage collection
proc = None

Expand Down
0