diff --git a/supervisor/homeassistant/core.py b/supervisor/homeassistant/core.py index 447aade51..bb3008c6c 100644 --- a/supervisor/homeassistant/core.py +++ b/supervisor/homeassistant/core.py @@ -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) diff --git a/tests/homeassistant/test_core.py b/tests/homeassistant/test_core.py index 5f0386dfd..090cefb6d 100644 --- a/tests/homeassistant/test_core.py +++ b/tests/homeassistant/test_core.py @@ -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"), [