Skip to content
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

Enable open protection in the event loop #117289

Merged
merged 6 commits into from
May 12, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 18 additions & 4 deletions homeassistant/block_async_io.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Block blocking calls being done in asyncio."""

import builtins
from contextlib import suppress
from http.client import HTTPConnection
import importlib
Expand All @@ -13,12 +14,21 @@

_IN_TESTS = "unittest" in sys.modules

ALLOWED_FILE_PREFIXES = ("/proc",)


def _check_import_call_allowed(mapped_args: dict[str, Any]) -> bool:
# If the module is already imported, we can ignore it.
return bool((args := mapped_args.get("args")) and args[0] in sys.modules)


def _check_file_allowed(mapped_args: dict[str, Any]) -> bool:
# If the file is in /proc we can ignore it.
args = mapped_args["args"]
path = args[0] if type(args[0]) is str else str(args[0]) # noqa: E721
return path.startswith(ALLOWED_FILE_PREFIXES)


def _check_sleep_call_allowed(mapped_args: dict[str, Any]) -> bool:
#
# Avoid extracting the stack unless we need to since it
Expand Down Expand Up @@ -50,11 +60,15 @@ def enable() -> None:
loop_thread_id=loop_thread_id,
)

# Currently disabled. pytz doing I/O when getting timezone.
# Prevent files being opened inside the event loop
# builtins.open = protect_loop(builtins.open)

if not _IN_TESTS:
# Prevent files being opened inside the event loop
builtins.open = protect_loop( # type: ignore[assignment]
builtins.open,
strict_core=False,
strict=False,
check_allowed=_check_file_allowed,
loop_thread_id=loop_thread_id,
)
# unittest uses `importlib.import_module` to do mocking
# so we cannot protect it if we are running tests
importlib.import_module = protect_loop(
Expand Down
37 changes: 37 additions & 0 deletions tests/test_block_async_io.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
"""Tests for async util methods from Python source."""

import contextlib
import importlib
from pathlib import Path, PurePosixPath
import time
from typing import Any
from unittest.mock import Mock, patch

import pytest
Expand Down Expand Up @@ -198,3 +201,37 @@ async def test_protect_loop_importlib_import_module_in_integration(
"Detected blocking call to import_module inside the event loop by "
"integration 'hue' at homeassistant/components/hue/light.py, line 23"
) in caplog.text


async def test_protect_loop_open(caplog: pytest.LogCaptureFixture) -> None:
"""Test open of a file in /proc is not reported."""
block_async_io.enable()
with contextlib.suppress(FileNotFoundError):
open("/proc/does_not_exist").close()
assert "Detected blocking call to open with args" not in caplog.text


async def test_protect_open(caplog: pytest.LogCaptureFixture) -> None:
"""Test opening a file in the event loop logs."""
block_async_io.enable()
with contextlib.suppress(FileNotFoundError):
open("/config/data_not_exist").close()

assert "Detected blocking call to open with args" in caplog.text


@pytest.mark.parametrize(
"path",
[
"/config/data_not_exist",
Path("/config/data_not_exist"),
PurePosixPath("/config/data_not_exist"),
],
)
async def test_protect_open_path(path: Any, caplog: pytest.LogCaptureFixture) -> None:
"""Test opening a file by path in the event loop logs."""
block_async_io.enable()
with contextlib.suppress(FileNotFoundError):
open(path).close()

assert "Detected blocking call to open with args" in caplog.text