Home Assistant Unofficial Reference 2024.12.1
runner.py
Go to the documentation of this file.
1 """Run Home Assistant."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 import dataclasses
7 import logging
8 import subprocess
9 import threading
10 from time import monotonic
11 import traceback
12 from typing import Any
13 
14 import packaging.tags
15 
16 from . import bootstrap
17 from .core import callback
18 from .helpers.frame import warn_use
19 from .util.executor import InterruptibleThreadPoolExecutor
20 from .util.thread import deadlock_safe_shutdown
21 
22 #
23 # Some Python versions may have different number of workers by default
24 # than others. In order to be consistent between
25 # supported versions, we need to set max_workers.
26 #
27 # In most cases the workers are not I/O bound, as they
28 # are sleeping/blocking waiting for data from integrations
29 # updating so this number should be higher than the default
30 # use case.
31 #
32 MAX_EXECUTOR_WORKERS = 64
33 TASK_CANCELATION_TIMEOUT = 5
34 
35 _LOGGER = logging.getLogger(__name__)
36 
37 
38 @dataclasses.dataclass(slots=True)
40  """Class to hold the information for running Home Assistant."""
41 
42  config_dir: str
43  skip_pip: bool = False
44  skip_pip_packages: list[str] = dataclasses.field(default_factory=list)
45  recovery_mode: bool = False
46 
47  verbose: bool = False
48 
49  log_rotate_days: int | None = None
50  log_file: str | None = None
51  log_no_color: bool = False
52 
53  debug: bool = False
54  open_ui: bool = False
55 
56  safe_mode: bool = False
57 
58 
59 class HassEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
60  """Event loop policy for Home Assistant."""
61 
62  def __init__(self, debug: bool) -> None:
63  """Init the event loop policy."""
64  super().__init__()
65  self.debugdebug = debug
66 
67  @property
68  def loop_name(self) -> str:
69  """Return name of the loop."""
70  return self._loop_factory.__name__ # type: ignore[no-any-return,attr-defined]
71 
72  def new_event_loop(self) -> asyncio.AbstractEventLoop:
73  """Get the event loop."""
74  loop: asyncio.AbstractEventLoop = super().new_event_loop()
75  loop.set_exception_handler(_async_loop_exception_handler)
76  if self.debugdebug:
77  loop.set_debug(True)
78 
80  thread_name_prefix="SyncWorker", max_workers=MAX_EXECUTOR_WORKERS
81  )
82  loop.set_default_executor(executor)
83  loop.set_default_executor = warn_use( # type: ignore[method-assign]
84  loop.set_default_executor, "sets default executor on the event loop"
85  )
86  # bind the built-in time.monotonic directly as loop.time to avoid the
87  # overhead of the additional method call since its the most called loop
88  # method and its roughly 10%+ of all the call time in base_events.py
89  loop.time = monotonic # type: ignore[method-assign]
90  return loop
91 
92 
93 @callback
94 def _async_loop_exception_handler(_: Any, context: dict[str, Any]) -> None:
95  """Handle all exception inside the core loop."""
96  kwargs = {}
97  if exception := context.get("exception"):
98  kwargs["exc_info"] = (type(exception), exception, exception.__traceback__)
99 
100  logger = logging.getLogger(__package__)
101  if source_traceback := context.get("source_traceback"):
102  stack_summary = "".join(traceback.format_list(source_traceback))
103  logger.error(
104  "Error doing job: %s (%s): %s",
105  context["message"],
106  context.get("task"),
107  stack_summary,
108  **kwargs, # type: ignore[arg-type]
109  )
110  return
111 
112  logger.error(
113  "Error doing job: %s (%s)",
114  context["message"],
115  context.get("task"),
116  **kwargs, # type: ignore[arg-type]
117  )
118 
119 
120 async def setup_and_run_hass(runtime_config: RuntimeConfig) -> int:
121  """Set up Home Assistant and run."""
122  hass = await bootstrap.async_setup_hass(runtime_config)
123 
124  if hass is None:
125  return 1
126 
127  # threading._shutdown can deadlock forever
128  threading._shutdown = deadlock_safe_shutdown # type: ignore[attr-defined] # noqa: SLF001
129 
130  return await hass.async_run()
131 
132 
133 def _enable_posix_spawn() -> None:
134  """Enable posix_spawn on Alpine Linux."""
135  if subprocess._USE_POSIX_SPAWN: # noqa: SLF001
136  return
137 
138  # The subprocess module does not know about Alpine Linux/musl
139  # and will use fork() instead of posix_spawn() which significantly
140  # less efficient. This is a workaround to force posix_spawn()
141  # when using musl since cpython is not aware its supported.
142  tag = next(packaging.tags.sys_tags())
143  subprocess._USE_POSIX_SPAWN = "musllinux" in tag.platform # type: ignore[misc] # noqa: SLF001
144 
145 
146 def run(runtime_config: RuntimeConfig) -> int:
147  """Run Home Assistant."""
149  asyncio.set_event_loop_policy(HassEventLoopPolicy(runtime_config.debug))
150  # Backport of cpython 3.9 asyncio.run with a _cancel_all_tasks that times out
151  loop = asyncio.new_event_loop()
152  try:
153  asyncio.set_event_loop(loop)
154  return loop.run_until_complete(setup_and_run_hass(runtime_config))
155  finally:
156  try:
157  _cancel_all_tasks_with_timeout(loop, TASK_CANCELATION_TIMEOUT)
158  loop.run_until_complete(loop.shutdown_asyncgens())
159  loop.run_until_complete(loop.shutdown_default_executor())
160  finally:
161  asyncio.set_event_loop(None)
162  loop.close()
163 
164 
166  loop: asyncio.AbstractEventLoop, timeout: int
167 ) -> None:
168  """Adapted _cancel_all_tasks from python 3.9 with a timeout."""
169  to_cancel = asyncio.all_tasks(loop)
170  if not to_cancel:
171  return
172 
173  for task in to_cancel:
174  task.cancel("Final process shutdown")
175 
176  loop.run_until_complete(asyncio.wait(to_cancel, timeout=timeout))
177 
178  for task in to_cancel:
179  if task.cancelled():
180  continue
181  if not task.done():
182  _LOGGER.warning(
183  "Task could not be canceled and was still running after shutdown: %s",
184  task,
185  )
186  continue
187  if task.exception() is not None:
188  loop.call_exception_handler(
189  {
190  "message": "unhandled exception during shutdown",
191  "exception": task.exception(),
192  "task": task,
193  }
194  )
asyncio.AbstractEventLoop new_event_loop(self)
Definition: runner.py:72
None __init__(self, bool debug)
Definition: runner.py:62
int setup_and_run_hass(RuntimeConfig runtime_config)
Definition: runner.py:120
None _async_loop_exception_handler(Any _, dict[str, Any] context)
Definition: runner.py:94
int run(RuntimeConfig runtime_config)
Definition: runner.py:146
None _enable_posix_spawn()
Definition: runner.py:133
None _cancel_all_tasks_with_timeout(asyncio.AbstractEventLoop loop, int timeout)
Definition: runner.py:167