mirror of
https://github.com/home-assistant/supervisor.git
synced 2026-02-14 23:19:37 +00:00
Add periodic progress logging during initial Core installation (#6562)
* Add periodic progress logging during initial Core installation Log installation progress every 15 seconds while downloading the Home Assistant Core image during initial setup (landing page to core transition). Uses asyncio.Event with wait_for timeout to produce time-based logs independent of Docker pull events, ensuring visibility even when the network stalls. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add test coverage --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Jan Čermák <sairon@users.noreply.github.com>
This commit is contained in:
@@ -182,28 +182,53 @@ class HomeAssistantCore(JobGroup):
|
||||
concurrency=JobConcurrency.GROUP_REJECT,
|
||||
)
|
||||
async def install(self) -> None:
|
||||
"""Install a landing page."""
|
||||
"""Install Home Assistant Core."""
|
||||
_LOGGER.info("Home Assistant setup")
|
||||
while True:
|
||||
# read homeassistant tag and install it
|
||||
if not self.sys_homeassistant.latest_version:
|
||||
await self.sys_updater.reload()
|
||||
stop_progress_log = asyncio.Event()
|
||||
|
||||
if to_version := self.sys_homeassistant.latest_version:
|
||||
async def _periodic_progress_log() -> None:
|
||||
"""Log installation progress periodically for user visibility."""
|
||||
while not stop_progress_log.is_set():
|
||||
try:
|
||||
await self.instance.update(
|
||||
to_version,
|
||||
image=self.sys_updater.image_homeassistant,
|
||||
)
|
||||
self.sys_homeassistant.version = self.instance.version or to_version
|
||||
break
|
||||
except (DockerError, JobException):
|
||||
pass
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
await async_capture_exception(err)
|
||||
await asyncio.wait_for(stop_progress_log.wait(), timeout=15)
|
||||
except TimeoutError:
|
||||
if (job := self.instance.active_job) and job.progress:
|
||||
_LOGGER.info(
|
||||
"Downloading Home Assistant Core image, %d%%",
|
||||
int(job.progress),
|
||||
)
|
||||
else:
|
||||
_LOGGER.info("Home Assistant Core installation in progress")
|
||||
|
||||
_LOGGER.warning("Error on Home Assistant installation. Retrying in 30sec")
|
||||
await asyncio.sleep(30)
|
||||
progress_task = self.sys_create_task(_periodic_progress_log())
|
||||
try:
|
||||
while True:
|
||||
# read homeassistant tag and install it
|
||||
if not self.sys_homeassistant.latest_version:
|
||||
await self.sys_updater.reload()
|
||||
|
||||
if to_version := self.sys_homeassistant.latest_version:
|
||||
try:
|
||||
await self.instance.update(
|
||||
to_version,
|
||||
image=self.sys_updater.image_homeassistant,
|
||||
)
|
||||
self.sys_homeassistant.version = (
|
||||
self.instance.version or to_version
|
||||
)
|
||||
break
|
||||
except (DockerError, JobException):
|
||||
pass
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
await async_capture_exception(err)
|
||||
|
||||
_LOGGER.warning(
|
||||
"Error on Home Assistant installation. Retrying in 30sec"
|
||||
)
|
||||
await asyncio.sleep(30)
|
||||
finally:
|
||||
stop_progress_log.set()
|
||||
await progress_task
|
||||
|
||||
_LOGGER.info("Home Assistant docker now installed")
|
||||
self.sys_homeassistant.set_image(self.sys_updater.image_homeassistant)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Test Home Assistant core."""
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
from unittest.mock import ANY, MagicMock, Mock, PropertyMock, call, patch
|
||||
@@ -206,6 +207,58 @@ async def test_install_other_error(
|
||||
assert "Unhandled exception:" not in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("active_job", "expected_log"),
|
||||
[
|
||||
(None, "Home Assistant Core installation in progress"),
|
||||
(MagicMock(progress=45.0), "Downloading Home Assistant Core image, 45%"),
|
||||
],
|
||||
)
|
||||
async def test_install_logs_progress_periodically(
|
||||
coresys: CoreSys,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
active_job: MagicMock | None,
|
||||
expected_log: str,
|
||||
):
|
||||
"""Test install logs progress periodically during image pull."""
|
||||
coresys.security.force = True
|
||||
coresys.docker.images.pull.return_value = AsyncIterator([{}])
|
||||
original_wait_for = asyncio.wait_for
|
||||
|
||||
async def mock_wait_for(coro, *, timeout=None):
|
||||
"""Immediately timeout for the progress log wait, pass through others."""
|
||||
if timeout == 15:
|
||||
coro.close()
|
||||
await asyncio.sleep(0)
|
||||
raise TimeoutError
|
||||
return await original_wait_for(coro, timeout=timeout)
|
||||
|
||||
with (
|
||||
patch.object(HomeAssistantCore, "start"),
|
||||
patch.object(DockerHomeAssistant, "cleanup"),
|
||||
patch.object(
|
||||
Updater,
|
||||
"image_homeassistant",
|
||||
new=PropertyMock(return_value="homeassistant"),
|
||||
),
|
||||
patch.object(
|
||||
Updater, "version_homeassistant", new=PropertyMock(return_value="2022.7.3")
|
||||
),
|
||||
patch.object(
|
||||
DockerInterface, "arch", new=PropertyMock(return_value=CpuArch.AMD64)
|
||||
),
|
||||
patch("supervisor.homeassistant.core.asyncio.wait_for", new=mock_wait_for),
|
||||
patch.object(
|
||||
DockerHomeAssistant,
|
||||
"active_job",
|
||||
new=PropertyMock(return_value=active_job),
|
||||
),
|
||||
):
|
||||
await coresys.homeassistant.core.install()
|
||||
|
||||
assert expected_log in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("container_exc", "image_exc", "delete_calls"),
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user