diff --git a/supervisor/addons/addon.py b/supervisor/addons/addon.py index f08391b5d60..4bfbd5f4bbd 100644 --- a/supervisor/addons/addon.py +++ b/supervisor/addons/addon.py @@ -1682,8 +1682,9 @@ async def watchdog_container(self, event: DockerContainerStateEvent) -> None: ]: await self._restart_after_problem(event.state) - def refresh_path_cache(self) -> Awaitable[None]: + async def refresh_path_cache(self) -> None: """Refresh cache of existing paths.""" if self.is_detached or not self.addon_store: - return super().refresh_path_cache() - return self.addon_store.refresh_path_cache() + await super().refresh_path_cache() + else: + await self.addon_store.refresh_path_cache() diff --git a/supervisor/addons/model.py b/supervisor/addons/model.py index c6ca928dacd..8b5cf1319b4 100644 --- a/supervisor/addons/model.py +++ b/supervisor/addons/model.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod from collections import defaultdict -from collections.abc import Awaitable, Callable +from collections.abc import Callable from contextlib import suppress from datetime import datetime import logging @@ -91,6 +91,7 @@ from ..coresys import CoreSys from ..docker.const import Capabilities from ..exceptions import ( + AddonFileReadError, AddonNotSupportedArchitectureError, AddonNotSupportedError, AddonNotSupportedHomeAssistantVersionError, @@ -682,9 +683,15 @@ def read_readme() -> str | None: # Return data return readme.read_text(encoding="utf-8", errors="replace") - return await self.sys_run_in_executor(read_readme) - - def refresh_path_cache(self) -> Awaitable[None]: + try: + return await self.sys_run_in_executor(read_readme) + except OSError as err: + self.sys_resolution.check_oserror(err) + raise AddonFileReadError( + _LOGGER.error, addon=self.slug, error=str(err) + ) from err + + async def refresh_path_cache(self) -> None: """Refresh cache of existing paths.""" def check_paths(): @@ -693,7 +700,13 @@ def check_paths(): self._path_changelog_exists = self.path_changelog.exists() self._path_documentation_exists = self.path_documentation.exists() - return self.sys_run_in_executor(check_paths) + try: + await self.sys_run_in_executor(check_paths) + except OSError as err: + self.sys_resolution.check_oserror(err) + raise AddonFileReadError( + _LOGGER.error, addon=self.slug, error=str(err) + ) from err def validate_availability(self) -> None: """Validate if addon is available for current system.""" diff --git a/supervisor/core.py b/supervisor/core.py index 0f77bd3b992..29571d1f5b5 100644 --- a/supervisor/core.py +++ b/supervisor/core.py @@ -185,17 +185,32 @@ async def setup(self) -> None: # Execute each load task in secure context for setup_task in setup_loads: + unhealthy_before = self.sys_resolution.unhealthy.copy() try: await setup_task except Exception as err: # pylint: disable=broad-except - _LOGGER.critical( - "Fatal error happening on load Task %s: %s", - setup_task, - err, - exc_info=True, - ) + # If the error already caused a new unhealthy reason to + # be set (e.g. via check_oserror), it was handled by the + # resolution system and the user has been notified. Skip + # capturing to Sentry in that case. + capture_exception = self.sys_resolution.unhealthy == unhealthy_before + self.sys_resolution.add_unhealthy_reason(UnhealthyReason.SETUP) - await async_capture_exception(err) + + if capture_exception: + _LOGGER.critical( + "Fatal error happening on load Task %s: %s", + setup_task, + err, + exc_info=True, + ) + await async_capture_exception(err) + else: + _LOGGER.error( + "Error on load Task %s: %s", + setup_task, + err, + ) async def start(self) -> None: """Start Supervisor orchestration.""" diff --git a/supervisor/exceptions.py b/supervisor/exceptions.py index 3b9415506cb..205293c7a45 100644 --- a/supervisor/exceptions.py +++ b/supervisor/exceptions.py @@ -561,6 +561,26 @@ def __init__( super().__init__(logger) +class AddonFileReadError(AddonsError, APIError): + """Raise when an add-on metadata file cannot be read due to a filesystem error.""" + + error_key = "addon_file_read_error" + message_template = ( + "Could not read metadata for add-on {addon} due to a filesystem error: {error}" + ) + + def __init__( + self, + logger: Callable[..., None] | None = None, + *, + addon: str, + error: str, + ) -> None: + """Initialize exception.""" + self.extra_fields = {"addon": addon, "error": error} + super().__init__(None, logger) + + class AddonsJobError(AddonsError, JobException): """Raise on job errors."""