147 lines
4.6 KiB
Python
147 lines
4.6 KiB
Python
"""asyncio loop utilities."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from asyncio import get_running_loop
|
|
from collections.abc import Callable
|
|
from contextlib import suppress
|
|
import functools
|
|
import linecache
|
|
import logging
|
|
from typing import Any, ParamSpec, TypeVar
|
|
|
|
from homeassistant.core import HomeAssistant, async_get_hass
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
from homeassistant.helpers.frame import (
|
|
MissingIntegrationFrame,
|
|
get_current_frame,
|
|
get_integration_frame,
|
|
)
|
|
from homeassistant.loader import async_suggest_report_issue
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
_R = TypeVar("_R")
|
|
_P = ParamSpec("_P")
|
|
|
|
|
|
def _get_line_from_cache(filename: str, lineno: int) -> str:
|
|
"""Get line from cache or read from file."""
|
|
return (linecache.getline(filename, lineno) or "?").strip()
|
|
|
|
|
|
def check_loop(
|
|
func: Callable[..., Any],
|
|
check_allowed: Callable[[dict[str, Any]], bool] | None = None,
|
|
strict: bool = True,
|
|
strict_core: bool = True,
|
|
advise_msg: str | None = None,
|
|
**mapped_args: Any,
|
|
) -> None:
|
|
"""Warn if called inside the event loop. Raise if `strict` is True.
|
|
|
|
The default advisory message is 'Use `await hass.async_add_executor_job()'
|
|
Set `advise_msg` to an alternate message if the solution differs.
|
|
"""
|
|
try:
|
|
get_running_loop()
|
|
in_loop = True
|
|
except RuntimeError:
|
|
in_loop = False
|
|
|
|
if not in_loop:
|
|
return
|
|
|
|
if check_allowed is not None and check_allowed(mapped_args):
|
|
return
|
|
|
|
found_frame = None
|
|
offender_frame = get_current_frame(2)
|
|
offender_filename = offender_frame.f_code.co_filename
|
|
offender_lineno = offender_frame.f_lineno
|
|
offender_line = _get_line_from_cache(offender_filename, offender_lineno)
|
|
|
|
try:
|
|
integration_frame = get_integration_frame()
|
|
except MissingIntegrationFrame:
|
|
# Did not source from integration? Hard error.
|
|
if not strict_core:
|
|
_LOGGER.warning(
|
|
"Detected blocking call to %s with args %s in %s, "
|
|
"line %s: %s inside the event loop",
|
|
func.__name__,
|
|
mapped_args.get("args"),
|
|
offender_filename,
|
|
offender_lineno,
|
|
offender_line,
|
|
)
|
|
return
|
|
|
|
if found_frame is None:
|
|
raise RuntimeError( # noqa: TRY200
|
|
f"Detected blocking call to {func.__name__} inside the event loop "
|
|
f"in {offender_filename}, line {offender_lineno}: {offender_line}. "
|
|
f"{advise_msg or 'Use `await hass.async_add_executor_job()`'}; "
|
|
"This is causing stability issues. Please create a bug report at "
|
|
f"https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue"
|
|
)
|
|
|
|
hass: HomeAssistant | None = None
|
|
with suppress(HomeAssistantError):
|
|
hass = async_get_hass()
|
|
report_issue = async_suggest_report_issue(
|
|
hass,
|
|
integration_domain=integration_frame.integration,
|
|
module=integration_frame.module,
|
|
)
|
|
|
|
_LOGGER.warning(
|
|
(
|
|
"Detected blocking call to %s inside the event loop by %sintegration '%s' "
|
|
"at %s, line %s: %s (offender: %s, line %s: %s), please %s"
|
|
),
|
|
func.__name__,
|
|
"custom " if integration_frame.custom_integration else "",
|
|
integration_frame.integration,
|
|
integration_frame.relative_filename,
|
|
integration_frame.line_number,
|
|
integration_frame.line,
|
|
offender_filename,
|
|
offender_lineno,
|
|
offender_line,
|
|
report_issue,
|
|
)
|
|
|
|
if strict:
|
|
raise RuntimeError(
|
|
"Blocking calls must be done in the executor or a separate thread;"
|
|
f" {advise_msg or 'Use `await hass.async_add_executor_job()`'}; at"
|
|
f" {integration_frame.relative_filename}, line {integration_frame.line_number}:"
|
|
f" {integration_frame.line} "
|
|
f"(offender: {offender_filename}, line {offender_lineno}: {offender_line})"
|
|
)
|
|
|
|
|
|
def protect_loop(
|
|
func: Callable[_P, _R],
|
|
strict: bool = True,
|
|
strict_core: bool = True,
|
|
check_allowed: Callable[[dict[str, Any]], bool] | None = None,
|
|
) -> Callable[_P, _R]:
|
|
"""Protect function from running in event loop."""
|
|
|
|
@functools.wraps(func)
|
|
def protected_loop_func(*args: _P.args, **kwargs: _P.kwargs) -> _R:
|
|
check_loop(
|
|
func,
|
|
strict=strict,
|
|
strict_core=strict_core,
|
|
check_allowed=check_allowed,
|
|
args=args,
|
|
kwargs=kwargs,
|
|
)
|
|
return func(*args, **kwargs)
|
|
|
|
return protected_loop_func
|