From 6656f7d6b9cd3d71b1e06222621ff0b8c5c361dd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 31 May 2024 10:09:19 -0500 Subject: [PATCH] Log directory blocking I/O functions that run in the event loop (#118529) * Log directory I/O functions that run in the event loop * tests --- homeassistant/block_async_io.py | 16 ++++++++++ tests/test_block_async_io.py | 55 +++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/homeassistant/block_async_io.py b/homeassistant/block_async_io.py index 5f58925c53c..e829ed4925b 100644 --- a/homeassistant/block_async_io.py +++ b/homeassistant/block_async_io.py @@ -2,8 +2,10 @@ import builtins from contextlib import suppress +import glob from http.client import HTTPConnection import importlib +import os import sys import threading import time @@ -59,8 +61,22 @@ def enable() -> None: loop_thread_id=loop_thread_id, ) + glob.glob = protect_loop( + glob.glob, strict_core=False, strict=False, loop_thread_id=loop_thread_id + ) + glob.iglob = protect_loop( + glob.iglob, strict_core=False, strict=False, loop_thread_id=loop_thread_id + ) + if not _IN_TESTS: # Prevent files being opened inside the event loop + os.listdir = protect_loop( # type: ignore[assignment] + os.listdir, strict_core=False, strict=False, loop_thread_id=loop_thread_id + ) + os.scandir = protect_loop( # type: ignore[assignment] + os.scandir, strict_core=False, strict=False, loop_thread_id=loop_thread_id + ) + builtins.open = protect_loop( # type: ignore[assignment] builtins.open, strict_core=False, diff --git a/tests/test_block_async_io.py b/tests/test_block_async_io.py index 11b83bdcd3a..e4f248e80d1 100644 --- a/tests/test_block_async_io.py +++ b/tests/test_block_async_io.py @@ -1,7 +1,9 @@ """Tests for async util methods from Python source.""" import contextlib +import glob import importlib +import os from pathlib import Path, PurePosixPath import time from typing import Any @@ -10,6 +12,7 @@ from unittest.mock import Mock, patch import pytest from homeassistant import block_async_io +from homeassistant.core import HomeAssistant from tests.common import extract_stack_to_frame @@ -235,3 +238,55 @@ async def test_protect_open_path(path: Any, caplog: pytest.LogCaptureFixture) -> open(path).close() assert "Detected blocking call to open with args" in caplog.text + + +async def test_protect_loop_glob( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test glob calls in the loop are logged.""" + block_async_io.enable() + glob.glob("/dev/null") + assert "Detected blocking call to glob with args" in caplog.text + caplog.clear() + await hass.async_add_executor_job(glob.glob, "/dev/null") + assert "Detected blocking call to glob with args" not in caplog.text + + +async def test_protect_loop_iglob( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test iglob calls in the loop are logged.""" + block_async_io.enable() + glob.iglob("/dev/null") + assert "Detected blocking call to iglob with args" in caplog.text + caplog.clear() + await hass.async_add_executor_job(glob.iglob, "/dev/null") + assert "Detected blocking call to iglob with args" not in caplog.text + + +async def test_protect_loop_scandir( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test glob calls in the loop are logged.""" + block_async_io.enable() + with contextlib.suppress(FileNotFoundError): + os.scandir("/path/that/does/not/exists") + assert "Detected blocking call to scandir with args" in caplog.text + caplog.clear() + with contextlib.suppress(FileNotFoundError): + await hass.async_add_executor_job(os.scandir, "/path/that/does/not/exists") + assert "Detected blocking call to listdir with args" not in caplog.text + + +async def test_protect_loop_listdir( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test listdir calls in the loop are logged.""" + block_async_io.enable() + with contextlib.suppress(FileNotFoundError): + os.listdir("/path/that/does/not/exists") + assert "Detected blocking call to listdir with args" in caplog.text + caplog.clear() + with contextlib.suppress(FileNotFoundError): + await hass.async_add_executor_job(os.listdir, "/path/that/does/not/exists") + assert "Detected blocking call to listdir with args" not in caplog.text