mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Protect loop set default executor (#37438)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
f8651d9faa
commit
f49ce5d1b4
@ -1,6 +1,5 @@
|
||||
"""Start Home Assistant."""
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
@ -8,32 +7,9 @@ import sys
|
||||
import threading
|
||||
from typing import List
|
||||
|
||||
import yarl
|
||||
|
||||
from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__
|
||||
|
||||
|
||||
def set_loop() -> None:
|
||||
"""Attempt to use different loop."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from asyncio.events import BaseDefaultEventLoopPolicy
|
||||
|
||||
if sys.platform == "win32":
|
||||
if hasattr(asyncio, "WindowsProactorEventLoopPolicy"):
|
||||
# pylint: disable=no-member
|
||||
policy = asyncio.WindowsProactorEventLoopPolicy()
|
||||
else:
|
||||
|
||||
class ProactorPolicy(BaseDefaultEventLoopPolicy):
|
||||
"""Event loop policy to create proactor loops."""
|
||||
|
||||
_loop_factory = asyncio.ProactorEventLoop
|
||||
|
||||
policy = ProactorPolicy()
|
||||
|
||||
asyncio.set_event_loop_policy(policy)
|
||||
|
||||
|
||||
def validate_python() -> None:
|
||||
"""Validate that the right Python version is running."""
|
||||
if sys.version_info[:3] < REQUIRED_PYTHON_VER:
|
||||
@ -240,39 +216,6 @@ def cmdline() -> List[str]:
|
||||
return [arg for arg in sys.argv if arg != "--daemon"]
|
||||
|
||||
|
||||
async def setup_and_run_hass(config_dir: str, args: argparse.Namespace) -> int:
|
||||
"""Set up Home Assistant and run."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from homeassistant import bootstrap
|
||||
|
||||
hass = await bootstrap.async_setup_hass(
|
||||
config_dir=config_dir,
|
||||
verbose=args.verbose,
|
||||
log_rotate_days=args.log_rotate_days,
|
||||
log_file=args.log_file,
|
||||
log_no_color=args.log_no_color,
|
||||
skip_pip=args.skip_pip,
|
||||
safe_mode=args.safe_mode,
|
||||
)
|
||||
|
||||
if hass is None:
|
||||
return 1
|
||||
|
||||
if args.open_ui:
|
||||
import webbrowser # pylint: disable=import-outside-toplevel
|
||||
|
||||
if hass.config.api is not None:
|
||||
scheme = "https" if hass.config.api.use_ssl else "http"
|
||||
url = str(
|
||||
yarl.URL.build(
|
||||
scheme=scheme, host="127.0.0.1", port=hass.config.api.port
|
||||
)
|
||||
)
|
||||
hass.add_job(webbrowser.open, url)
|
||||
|
||||
return await hass.async_run()
|
||||
|
||||
|
||||
def try_to_restart() -> None:
|
||||
"""Attempt to clean up state and start a new Home Assistant instance."""
|
||||
# Things should be mostly shut down already at this point, now just try
|
||||
@ -319,8 +262,6 @@ def main() -> int:
|
||||
"""Start Home Assistant."""
|
||||
validate_python()
|
||||
|
||||
set_loop()
|
||||
|
||||
# Run a simple daemon runner process on Windows to handle restarts
|
||||
if os.name == "nt" and "--runner" not in sys.argv:
|
||||
nt_args = cmdline() + ["--runner"]
|
||||
@ -353,7 +294,22 @@ def main() -> int:
|
||||
if args.pid_file:
|
||||
write_pid(args.pid_file)
|
||||
|
||||
exit_code = asyncio.run(setup_and_run_hass(config_dir, args), debug=args.debug)
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from homeassistant import runner
|
||||
|
||||
runtime_conf = runner.RuntimeConfig(
|
||||
config_dir=config_dir,
|
||||
verbose=args.verbose,
|
||||
log_rotate_days=args.log_rotate_days,
|
||||
log_file=args.log_file,
|
||||
log_no_color=args.log_no_color,
|
||||
skip_pip=args.skip_pip,
|
||||
safe_mode=args.safe_mode,
|
||||
debug=args.debug,
|
||||
open_ui=args.open_ui,
|
||||
)
|
||||
|
||||
exit_code = runner.run(runtime_conf)
|
||||
if exit_code == RESTART_EXIT_CODE and not args.runner:
|
||||
try_to_restart()
|
||||
|
||||
|
@ -7,10 +7,11 @@ import logging.handlers
|
||||
import os
|
||||
import sys
|
||||
from time import monotonic
|
||||
from typing import Any, Dict, Optional, Set
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Set
|
||||
|
||||
from async_timeout import timeout
|
||||
import voluptuous as vol
|
||||
import yarl
|
||||
|
||||
from homeassistant import config as conf_util, config_entries, core, loader
|
||||
from homeassistant.components import http
|
||||
@ -31,6 +32,9 @@ from homeassistant.util.logging import async_activate_log_queue_handler
|
||||
from homeassistant.util.package import async_get_user_site, is_virtual_env
|
||||
from homeassistant.util.yaml import clear_secret_cache
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .runner import RuntimeConfig
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ERROR_LOG_FILENAME = "home-assistant.log"
|
||||
@ -66,23 +70,22 @@ STAGE_1_INTEGRATIONS = {
|
||||
|
||||
|
||||
async def async_setup_hass(
|
||||
*,
|
||||
config_dir: str,
|
||||
verbose: bool,
|
||||
log_rotate_days: int,
|
||||
log_file: str,
|
||||
log_no_color: bool,
|
||||
skip_pip: bool,
|
||||
safe_mode: bool,
|
||||
runtime_config: "RuntimeConfig",
|
||||
) -> Optional[core.HomeAssistant]:
|
||||
"""Set up Home Assistant."""
|
||||
hass = core.HomeAssistant()
|
||||
hass.config.config_dir = config_dir
|
||||
hass.config.config_dir = runtime_config.config_dir
|
||||
|
||||
async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color)
|
||||
async_enable_logging(
|
||||
hass,
|
||||
runtime_config.verbose,
|
||||
runtime_config.log_rotate_days,
|
||||
runtime_config.log_file,
|
||||
runtime_config.log_no_color,
|
||||
)
|
||||
|
||||
hass.config.skip_pip = skip_pip
|
||||
if skip_pip:
|
||||
hass.config.skip_pip = runtime_config.skip_pip
|
||||
if runtime_config.skip_pip:
|
||||
_LOGGER.warning(
|
||||
"Skipping pip installation of required modules. This may cause issues"
|
||||
)
|
||||
@ -91,10 +94,11 @@ async def async_setup_hass(
|
||||
_LOGGER.error("Error getting configuration path")
|
||||
return None
|
||||
|
||||
_LOGGER.info("Config directory: %s", config_dir)
|
||||
_LOGGER.info("Config directory: %s", runtime_config.config_dir)
|
||||
|
||||
config_dict = None
|
||||
basic_setup_success = False
|
||||
safe_mode = runtime_config.safe_mode
|
||||
|
||||
if not safe_mode:
|
||||
await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass)
|
||||
@ -107,7 +111,7 @@ async def async_setup_hass(
|
||||
)
|
||||
else:
|
||||
if not is_virtual_env():
|
||||
await async_mount_local_lib_path(config_dir)
|
||||
await async_mount_local_lib_path(runtime_config.config_dir)
|
||||
|
||||
basic_setup_success = (
|
||||
await async_from_config_dict(config_dict, hass) is not None
|
||||
@ -153,9 +157,32 @@ async def async_setup_hass(
|
||||
{"safe_mode": {}, "http": http_conf}, hass,
|
||||
)
|
||||
|
||||
if runtime_config.open_ui:
|
||||
hass.add_job(open_hass_ui, hass)
|
||||
|
||||
return hass
|
||||
|
||||
|
||||
def open_hass_ui(hass: core.HomeAssistant) -> None:
|
||||
"""Open the UI."""
|
||||
import webbrowser # pylint: disable=import-outside-toplevel
|
||||
|
||||
if hass.config.api is None or "frontend" not in hass.config.components:
|
||||
_LOGGER.warning("Cannot launch the UI because frontend not loaded")
|
||||
return
|
||||
|
||||
scheme = "https" if hass.config.api.use_ssl else "http"
|
||||
url = str(
|
||||
yarl.URL.build(scheme=scheme, host="127.0.0.1", port=hass.config.api.port)
|
||||
)
|
||||
|
||||
if not webbrowser.open(url):
|
||||
_LOGGER.warning(
|
||||
"Unable to open the Home Assistant UI in a browser. Open it yourself at %s",
|
||||
url,
|
||||
)
|
||||
|
||||
|
||||
async def async_from_config_dict(
|
||||
config: ConfigType, hass: core.HomeAssistant
|
||||
) -> Optional[core.HomeAssistant]:
|
||||
|
@ -7,8 +7,8 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.helpers import ConfigType
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import ( # pylint:disable=unused-import
|
||||
CONF_DEVICE_IDENT,
|
||||
|
@ -8,7 +8,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers import ConfigType
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN # pylint: disable=unused-import
|
||||
from .utils import load_plum
|
||||
|
@ -5,7 +5,6 @@ Home Assistant is a Home Automation framework for observing the state
|
||||
of entities and react to changes.
|
||||
"""
|
||||
import asyncio
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import datetime
|
||||
import enum
|
||||
import functools
|
||||
@ -145,19 +144,6 @@ def is_callback(func: Callable[..., Any]) -> bool:
|
||||
return getattr(func, "_hass_callback", False) is True
|
||||
|
||||
|
||||
@callback
|
||||
def async_loop_exception_handler(_: Any, context: Dict) -> None:
|
||||
"""Handle all exception inside the core loop."""
|
||||
kwargs = {}
|
||||
exception = context.get("exception")
|
||||
if exception:
|
||||
kwargs["exc_info"] = (type(exception), exception, exception.__traceback__)
|
||||
|
||||
_LOGGER.error(
|
||||
"Error doing job: %s", context["message"], **kwargs # type: ignore
|
||||
)
|
||||
|
||||
|
||||
class CoreState(enum.Enum):
|
||||
"""Represent the current state of Home Assistant."""
|
||||
|
||||
@ -179,18 +165,9 @@ class HomeAssistant:
|
||||
http: "HomeAssistantHTTP" = None # type: ignore
|
||||
config_entries: "ConfigEntries" = None # type: ignore
|
||||
|
||||
def __init__(self, loop: Optional[asyncio.events.AbstractEventLoop] = None) -> None:
|
||||
def __init__(self) -> None:
|
||||
"""Initialize new Home Assistant object."""
|
||||
self.loop: asyncio.events.AbstractEventLoop = (loop or asyncio.get_event_loop())
|
||||
|
||||
executor_opts: Dict[str, Any] = {
|
||||
"max_workers": None,
|
||||
"thread_name_prefix": "SyncWorker",
|
||||
}
|
||||
|
||||
self.executor = ThreadPoolExecutor(**executor_opts)
|
||||
self.loop.set_default_executor(self.executor)
|
||||
self.loop.set_exception_handler(async_loop_exception_handler)
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self._pending_tasks: list = []
|
||||
self._track_task = True
|
||||
self.bus = EventBus(self)
|
||||
@ -461,7 +438,9 @@ class HomeAssistant:
|
||||
self.state = CoreState.not_running
|
||||
self.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE)
|
||||
await self.async_block_till_done()
|
||||
self.executor.shutdown()
|
||||
|
||||
# Python 3.9+ and backported in runner.py
|
||||
await self.loop.shutdown_default_executor() # type: ignore
|
||||
|
||||
self.exit_code = exit_code
|
||||
|
||||
|
@ -1,13 +1,14 @@
|
||||
"""Helper methods for components within Home Assistant."""
|
||||
import re
|
||||
from typing import Any, Iterable, Sequence, Tuple
|
||||
from typing import TYPE_CHECKING, Any, Iterable, Sequence, Tuple
|
||||
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
|
||||
from .typing import ConfigType
|
||||
if TYPE_CHECKING:
|
||||
from .typing import ConfigType
|
||||
|
||||
|
||||
def config_per_platform(config: ConfigType, domain: str) -> Iterable[Tuple[Any, Any]]:
|
||||
def config_per_platform(config: "ConfigType", domain: str) -> Iterable[Tuple[Any, Any]]:
|
||||
"""Break a component config into different platforms.
|
||||
|
||||
For example, will find 'switch', 'switch 2', 'switch 3', .. etc
|
||||
@ -31,7 +32,7 @@ def config_per_platform(config: ConfigType, domain: str) -> Iterable[Tuple[Any,
|
||||
yield platform, item
|
||||
|
||||
|
||||
def extract_domain_configs(config: ConfigType, domain: str) -> Sequence[str]:
|
||||
def extract_domain_configs(config: "ConfigType", domain: str) -> Sequence[str]:
|
||||
"""Extract keys from config for given domain name.
|
||||
|
||||
Async friendly.
|
||||
|
@ -13,7 +13,7 @@ import async_timeout
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__
|
||||
from homeassistant.core import Event, callback
|
||||
from homeassistant.helpers.frame import MissingIntegrationFrame, get_integration_frame
|
||||
from homeassistant.helpers.frame import warn_use
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util import ssl as ssl_util
|
||||
@ -71,34 +71,9 @@ def async_create_clientsession(
|
||||
connector=connector, headers={USER_AGENT: SERVER_SOFTWARE}, **kwargs,
|
||||
)
|
||||
|
||||
async def patched_close() -> None:
|
||||
"""Mock close to avoid integrations closing our session."""
|
||||
try:
|
||||
found_frame, integration, path = get_integration_frame()
|
||||
except MissingIntegrationFrame:
|
||||
# Did not source from an integration? Hard error.
|
||||
raise RuntimeError(
|
||||
"Detected closing of the Home Assistant aiohttp session in the Home Assistant core. "
|
||||
"Please report this issue."
|
||||
)
|
||||
|
||||
index = found_frame.filename.index(path)
|
||||
if path == "custom_components/":
|
||||
extra = " to the custom component author"
|
||||
else:
|
||||
extra = ""
|
||||
|
||||
_LOGGER.warning(
|
||||
"Detected integration that closes the Home Assistant aiohttp session. "
|
||||
"Please report issue%s for %s using this method at %s, line %s: %s",
|
||||
extra,
|
||||
integration,
|
||||
found_frame.filename[index:],
|
||||
found_frame.lineno,
|
||||
found_frame.line.strip(),
|
||||
)
|
||||
|
||||
clientsession.close = patched_close # type: ignore
|
||||
clientsession.close = warn_use( # type: ignore
|
||||
clientsession.close, "closes the Home Assistant aiohttp session"
|
||||
)
|
||||
|
||||
if auto_cleanup:
|
||||
_async_register_clientsession_shutdown(hass, clientsession)
|
||||
|
@ -1,9 +1,16 @@
|
||||
"""Provide frame helper for finding the current frame context."""
|
||||
import asyncio
|
||||
import functools
|
||||
import logging
|
||||
from traceback import FrameSummary, extract_stack
|
||||
from typing import Tuple
|
||||
from typing import Any, Callable, Tuple, TypeVar, cast
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name
|
||||
|
||||
|
||||
def get_integration_frame() -> Tuple[FrameSummary, str, str]:
|
||||
"""Return the frame, integration and integration path of the current stack frame."""
|
||||
@ -34,3 +41,49 @@ def get_integration_frame() -> Tuple[FrameSummary, str, str]:
|
||||
|
||||
class MissingIntegrationFrame(HomeAssistantError):
|
||||
"""Raised when no integration is found in the frame."""
|
||||
|
||||
|
||||
def report(what: str) -> None:
|
||||
"""Report incorrect usage.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
try:
|
||||
found_frame, integration, path = get_integration_frame()
|
||||
except MissingIntegrationFrame:
|
||||
# Did not source from an integration? Hard error.
|
||||
raise RuntimeError(f"Detected code that {what}. Please report this issue.")
|
||||
|
||||
index = found_frame.filename.index(path)
|
||||
if path == "custom_components/":
|
||||
extra = " to the custom component author"
|
||||
else:
|
||||
extra = ""
|
||||
|
||||
_LOGGER.warning(
|
||||
"Detected integration that %s. "
|
||||
"Please report issue%s for %s using this method at %s, line %s: %s",
|
||||
what,
|
||||
extra,
|
||||
integration,
|
||||
found_frame.filename[index:],
|
||||
found_frame.lineno,
|
||||
found_frame.line.strip(),
|
||||
)
|
||||
|
||||
|
||||
def warn_use(func: CALLABLE_T, what: str) -> CALLABLE_T:
|
||||
"""Mock a function to warn when it was about to be used."""
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
|
||||
@functools.wraps(func)
|
||||
async def report_use(*args: Any, **kwargs: Any) -> None:
|
||||
report(what)
|
||||
|
||||
else:
|
||||
|
||||
@functools.wraps(func)
|
||||
def report_use(*args: Any, **kwargs: Any) -> None:
|
||||
report(what)
|
||||
|
||||
return cast(CALLABLE_T, report_use)
|
||||
|
119
homeassistant/runner.py
Normal file
119
homeassistant/runner.py
Normal file
@ -0,0 +1,119 @@
|
||||
"""Run Home Assistant."""
|
||||
import asyncio
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import dataclasses
|
||||
import logging
|
||||
import sys
|
||||
import threading
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.frame import warn_use
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class RuntimeConfig:
|
||||
"""Class to hold the information for running Home Assistant."""
|
||||
|
||||
config_dir: str
|
||||
skip_pip: bool = False
|
||||
safe_mode: bool = False
|
||||
|
||||
verbose: bool = False
|
||||
|
||||
log_rotate_days: Optional[int] = None
|
||||
log_file: Optional[str] = None
|
||||
log_no_color: bool = False
|
||||
|
||||
debug: bool = False
|
||||
open_ui: bool = False
|
||||
|
||||
|
||||
# In Python 3.8+ proactor policy is the default on Windows
|
||||
if sys.platform == "win32" and sys.version_info[:2] < (3, 8):
|
||||
PolicyBase = asyncio.WindowsProactorEventLoopPolicy
|
||||
else:
|
||||
PolicyBase = asyncio.DefaultEventLoopPolicy # pylint: disable=invalid-name
|
||||
|
||||
|
||||
class HassEventLoopPolicy(PolicyBase):
|
||||
"""Event loop policy for Home Assistant."""
|
||||
|
||||
def __init__(self, debug: bool) -> None:
|
||||
"""Init the event loop policy."""
|
||||
super().__init__()
|
||||
self.debug = debug
|
||||
|
||||
@property
|
||||
def loop_name(self) -> str:
|
||||
"""Return name of the loop."""
|
||||
return self._loop_factory.__name__
|
||||
|
||||
def new_event_loop(self):
|
||||
"""Get the event loop."""
|
||||
loop = super().new_event_loop()
|
||||
loop.set_exception_handler(_async_loop_exception_handler)
|
||||
if self.debug:
|
||||
loop.set_debug(True)
|
||||
|
||||
executor = ThreadPoolExecutor(thread_name_prefix="SyncWorker")
|
||||
loop.set_default_executor(executor)
|
||||
loop.set_default_executor = warn_use( # type: ignore
|
||||
loop.set_default_executor, "sets default executor on the event loop"
|
||||
)
|
||||
|
||||
# Python 3.9+
|
||||
if hasattr(loop, "shutdown_default_executor"):
|
||||
return loop
|
||||
|
||||
# Copied from Python 3.9 source
|
||||
def _do_shutdown(future):
|
||||
try:
|
||||
executor.shutdown(wait=True)
|
||||
loop.call_soon_threadsafe(future.set_result, None)
|
||||
except Exception as ex: # pylint: disable=broad-except
|
||||
loop.call_soon_threadsafe(future.set_exception, ex)
|
||||
|
||||
async def shutdown_default_executor():
|
||||
"""Schedule the shutdown of the default executor."""
|
||||
future = loop.create_future()
|
||||
thread = threading.Thread(target=_do_shutdown, args=(future,))
|
||||
thread.start()
|
||||
try:
|
||||
await future
|
||||
finally:
|
||||
thread.join()
|
||||
|
||||
loop.shutdown_default_executor = shutdown_default_executor
|
||||
|
||||
return loop
|
||||
|
||||
|
||||
@callback
|
||||
def _async_loop_exception_handler(self, _: Any, context: Dict) -> None:
|
||||
"""Handle all exception inside the core loop."""
|
||||
kwargs = {}
|
||||
exception = context.get("exception")
|
||||
if exception:
|
||||
kwargs["exc_info"] = (type(exception), exception, exception.__traceback__)
|
||||
|
||||
logging.getLogger(__package__).error(
|
||||
"Error doing job: %s", context["message"], **kwargs # type: ignore
|
||||
)
|
||||
|
||||
|
||||
async def setup_and_run_hass(runtime_config: RuntimeConfig,) -> int:
|
||||
"""Set up Home Assistant and run."""
|
||||
hass = await bootstrap.async_setup_hass(runtime_config)
|
||||
|
||||
if hass is None:
|
||||
return 1
|
||||
|
||||
return await hass.async_run()
|
||||
|
||||
|
||||
def run(runtime_config: RuntimeConfig) -> int:
|
||||
"""Run Home Assistant."""
|
||||
asyncio.set_event_loop_policy(HassEventLoopPolicy(runtime_config.debug))
|
||||
return asyncio.run(setup_and_run_hass(runtime_config))
|
@ -7,6 +7,7 @@ import os
|
||||
import sys
|
||||
from typing import List, Optional, Sequence, Text
|
||||
|
||||
from homeassistant import runner
|
||||
from homeassistant.bootstrap import async_mount_local_lib_path
|
||||
from homeassistant.config import get_default_config_dir
|
||||
from homeassistant.requirements import pip_kwargs
|
||||
@ -59,6 +60,8 @@ def run(args: List) -> int:
|
||||
print("Aborting script, could not install dependency", req)
|
||||
return 1
|
||||
|
||||
asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False))
|
||||
|
||||
return script.run(args[1:]) # type: ignore
|
||||
|
||||
|
||||
|
@ -4,6 +4,7 @@ import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
from homeassistant import runner
|
||||
from homeassistant.auth import auth_manager_from_config
|
||||
from homeassistant.auth.providers import homeassistant as hass_auth
|
||||
from homeassistant.config import get_default_config_dir
|
||||
@ -43,24 +44,24 @@ def run(args):
|
||||
parser_change_pw.add_argument("new_password", type=str)
|
||||
parser_change_pw.set_defaults(func=change_password)
|
||||
|
||||
args = parser.parse_args(args)
|
||||
loop = asyncio.get_event_loop()
|
||||
hass = HomeAssistant(loop=loop)
|
||||
loop.run_until_complete(run_command(hass, args))
|
||||
|
||||
# Triggers save on used storage helpers with delay (core auth)
|
||||
logging.getLogger("homeassistant.core").setLevel(logging.WARNING)
|
||||
loop.run_until_complete(hass.async_stop())
|
||||
asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False))
|
||||
asyncio.run(run_command(parser.parse_args(args)))
|
||||
|
||||
|
||||
async def run_command(hass, args):
|
||||
async def run_command(args):
|
||||
"""Run the command."""
|
||||
hass = HomeAssistant()
|
||||
hass.config.config_dir = os.path.join(os.getcwd(), args.config)
|
||||
hass.auth = await auth_manager_from_config(hass, [{"type": "homeassistant"}], [])
|
||||
provider = hass.auth.auth_providers[0]
|
||||
await provider.async_initialize()
|
||||
await args.func(hass, provider, args)
|
||||
|
||||
# Triggers save on used storage helpers with delay (core auth)
|
||||
logging.getLogger("homeassistant.core").setLevel(logging.WARNING)
|
||||
|
||||
await hass.async_stop()
|
||||
|
||||
|
||||
async def list_users(hass, provider, args):
|
||||
"""List the users."""
|
||||
|
@ -36,18 +36,19 @@ def run(args):
|
||||
args = parser.parse_args()
|
||||
|
||||
bench = BENCHMARKS[args.name]
|
||||
|
||||
print("Using event loop:", asyncio.get_event_loop_policy().__module__)
|
||||
print("Using event loop:", asyncio.get_event_loop_policy().loop_name)
|
||||
|
||||
with suppress(KeyboardInterrupt):
|
||||
while True:
|
||||
loop = asyncio.new_event_loop()
|
||||
hass = core.HomeAssistant(loop)
|
||||
hass.async_stop_track_tasks()
|
||||
runtime = loop.run_until_complete(bench(hass))
|
||||
print(f"Benchmark {bench.__name__} done in {runtime}s")
|
||||
loop.run_until_complete(hass.async_stop())
|
||||
loop.close()
|
||||
asyncio.run(run_benchmark(bench))
|
||||
|
||||
|
||||
async def run_benchmark(bench):
|
||||
"""Run a benchmark."""
|
||||
hass = core.HomeAssistant()
|
||||
runtime = await bench(hass)
|
||||
print(f"Benchmark {bench.__name__} done in {runtime}s")
|
||||
await hass.async_stop()
|
||||
|
||||
|
||||
def benchmark(func: CALLABLE_T) -> CALLABLE_T:
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Script to check the configuration file."""
|
||||
import argparse
|
||||
import asyncio
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Mapping, Sequence
|
||||
from glob import glob
|
||||
@ -199,12 +200,7 @@ def check(config_dir, secrets=False):
|
||||
yaml_loader.yaml.SafeLoader.add_constructor("!secret", yaml_loader.secret_yaml)
|
||||
|
||||
try:
|
||||
hass = core.HomeAssistant()
|
||||
hass.config.config_dir = config_dir
|
||||
|
||||
res["components"] = hass.loop.run_until_complete(
|
||||
async_check_ha_config_file(hass)
|
||||
)
|
||||
res["components"] = asyncio.run(async_check_config(config_dir))
|
||||
res["secret_cache"] = OrderedDict(yaml_loader.__SECRET_CACHE)
|
||||
for err in res["components"].errors:
|
||||
domain = err.domain or ERROR_STR
|
||||
@ -213,7 +209,6 @@ def check(config_dir, secrets=False):
|
||||
res["except"].setdefault(domain, []).append(err.config)
|
||||
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
_LOGGER.exception("BURB")
|
||||
print(color("red", "Fatal error while loading config:"), str(err))
|
||||
res["except"].setdefault(ERROR_STR, []).append(str(err))
|
||||
finally:
|
||||
@ -230,6 +225,15 @@ def check(config_dir, secrets=False):
|
||||
return res
|
||||
|
||||
|
||||
async def async_check_config(config_dir):
|
||||
"""Check the HA config."""
|
||||
hass = core.HomeAssistant()
|
||||
hass.config.config_dir = config_dir
|
||||
components = await async_check_ha_config_file(hass)
|
||||
await hass.async_stop(force=True)
|
||||
return components
|
||||
|
||||
|
||||
def line_info(obj, **kwargs):
|
||||
"""Display line config source."""
|
||||
if hasattr(obj, "__config_file__"):
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Script to ensure a configuration file exists."""
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
import homeassistant.config as config_util
|
||||
@ -31,15 +32,15 @@ def run(args):
|
||||
print("Creating directory", config_dir)
|
||||
os.makedirs(config_dir)
|
||||
|
||||
hass = HomeAssistant()
|
||||
hass.config.config_dir = config_dir
|
||||
config_path = hass.loop.run_until_complete(async_run(hass))
|
||||
config_path = asyncio.run(async_run(config_dir))
|
||||
print("Configuration file:", config_path)
|
||||
return 0
|
||||
|
||||
|
||||
async def async_run(hass):
|
||||
async def async_run(config_dir):
|
||||
"""Make sure config exists."""
|
||||
hass = HomeAssistant()
|
||||
hass.config.config_dir = config_dir
|
||||
path = await config_util.async_ensure_config_exists(hass)
|
||||
await hass.async_stop(force=True)
|
||||
return path
|
||||
|
@ -149,7 +149,7 @@ def get_test_home_assistant():
|
||||
# pylint: disable=protected-access
|
||||
async def async_test_home_assistant(loop):
|
||||
"""Return a Home Assistant object pointing at test config dir."""
|
||||
hass = ha.HomeAssistant(loop)
|
||||
hass = ha.HomeAssistant()
|
||||
store = auth_store.AuthStore(hass)
|
||||
hass.auth = auth.AuthManager(hass, store, {}, {})
|
||||
ensure_auth_manager_loaded(hass.auth)
|
||||
|
@ -8,8 +8,8 @@ from homeassistant.core import callback as ha_callback
|
||||
from tests.async_mock import patch
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def hk_driver():
|
||||
@pytest.fixture
|
||||
def hk_driver(loop):
|
||||
"""Return a custom AccessoryDriver instance for HomeKit accessory init."""
|
||||
with patch("pyhap.accessory_driver.Zeroconf"), patch(
|
||||
"pyhap.accessory_driver.AccessoryEncoder"
|
||||
@ -18,7 +18,7 @@ def hk_driver():
|
||||
), patch(
|
||||
"pyhap.accessory_driver.AccessoryDriver.persist"
|
||||
):
|
||||
yield AccessoryDriver(pincode=b"123-45-678", address="127.0.0.1")
|
||||
yield AccessoryDriver(pincode=b"123-45-678", address="127.0.0.1", loop=loop)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -68,6 +68,11 @@ from tests.components.homekit.common import patch_debounce
|
||||
IP_ADDRESS = "127.0.0.1"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def always_patch_driver(hk_driver):
|
||||
"""Load the hk_driver fixture."""
|
||||
|
||||
|
||||
@pytest.fixture(name="device_reg")
|
||||
def device_reg_fixture(hass):
|
||||
"""Return an empty, loaded, registry."""
|
||||
|
@ -215,7 +215,7 @@ def test_density_to_air_quality():
|
||||
assert density_to_air_quality(300) == 5
|
||||
|
||||
|
||||
async def test_show_setup_msg(hass):
|
||||
async def test_show_setup_msg(hass, hk_driver):
|
||||
"""Test show setup message as persistence notification."""
|
||||
pincode = b"123-45-678"
|
||||
|
||||
|
@ -1,11 +1,12 @@
|
||||
"""Set up some common test helper things."""
|
||||
import asyncio
|
||||
import functools
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
import requests_mock as _requests_mock
|
||||
|
||||
from homeassistant import core as ha, loader, util
|
||||
from homeassistant import core as ha, loader, runner, util
|
||||
from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY
|
||||
from homeassistant.auth.providers import homeassistant, legacy_api_password
|
||||
from homeassistant.components import mqtt
|
||||
@ -40,6 +41,10 @@ from tests.test_util.aiohttp import mock_aiohttp_client # noqa: E402, isort:ski
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
|
||||
|
||||
asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False))
|
||||
# Disable fixtures overriding our beautiful policy
|
||||
asyncio.set_event_loop_policy = lambda policy: None
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
"""Register marker for tests that log exceptions."""
|
||||
|
@ -7,17 +7,15 @@ from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant import bootstrap, core
|
||||
from homeassistant import bootstrap, core, runner
|
||||
import homeassistant.config as config_util
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from tests.async_mock import patch
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
MockModule,
|
||||
MockPlatform,
|
||||
flush_store,
|
||||
get_test_config_dir,
|
||||
mock_coro,
|
||||
mock_entity_platform,
|
||||
@ -351,6 +349,7 @@ async def test_setup_hass(
|
||||
mock_ensure_config_exists,
|
||||
mock_process_ha_config_upgrade,
|
||||
caplog,
|
||||
loop,
|
||||
):
|
||||
"""Test it works."""
|
||||
verbose = Mock()
|
||||
@ -365,13 +364,15 @@ async def test_setup_hass(
|
||||
"homeassistant.components.http.start_http_server_and_save_config"
|
||||
):
|
||||
hass = await bootstrap.async_setup_hass(
|
||||
config_dir=get_test_config_dir(),
|
||||
verbose=verbose,
|
||||
log_rotate_days=log_rotate_days,
|
||||
log_file=log_file,
|
||||
log_no_color=log_no_color,
|
||||
skip_pip=True,
|
||||
safe_mode=False,
|
||||
runner.RuntimeConfig(
|
||||
config_dir=get_test_config_dir(),
|
||||
verbose=verbose,
|
||||
log_rotate_days=log_rotate_days,
|
||||
log_file=log_file,
|
||||
log_no_color=log_no_color,
|
||||
skip_pip=True,
|
||||
safe_mode=False,
|
||||
),
|
||||
)
|
||||
|
||||
assert "Waiting on integrations to complete setup" not in caplog.text
|
||||
@ -399,6 +400,7 @@ async def test_setup_hass_takes_longer_than_log_slow_startup(
|
||||
mock_ensure_config_exists,
|
||||
mock_process_ha_config_upgrade,
|
||||
caplog,
|
||||
loop,
|
||||
):
|
||||
"""Test it works."""
|
||||
verbose = Mock()
|
||||
@ -420,13 +422,15 @@ async def test_setup_hass_takes_longer_than_log_slow_startup(
|
||||
"homeassistant.components.http.start_http_server_and_save_config"
|
||||
):
|
||||
await bootstrap.async_setup_hass(
|
||||
config_dir=get_test_config_dir(),
|
||||
verbose=verbose,
|
||||
log_rotate_days=log_rotate_days,
|
||||
log_file=log_file,
|
||||
log_no_color=log_no_color,
|
||||
skip_pip=True,
|
||||
safe_mode=False,
|
||||
runner.RuntimeConfig(
|
||||
config_dir=get_test_config_dir(),
|
||||
verbose=verbose,
|
||||
log_rotate_days=log_rotate_days,
|
||||
log_file=log_file,
|
||||
log_no_color=log_no_color,
|
||||
skip_pip=True,
|
||||
safe_mode=False,
|
||||
),
|
||||
)
|
||||
|
||||
assert "Waiting on integrations to complete setup" in caplog.text
|
||||
@ -438,19 +442,22 @@ async def test_setup_hass_invalid_yaml(
|
||||
mock_mount_local_lib_path,
|
||||
mock_ensure_config_exists,
|
||||
mock_process_ha_config_upgrade,
|
||||
loop,
|
||||
):
|
||||
"""Test it works."""
|
||||
with patch(
|
||||
"homeassistant.config.async_hass_config_yaml", side_effect=HomeAssistantError
|
||||
), patch("homeassistant.components.http.start_http_server_and_save_config"):
|
||||
hass = await bootstrap.async_setup_hass(
|
||||
config_dir=get_test_config_dir(),
|
||||
verbose=False,
|
||||
log_rotate_days=10,
|
||||
log_file="",
|
||||
log_no_color=False,
|
||||
skip_pip=True,
|
||||
safe_mode=False,
|
||||
runner.RuntimeConfig(
|
||||
config_dir=get_test_config_dir(),
|
||||
verbose=False,
|
||||
log_rotate_days=10,
|
||||
log_file="",
|
||||
log_no_color=False,
|
||||
skip_pip=True,
|
||||
safe_mode=False,
|
||||
),
|
||||
)
|
||||
|
||||
assert "safe_mode" in hass.config.components
|
||||
@ -463,49 +470,52 @@ async def test_setup_hass_config_dir_nonexistent(
|
||||
mock_mount_local_lib_path,
|
||||
mock_ensure_config_exists,
|
||||
mock_process_ha_config_upgrade,
|
||||
loop,
|
||||
):
|
||||
"""Test it works."""
|
||||
mock_ensure_config_exists.return_value = False
|
||||
|
||||
assert (
|
||||
await bootstrap.async_setup_hass(
|
||||
config_dir=get_test_config_dir(),
|
||||
verbose=False,
|
||||
log_rotate_days=10,
|
||||
log_file="",
|
||||
log_no_color=False,
|
||||
skip_pip=True,
|
||||
safe_mode=False,
|
||||
runner.RuntimeConfig(
|
||||
config_dir=get_test_config_dir(),
|
||||
verbose=False,
|
||||
log_rotate_days=10,
|
||||
log_file="",
|
||||
log_no_color=False,
|
||||
skip_pip=True,
|
||||
safe_mode=False,
|
||||
),
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
async def test_setup_hass_safe_mode(
|
||||
hass,
|
||||
mock_enable_logging,
|
||||
mock_is_virtual_env,
|
||||
mock_mount_local_lib_path,
|
||||
mock_ensure_config_exists,
|
||||
mock_process_ha_config_upgrade,
|
||||
loop,
|
||||
):
|
||||
"""Test it works."""
|
||||
# Add a config entry to storage.
|
||||
MockConfigEntry(domain="browser").add_to_hass(hass)
|
||||
hass.config_entries._async_schedule_save()
|
||||
await flush_store(hass.config_entries._store)
|
||||
|
||||
with patch("homeassistant.components.browser.setup") as browser_setup, patch(
|
||||
"homeassistant.components.http.start_http_server_and_save_config"
|
||||
), patch(
|
||||
"homeassistant.config_entries.ConfigEntries.async_domains",
|
||||
return_value=["browser"],
|
||||
):
|
||||
hass = await bootstrap.async_setup_hass(
|
||||
config_dir=get_test_config_dir(),
|
||||
verbose=False,
|
||||
log_rotate_days=10,
|
||||
log_file="",
|
||||
log_no_color=False,
|
||||
skip_pip=True,
|
||||
safe_mode=True,
|
||||
runner.RuntimeConfig(
|
||||
config_dir=get_test_config_dir(),
|
||||
verbose=False,
|
||||
log_rotate_days=10,
|
||||
log_file="",
|
||||
log_no_color=False,
|
||||
skip_pip=True,
|
||||
safe_mode=True,
|
||||
),
|
||||
)
|
||||
|
||||
assert "safe_mode" in hass.config.components
|
||||
@ -522,6 +532,7 @@ async def test_setup_hass_invalid_core_config(
|
||||
mock_mount_local_lib_path,
|
||||
mock_ensure_config_exists,
|
||||
mock_process_ha_config_upgrade,
|
||||
loop,
|
||||
):
|
||||
"""Test it works."""
|
||||
with patch(
|
||||
@ -529,13 +540,15 @@ async def test_setup_hass_invalid_core_config(
|
||||
return_value={"homeassistant": {"non-existing": 1}},
|
||||
), patch("homeassistant.components.http.start_http_server_and_save_config"):
|
||||
hass = await bootstrap.async_setup_hass(
|
||||
config_dir=get_test_config_dir(),
|
||||
verbose=False,
|
||||
log_rotate_days=10,
|
||||
log_file="",
|
||||
log_no_color=False,
|
||||
skip_pip=True,
|
||||
safe_mode=False,
|
||||
runner.RuntimeConfig(
|
||||
config_dir=get_test_config_dir(),
|
||||
verbose=False,
|
||||
log_rotate_days=10,
|
||||
log_file="",
|
||||
log_no_color=False,
|
||||
skip_pip=True,
|
||||
safe_mode=False,
|
||||
),
|
||||
)
|
||||
|
||||
assert "safe_mode" in hass.config.components
|
||||
@ -547,6 +560,7 @@ async def test_setup_safe_mode_if_no_frontend(
|
||||
mock_mount_local_lib_path,
|
||||
mock_ensure_config_exists,
|
||||
mock_process_ha_config_upgrade,
|
||||
loop,
|
||||
):
|
||||
"""Test we setup safe mode if frontend didn't load."""
|
||||
verbose = Mock()
|
||||
@ -566,13 +580,15 @@ async def test_setup_safe_mode_if_no_frontend(
|
||||
},
|
||||
), patch("homeassistant.components.http.start_http_server_and_save_config"):
|
||||
hass = await bootstrap.async_setup_hass(
|
||||
config_dir=get_test_config_dir(),
|
||||
verbose=verbose,
|
||||
log_rotate_days=log_rotate_days,
|
||||
log_file=log_file,
|
||||
log_no_color=log_no_color,
|
||||
skip_pip=True,
|
||||
safe_mode=False,
|
||||
runner.RuntimeConfig(
|
||||
config_dir=get_test_config_dir(),
|
||||
verbose=verbose,
|
||||
log_rotate_days=log_rotate_days,
|
||||
log_file=log_file,
|
||||
log_no_color=log_no_color,
|
||||
skip_pip=True,
|
||||
safe_mode=False,
|
||||
),
|
||||
)
|
||||
|
||||
assert "safe_mode" in hass.config.components
|
||||
|
@ -1104,14 +1104,13 @@ def test_timer_out_of_sync(mock_monotonic, loop):
|
||||
assert abs(target - 14.2) < 0.001
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_hass_start_starts_the_timer(loop):
|
||||
async def test_hass_start_starts_the_timer(loop):
|
||||
"""Test when hass starts, it starts the timer."""
|
||||
hass = ha.HomeAssistant(loop=loop)
|
||||
hass = ha.HomeAssistant()
|
||||
|
||||
try:
|
||||
with patch("homeassistant.core._async_create_timer") as mock_timer:
|
||||
yield from hass.async_start()
|
||||
await hass.async_start()
|
||||
|
||||
assert hass.state == ha.CoreState.running
|
||||
assert not hass._track_task
|
||||
@ -1119,21 +1118,20 @@ def test_hass_start_starts_the_timer(loop):
|
||||
assert mock_timer.mock_calls[0][1][0] is hass
|
||||
|
||||
finally:
|
||||
yield from hass.async_stop()
|
||||
await hass.async_stop()
|
||||
assert hass.state == ha.CoreState.not_running
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_start_taking_too_long(loop, caplog):
|
||||
async def test_start_taking_too_long(loop, caplog):
|
||||
"""Test when async_start takes too long."""
|
||||
hass = ha.HomeAssistant(loop=loop)
|
||||
hass = ha.HomeAssistant()
|
||||
caplog.set_level(logging.WARNING)
|
||||
|
||||
try:
|
||||
with patch(
|
||||
"homeassistant.core.timeout", side_effect=asyncio.TimeoutError
|
||||
), patch("homeassistant.core._async_create_timer") as mock_timer:
|
||||
yield from hass.async_start()
|
||||
await hass.async_start()
|
||||
|
||||
assert hass.state == ha.CoreState.running
|
||||
assert len(mock_timer.mock_calls) == 1
|
||||
@ -1141,14 +1139,13 @@ def test_start_taking_too_long(loop, caplog):
|
||||
assert "Something is blocking Home Assistant" in caplog.text
|
||||
|
||||
finally:
|
||||
yield from hass.async_stop()
|
||||
await hass.async_stop()
|
||||
assert hass.state == ha.CoreState.not_running
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_track_task_functions(loop):
|
||||
async def test_track_task_functions(loop):
|
||||
"""Test function to start/stop track task and initial state."""
|
||||
hass = ha.HomeAssistant(loop=loop)
|
||||
hass = ha.HomeAssistant()
|
||||
try:
|
||||
assert hass._track_task
|
||||
|
||||
@ -1158,7 +1155,7 @@ def test_track_task_functions(loop):
|
||||
hass.async_track_tasks()
|
||||
assert hass._track_task
|
||||
finally:
|
||||
yield from hass.async_stop()
|
||||
await hass.async_stop()
|
||||
|
||||
|
||||
async def test_service_executed_with_subservices(hass):
|
||||
|
Loading…
x
Reference in New Issue
Block a user