From 86133f8ecd6a7f757cd407621d215261d7b89c67 Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Sat, 1 Mar 2025 10:02:43 -0500 Subject: [PATCH] Move `read_text` to executor (#5688) * Move read_text to executor * Fix issues found by coderabbit * formated to formatted * switch to async_capture_exception * Find and replace got one too many * Update patch mock to async_capture_exception * Drop Sentry capture from format_message The error handling got introduced in #2052, however, #2100 essentially makes sure there will never be a byte object passed to this function. And even if, the Sentry aiohttp plug-in will properly catch such an exception. --------- Co-authored-by: Stefan Agner --- supervisor/__main__.py | 2 +- supervisor/addons/addon.py | 4 +- supervisor/addons/manager.py | 6 +- supervisor/addons/model.py | 27 +++++---- supervisor/api/__init__.py | 4 +- supervisor/api/addons.py | 2 +- supervisor/api/host.py | 8 +-- supervisor/api/store.py | 34 ++++++----- supervisor/backups/manager.py | 6 +- supervisor/bootstrap.py | 7 ++- supervisor/core.py | 10 ++-- supervisor/dbus/network/__init__.py | 6 +- supervisor/docker/addon.py | 6 +- supervisor/docker/interface.py | 6 +- supervisor/hardware/disk.py | 20 +++++-- supervisor/homeassistant/core.py | 10 ++-- supervisor/host/info.py | 56 +++++++++---------- supervisor/jobs/__init__.py | 8 +-- supervisor/jobs/decorator.py | 9 +-- supervisor/misc/filter.py | 4 +- supervisor/misc/tasks.py | 6 +- supervisor/mounts/manager.py | 4 +- supervisor/mounts/mount.py | 6 +- supervisor/os/data_disk.py | 6 +- supervisor/os/manager.py | 4 +- supervisor/plugins/audio.py | 4 +- supervisor/plugins/base.py | 4 +- supervisor/plugins/cli.py | 4 +- supervisor/plugins/dns.py | 4 +- supervisor/plugins/manager.py | 8 +-- supervisor/plugins/multicast.py | 4 +- supervisor/plugins/observer.py | 4 +- supervisor/resolution/check.py | 4 +- supervisor/resolution/checks/dns_server.py | 4 +- .../resolution/checks/dns_server_ipv6.py | 4 +- supervisor/resolution/checks/free_space.py | 4 +- supervisor/resolution/evaluate.py | 4 +- supervisor/resolution/fixup.py | 4 +- supervisor/resolution/notify.py | 2 +- supervisor/supervisor.py | 4 +- supervisor/utils/dbus.py | 4 +- supervisor/utils/log_format.py | 16 ++---- supervisor/utils/sentry.py | 29 +++++++++- tests/api/test_supervisor.py | 4 +- .../test_home_assistant_watchdog.py | 2 +- tests/host/test_info.py | 4 +- tests/utils/test_log_format.py | 6 -- 47 files changed, 213 insertions(+), 175 deletions(-) diff --git a/supervisor/__main__.py b/supervisor/__main__.py index de6920bd8..f5c438e1a 100644 --- a/supervisor/__main__.py +++ b/supervisor/__main__.py @@ -54,7 +54,7 @@ if __name__ == "__main__": loop.set_debug(coresys.config.debug) loop.run_until_complete(coresys.core.connect()) - bootstrap.supervisor_debugger(coresys) + loop.run_until_complete(bootstrap.supervisor_debugger(coresys)) # Signal health startup for container run_os_startup_check_cleanup() diff --git a/supervisor/addons/addon.py b/supervisor/addons/addon.py index 2d5dee5b9..c7cfe4c28 100644 --- a/supervisor/addons/addon.py +++ b/supervisor/addons/addon.py @@ -88,7 +88,7 @@ from ..store.addon import AddonStore from ..utils import check_port from ..utils.apparmor import adjust_profile from ..utils.json import read_json_file, write_json_file -from ..utils.sentry import capture_exception +from ..utils.sentry import async_capture_exception from .const import ( WATCHDOG_MAX_ATTEMPTS, WATCHDOG_RETRY_SECONDS, @@ -1530,7 +1530,7 @@ class Addon(AddonModel): except AddonsError as err: attempts = attempts + 1 _LOGGER.error("Watchdog restart of addon %s failed!", self.name) - capture_exception(err) + await async_capture_exception(err) else: break diff --git a/supervisor/addons/manager.py b/supervisor/addons/manager.py index 23cf175b7..56eae30ed 100644 --- a/supervisor/addons/manager.py +++ b/supervisor/addons/manager.py @@ -23,7 +23,7 @@ from ..exceptions import ( from ..jobs.decorator import Job, JobCondition from ..resolution.const import ContextType, IssueType, SuggestionType from ..store.addon import AddonStore -from ..utils.sentry import capture_exception +from ..utils.sentry import async_capture_exception from .addon import Addon from .const import ADDON_UPDATE_CONDITIONS from .data import AddonsData @@ -170,7 +170,7 @@ class AddonManager(CoreSysAttributes): await addon.stop() except Exception as err: # pylint: disable=broad-except _LOGGER.warning("Can't stop Add-on %s: %s", addon.slug, err) - capture_exception(err) + await async_capture_exception(err) @Job( name="addon_manager_install", @@ -388,7 +388,7 @@ class AddonManager(CoreSysAttributes): reference=addon.slug, suggestions=[SuggestionType.EXECUTE_REPAIR], ) - capture_exception(err) + await async_capture_exception(err) else: add_host_coros.append( self.sys_plugins.dns.add_host( diff --git a/supervisor/addons/model.py b/supervisor/addons/model.py index 8f0fb8033..4ebd45997 100644 --- a/supervisor/addons/model.py +++ b/supervisor/addons/model.py @@ -210,18 +210,6 @@ class AddonModel(JobGroup, ABC): """Return description of add-on.""" return self.data[ATTR_DESCRIPTON] - @property - def long_description(self) -> str | None: - """Return README.md as long_description.""" - readme = Path(self.path_location, "README.md") - - # If readme not exists - if not readme.exists(): - return None - - # Return data - return readme.read_text(encoding="utf-8") - @property def repository(self) -> str: """Return repository of add-on.""" @@ -646,6 +634,21 @@ class AddonModel(JobGroup, ABC): """Return breaking versions of addon.""" return self.data[ATTR_BREAKING_VERSIONS] + async def long_description(self) -> str | None: + """Return README.md as long_description.""" + + def read_readme() -> str | None: + readme = Path(self.path_location, "README.md") + + # If readme not exists + if not readme.exists(): + return None + + # Return data + return readme.read_text(encoding="utf-8") + + return await self.sys_run_in_executor(read_readme) + def refresh_path_cache(self) -> Awaitable[None]: """Refresh cache of existing paths.""" diff --git a/supervisor/api/__init__.py b/supervisor/api/__init__.py index f71e028f9..fe3f17ace 100644 --- a/supervisor/api/__init__.py +++ b/supervisor/api/__init__.py @@ -10,7 +10,7 @@ from aiohttp import web from ..const import AddonState from ..coresys import CoreSys, CoreSysAttributes from ..exceptions import APIAddonNotInstalled, HostNotSupportedError -from ..utils.sentry import capture_exception +from ..utils.sentry import async_capture_exception from .addons import APIAddons from .audio import APIAudio from .auth import APIAuth @@ -412,7 +412,7 @@ class RestAPI(CoreSysAttributes): if not isinstance(err, HostNotSupportedError): # No need to capture HostNotSupportedError to Sentry, the cause # is known and reported to the user using the resolution center. - capture_exception(err) + await async_capture_exception(err) kwargs.pop("follow", None) # Follow is not supported for Docker logs return await api_supervisor.logs(*args, **kwargs) diff --git a/supervisor/api/addons.py b/supervisor/api/addons.py index 7b862061d..0a2e6bef8 100644 --- a/supervisor/api/addons.py +++ b/supervisor/api/addons.py @@ -212,7 +212,7 @@ class APIAddons(CoreSysAttributes): ATTR_HOSTNAME: addon.hostname, ATTR_DNS: addon.dns, ATTR_DESCRIPTON: addon.description, - ATTR_LONG_DESCRIPTION: addon.long_description, + ATTR_LONG_DESCRIPTION: await addon.long_description(), ATTR_ADVANCED: addon.advanced, ATTR_STAGE: addon.stage, ATTR_REPOSITORY: addon.repository, diff --git a/supervisor/api/host.py b/supervisor/api/host.py index 7d2cc6650..6e3399420 100644 --- a/supervisor/api/host.py +++ b/supervisor/api/host.py @@ -98,10 +98,10 @@ class APIHost(CoreSysAttributes): ATTR_VIRTUALIZATION: self.sys_host.info.virtualization, ATTR_CPE: self.sys_host.info.cpe, ATTR_DEPLOYMENT: self.sys_host.info.deployment, - ATTR_DISK_FREE: self.sys_host.info.free_space, - ATTR_DISK_TOTAL: self.sys_host.info.total_space, - ATTR_DISK_USED: self.sys_host.info.used_space, - ATTR_DISK_LIFE_TIME: self.sys_host.info.disk_life_time, + ATTR_DISK_FREE: await self.sys_host.info.free_space(), + ATTR_DISK_TOTAL: await self.sys_host.info.total_space(), + ATTR_DISK_USED: await self.sys_host.info.used_space(), + ATTR_DISK_LIFE_TIME: await self.sys_host.info.disk_life_time(), ATTR_FEATURES: self.sys_host.features, ATTR_HOSTNAME: self.sys_host.info.hostname, ATTR_LLMNR_HOSTNAME: self.sys_host.info.llmnr_hostname, diff --git a/supervisor/api/store.py b/supervisor/api/store.py index 9f689cbac..cfed482c3 100644 --- a/supervisor/api/store.py +++ b/supervisor/api/store.py @@ -69,12 +69,12 @@ SCHEMA_ADD_REPOSITORY = vol.Schema( ) -def _read_static_file(path: Path) -> Any: +def _read_static_file(path: Path, binary: bool = False) -> Any: """Read in a static file asset for API output. Must be run in executor. """ - with path.open("r") as asset: + with path.open("rb" if binary else "r") as asset: return asset.read() @@ -109,7 +109,7 @@ class APIStore(CoreSysAttributes): return self.sys_store.get(repository_slug) - def _generate_addon_information( + async def _generate_addon_information( self, addon: AddonStore, extended: bool = False ) -> dict[str, Any]: """Generate addon information.""" @@ -156,7 +156,7 @@ class APIStore(CoreSysAttributes): ATTR_HOST_NETWORK: addon.host_network, ATTR_HOST_PID: addon.host_pid, ATTR_INGRESS: addon.with_ingress, - ATTR_LONG_DESCRIPTION: addon.long_description, + ATTR_LONG_DESCRIPTION: await addon.long_description(), ATTR_RATING: rating_security(addon), ATTR_SIGNED: addon.signed, } @@ -185,10 +185,12 @@ class APIStore(CoreSysAttributes): async def store_info(self, request: web.Request) -> dict[str, Any]: """Return store information.""" return { - ATTR_ADDONS: [ - self._generate_addon_information(self.sys_addons.store[addon]) - for addon in self.sys_addons.store - ], + ATTR_ADDONS: await asyncio.gather( + *[ + self._generate_addon_information(self.sys_addons.store[addon]) + for addon in self.sys_addons.store + ] + ), ATTR_REPOSITORIES: [ self._generate_repository_information(repository) for repository in self.sys_store.all @@ -199,10 +201,12 @@ class APIStore(CoreSysAttributes): async def addons_list(self, request: web.Request) -> dict[str, Any]: """Return all store add-ons.""" return { - ATTR_ADDONS: [ - self._generate_addon_information(self.sys_addons.store[addon]) - for addon in self.sys_addons.store - ] + ATTR_ADDONS: await asyncio.gather( + *[ + self._generate_addon_information(self.sys_addons.store[addon]) + for addon in self.sys_addons.store + ] + ) } @api_process @@ -234,7 +238,7 @@ class APIStore(CoreSysAttributes): async def addons_addon_info_wrapped(self, request: web.Request) -> dict[str, Any]: """Return add-on information directly (not api).""" addon: AddonStore = self._extract_addon(request) - return self._generate_addon_information(addon, True) + return await self._generate_addon_information(addon, True) @api_process_raw(CONTENT_TYPE_PNG) async def addons_addon_icon(self, request: web.Request) -> bytes: @@ -243,7 +247,7 @@ class APIStore(CoreSysAttributes): if not addon.with_icon: raise APIError(f"No icon found for add-on {addon.slug}!") - return await self.sys_run_in_executor(_read_static_file, addon.path_icon) + return await self.sys_run_in_executor(_read_static_file, addon.path_icon, True) @api_process_raw(CONTENT_TYPE_PNG) async def addons_addon_logo(self, request: web.Request) -> bytes: @@ -252,7 +256,7 @@ class APIStore(CoreSysAttributes): if not addon.with_logo: raise APIError(f"No logo found for add-on {addon.slug}!") - return await self.sys_run_in_executor(_read_static_file, addon.path_logo) + return await self.sys_run_in_executor(_read_static_file, addon.path_logo, True) @api_process_raw(CONTENT_TYPE_TEXT) async def addons_addon_changelog(self, request: web.Request) -> str: diff --git a/supervisor/backups/manager.py b/supervisor/backups/manager.py index a64037100..ed60ea1de 100644 --- a/supervisor/backups/manager.py +++ b/supervisor/backups/manager.py @@ -36,7 +36,7 @@ from ..resolution.const import UnhealthyReason from ..utils.common import FileConfiguration from ..utils.dt import utcnow from ..utils.sentinel import DEFAULT -from ..utils.sentry import capture_exception +from ..utils.sentry import async_capture_exception from .backup import Backup from .const import ( DEFAULT_FREEZE_TIMEOUT, @@ -525,7 +525,7 @@ class BackupManager(FileConfiguration, JobGroup): return None except Exception as err: # pylint: disable=broad-except _LOGGER.exception("Backup %s error", backup.slug) - capture_exception(err) + await async_capture_exception(err) self.sys_jobs.current.capture_error( BackupError(f"Backup {backup.slug} error, see supervisor logs") ) @@ -718,7 +718,7 @@ class BackupManager(FileConfiguration, JobGroup): raise except Exception as err: # pylint: disable=broad-except _LOGGER.exception("Restore %s error", backup.slug) - capture_exception(err) + await async_capture_exception(err) raise BackupError( f"Restore {backup.slug} error, see supervisor logs" ) from err diff --git a/supervisor/bootstrap.py b/supervisor/bootstrap.py index 64ded55ce..c6ccfa14d 100644 --- a/supervisor/bootstrap.py +++ b/supervisor/bootstrap.py @@ -1,6 +1,7 @@ """Bootstrap Supervisor.""" # ruff: noqa: T100 +from importlib import import_module import logging import os import signal @@ -306,12 +307,12 @@ def reg_signal(loop, coresys: CoreSys) -> None: _LOGGER.warning("Could not bind to SIGINT") -def supervisor_debugger(coresys: CoreSys) -> None: +async def supervisor_debugger(coresys: CoreSys) -> None: """Start debugger if needed.""" if not coresys.config.debug: return - # pylint: disable=import-outside-toplevel - import debugpy + + debugpy = await coresys.run_in_executor(import_module, "debugpy") _LOGGER.info("Initializing Supervisor debugger") diff --git a/supervisor/core.py b/supervisor/core.py index 211cedfc5..913c4c8aa 100644 --- a/supervisor/core.py +++ b/supervisor/core.py @@ -26,7 +26,7 @@ from .exceptions import ( from .homeassistant.core import LANDINGPAGE from .resolution.const import ContextType, IssueType, SuggestionType, UnhealthyReason from .utils.dt import utcnow -from .utils.sentry import capture_exception +from .utils.sentry import async_capture_exception from .utils.whoami import WhoamiData, retrieve_whoami _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -172,7 +172,7 @@ class Core(CoreSysAttributes): "Fatal error happening on load Task %s: %s", setup_task, err ) self.sys_resolution.unhealthy = UnhealthyReason.SETUP - capture_exception(err) + await async_capture_exception(err) # Set OS Agent diagnostics if needed if ( @@ -189,7 +189,7 @@ class Core(CoreSysAttributes): self.sys_config.diagnostics, err, ) - capture_exception(err) + await async_capture_exception(err) # Evaluate the system await self.sys_resolution.evaluate.evaluate_system() @@ -246,12 +246,12 @@ class Core(CoreSysAttributes): await self.sys_homeassistant.core.start() except HomeAssistantCrashError as err: _LOGGER.error("Can't start Home Assistant Core - rebuiling") - capture_exception(err) + await async_capture_exception(err) with suppress(HomeAssistantError): await self.sys_homeassistant.core.rebuild() except HomeAssistantError as err: - capture_exception(err) + await async_capture_exception(err) else: _LOGGER.info("Skipping start of Home Assistant") diff --git a/supervisor/dbus/network/__init__.py b/supervisor/dbus/network/__init__.py index 741aceb0f..502a759b8 100644 --- a/supervisor/dbus/network/__init__.py +++ b/supervisor/dbus/network/__init__.py @@ -15,7 +15,7 @@ from ...exceptions import ( HostNotSupportedError, NetworkInterfaceNotFound, ) -from ...utils.sentry import capture_exception +from ...utils.sentry import async_capture_exception from ..const import ( DBUS_ATTR_CONNECTION_ENABLED, DBUS_ATTR_DEVICES, @@ -223,13 +223,13 @@ class NetworkManager(DBusInterfaceProxy): device, err, ) - capture_exception(err) + await async_capture_exception(err) return except Exception as err: # pylint: disable=broad-except _LOGGER.exception( "Unkown error while processing %s: %s", device, err ) - capture_exception(err) + await async_capture_exception(err) continue # Skeep interface diff --git a/supervisor/docker/addon.py b/supervisor/docker/addon.py index cb8251896..6f02e3130 100644 --- a/supervisor/docker/addon.py +++ b/supervisor/docker/addon.py @@ -42,7 +42,7 @@ from ..hardware.data import Device from ..jobs.const import JobCondition, JobExecutionLimit from ..jobs.decorator import Job from ..resolution.const import CGROUP_V2_VERSION, ContextType, IssueType, SuggestionType -from ..utils.sentry import capture_exception +from ..utils.sentry import async_capture_exception from .const import ( ENV_TIME, ENV_TOKEN, @@ -606,7 +606,7 @@ class DockerAddon(DockerInterface): ) except CoreDNSError as err: _LOGGER.warning("Can't update DNS for %s", self.name) - capture_exception(err) + await async_capture_exception(err) # Hardware Access if self.addon.static_devices: @@ -787,7 +787,7 @@ class DockerAddon(DockerInterface): await self.sys_plugins.dns.delete_host(self.addon.hostname) except CoreDNSError as err: _LOGGER.warning("Can't update DNS for %s", self.name) - capture_exception(err) + await async_capture_exception(err) # Hardware if self._hw_listener: diff --git a/supervisor/docker/interface.py b/supervisor/docker/interface.py index 44446d53f..6c8c843d8 100644 --- a/supervisor/docker/interface.py +++ b/supervisor/docker/interface.py @@ -42,7 +42,7 @@ from ..jobs.const import JOB_GROUP_DOCKER_INTERFACE, JobExecutionLimit from ..jobs.decorator import Job from ..jobs.job_group import JobGroup from ..resolution.const import ContextType, IssueType, SuggestionType -from ..utils.sentry import capture_exception +from ..utils.sentry import async_capture_exception from .const import ContainerState, RestartPolicy from .manager import CommandReturn from .monitor import DockerContainerStateEvent @@ -278,7 +278,7 @@ class DockerInterface(JobGroup): f"Can't install {image}:{version!s}: {err}", _LOGGER.error ) from err except (docker.errors.DockerException, requests.RequestException) as err: - capture_exception(err) + await async_capture_exception(err) raise DockerError( f"Unknown error with {image}:{version!s} -> {err!s}", _LOGGER.error ) from err @@ -394,7 +394,7 @@ class DockerInterface(JobGroup): ) except DockerNotFound as err: # If image is missing, capture the exception as this shouldn't happen - capture_exception(err) + await async_capture_exception(err) raise # Store metadata diff --git a/supervisor/hardware/disk.py b/supervisor/hardware/disk.py index 649a63e37..531559b6c 100644 --- a/supervisor/hardware/disk.py +++ b/supervisor/hardware/disk.py @@ -49,17 +49,26 @@ class HwDisk(CoreSysAttributes): return False def get_disk_total_space(self, path: str | Path) -> float: - """Return total space (GiB) on disk for path.""" + """Return total space (GiB) on disk for path. + + Must be run in executor. + """ total, _, _ = shutil.disk_usage(path) return round(total / (1024.0**3), 1) def get_disk_used_space(self, path: str | Path) -> float: - """Return used space (GiB) on disk for path.""" + """Return used space (GiB) on disk for path. + + Must be run in executor. + """ _, used, _ = shutil.disk_usage(path) return round(used / (1024.0**3), 1) def get_disk_free_space(self, path: str | Path) -> float: - """Return free space (GiB) on disk for path.""" + """Return free space (GiB) on disk for path. + + Must be run in executor. + """ _, _, free = shutil.disk_usage(path) return round(free / (1024.0**3), 1) @@ -113,7 +122,10 @@ class HwDisk(CoreSysAttributes): return life_time_value * 10.0 def get_disk_life_time(self, path: str | Path) -> float: - """Return life time estimate of the underlying SSD drive.""" + """Return life time estimate of the underlying SSD drive. + + Must be run in executor. + """ mount_source = self._get_mount_source(str(path)) if mount_source == "overlay": return None diff --git a/supervisor/homeassistant/core.py b/supervisor/homeassistant/core.py index 5d937f348..2137a377c 100644 --- a/supervisor/homeassistant/core.py +++ b/supervisor/homeassistant/core.py @@ -33,7 +33,7 @@ from ..jobs.decorator import Job, JobCondition from ..jobs.job_group import JobGroup from ..resolution.const import ContextType, IssueType from ..utils import convert_to_ascii -from ..utils.sentry import capture_exception +from ..utils.sentry import async_capture_exception from .const import ( LANDINGPAGE, SAFE_MODE_FILENAME, @@ -160,7 +160,7 @@ class HomeAssistantCore(JobGroup): except (DockerError, JobException): pass except Exception as err: # pylint: disable=broad-except - capture_exception(err) + await async_capture_exception(err) _LOGGER.warning("Failed to install landingpage, retrying after 30sec") await asyncio.sleep(30) @@ -192,7 +192,7 @@ class HomeAssistantCore(JobGroup): except (DockerError, JobException): pass except Exception as err: # pylint: disable=broad-except - capture_exception(err) + await async_capture_exception(err) _LOGGER.warning("Error on Home Assistant installation. Retrying in 30sec") await asyncio.sleep(30) @@ -557,7 +557,7 @@ class HomeAssistantCore(JobGroup): try: await self.start() except HomeAssistantError as err: - capture_exception(err) + await async_capture_exception(err) else: break @@ -569,7 +569,7 @@ class HomeAssistantCore(JobGroup): except HomeAssistantError as err: attempts = attempts + 1 _LOGGER.error("Watchdog restart of Home Assistant failed!") - capture_exception(err) + await async_capture_exception(err) else: break diff --git a/supervisor/host/info.py b/supervisor/host/info.py index 87c7a041c..58d6f0656 100644 --- a/supervisor/host/info.py +++ b/supervisor/host/info.py @@ -102,39 +102,39 @@ class InfoCenter(CoreSysAttributes): """Return the boot timestamp.""" return self.sys_dbus.systemd.boot_timestamp - @property - def total_space(self) -> float: - """Return total space (GiB) on disk for supervisor data directory.""" - return self.sys_hardware.disk.get_disk_total_space( - self.coresys.config.path_supervisor - ) - - @property - def used_space(self) -> float: - """Return used space (GiB) on disk for supervisor data directory.""" - return self.sys_hardware.disk.get_disk_used_space( - self.coresys.config.path_supervisor - ) - - @property - def free_space(self) -> float: - """Return available space (GiB) on disk for supervisor data directory.""" - return self.sys_hardware.disk.get_disk_free_space( - self.coresys.config.path_supervisor - ) - - @property - def disk_life_time(self) -> float: - """Return the estimated life-time usage (in %) of the SSD storing the data directory.""" - return self.sys_hardware.disk.get_disk_life_time( - self.coresys.config.path_supervisor - ) - @property def virtualization(self) -> str | None: """Return virtualization hypervisor being used.""" return self.sys_dbus.systemd.virtualization + async def total_space(self) -> float: + """Return total space (GiB) on disk for supervisor data directory.""" + return await self.sys_run_in_executor( + self.sys_hardware.disk.get_disk_total_space, + self.coresys.config.path_supervisor, + ) + + async def used_space(self) -> float: + """Return used space (GiB) on disk for supervisor data directory.""" + return await self.sys_run_in_executor( + self.sys_hardware.disk.get_disk_used_space, + self.coresys.config.path_supervisor, + ) + + async def free_space(self) -> float: + """Return available space (GiB) on disk for supervisor data directory.""" + return await self.sys_run_in_executor( + self.sys_hardware.disk.get_disk_free_space, + self.coresys.config.path_supervisor, + ) + + async def disk_life_time(self) -> float: + """Return the estimated life-time usage (in %) of the SSD storing the data directory.""" + return await self.sys_run_in_executor( + self.sys_hardware.disk.get_disk_life_time, + self.coresys.config.path_supervisor, + ) + async def get_dmesg(self) -> bytes: """Return host dmesg output.""" proc = await asyncio.create_subprocess_shell( diff --git a/supervisor/jobs/__init__.py b/supervisor/jobs/__init__.py index ca8c2d07b..6aaca8762 100644 --- a/supervisor/jobs/__init__.py +++ b/supervisor/jobs/__init__.py @@ -20,7 +20,6 @@ from ..exceptions import HassioError, JobNotFound, JobStartException from ..homeassistant.const import WSEvent from ..utils.common import FileConfiguration from ..utils.dt import utcnow -from ..utils.sentry import capture_exception from .const import ATTR_IGNORE_CONDITIONS, FILE_CONFIG_JOBS, JobCondition from .validate import SCHEMA_JOBS_CONFIG @@ -191,9 +190,10 @@ class JobManager(FileConfiguration, CoreSysAttributes): """ try: return self.get_job(_CURRENT_JOB.get()) - except (LookupError, JobNotFound) as err: - capture_exception(err) - raise RuntimeError("No job for the current asyncio task!") from None + except (LookupError, JobNotFound): + raise RuntimeError( + "No job for the current asyncio task!", _LOGGER.critical + ) from None @property def is_job(self) -> bool: diff --git a/supervisor/jobs/decorator.py b/supervisor/jobs/decorator.py index 5f8934b21..033cda757 100644 --- a/supervisor/jobs/decorator.py +++ b/supervisor/jobs/decorator.py @@ -18,7 +18,7 @@ from ..exceptions import ( ) from ..host.const import HostFeature from ..resolution.const import MINIMUM_FREE_SPACE_THRESHOLD, ContextType, IssueType -from ..utils.sentry import capture_exception +from ..utils.sentry import async_capture_exception from . import SupervisorJob from .const import JobCondition, JobExecutionLimit from .job_group import JobGroup @@ -313,7 +313,7 @@ class Job(CoreSysAttributes): except Exception as err: _LOGGER.exception("Unhandled exception: %s", err) job.capture_error() - capture_exception(err) + await async_capture_exception(err) raise JobException() from err finally: self._release_exception_limits() @@ -373,13 +373,14 @@ class Job(CoreSysAttributes): if ( JobCondition.FREE_SPACE in used_conditions - and coresys.sys_host.info.free_space < MINIMUM_FREE_SPACE_THRESHOLD + and (free_space := await coresys.sys_host.info.free_space()) + < MINIMUM_FREE_SPACE_THRESHOLD ): coresys.sys_resolution.create_issue( IssueType.FREE_SPACE, ContextType.SYSTEM ) raise JobConditionException( - f"'{method_name}' blocked from execution, not enough free space ({coresys.sys_host.info.free_space}GB) left on the device" + f"'{method_name}' blocked from execution, not enough free space ({free_space}GB) left on the device" ) if JobCondition.INTERNET_SYSTEM in used_conditions: diff --git a/supervisor/misc/filter.py b/supervisor/misc/filter.py index 796072422..614209feb 100644 --- a/supervisor/misc/filter.py +++ b/supervisor/misc/filter.py @@ -80,7 +80,9 @@ def filter_data(coresys: CoreSys, event: dict, hint: dict) -> dict: "arch": coresys.arch.default, "board": coresys.os.board, "deployment": coresys.host.info.deployment, - "disk_free_space": coresys.host.info.free_space, + "disk_free_space": coresys.hardware.disk.get_disk_free_space( + coresys.config.path_supervisor + ), "host": coresys.host.info.operating_system, "kernel": coresys.host.info.kernel, "machine": coresys.machine, diff --git a/supervisor/misc/tasks.py b/supervisor/misc/tasks.py index 46b847e3d..4cc1c4e30 100644 --- a/supervisor/misc/tasks.py +++ b/supervisor/misc/tasks.py @@ -19,7 +19,7 @@ from ..homeassistant.const import LANDINGPAGE from ..jobs.decorator import Job, JobCondition, JobExecutionLimit from ..plugins.const import PLUGIN_UPDATE_CONDITIONS from ..utils.dt import utcnow -from ..utils.sentry import capture_exception +from ..utils.sentry import async_capture_exception _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -224,7 +224,7 @@ class Tasks(CoreSysAttributes): await self.sys_homeassistant.core.restart() except HomeAssistantError as err: if reanimate_fails == 0 or safe_mode: - capture_exception(err) + await async_capture_exception(err) if safe_mode: _LOGGER.critical( @@ -341,7 +341,7 @@ class Tasks(CoreSysAttributes): await (await addon.restart()) except AddonsError as err: _LOGGER.error("%s watchdog reanimation failed with %s", addon.slug, err) - capture_exception(err) + await async_capture_exception(err) finally: self._cache[addon.slug] = 0 diff --git a/supervisor/mounts/manager.py b/supervisor/mounts/manager.py index 2bd54a2d8..8289d0389 100644 --- a/supervisor/mounts/manager.py +++ b/supervisor/mounts/manager.py @@ -18,7 +18,7 @@ from ..jobs.const import JobCondition from ..jobs.decorator import Job from ..resolution.const import SuggestionType from ..utils.common import FileConfiguration -from ..utils.sentry import capture_exception +from ..utils.sentry import async_capture_exception from .const import ( ATTR_DEFAULT_BACKUP_MOUNT, ATTR_MOUNTS, @@ -177,7 +177,7 @@ class MountManager(FileConfiguration, CoreSysAttributes): if mounts[i].failed_issue in self.sys_resolution.issues: continue if not isinstance(errors[i], MountError): - capture_exception(errors[i]) + await async_capture_exception(errors[i]) self.sys_resolution.add_issue( evolve(mounts[i].failed_issue), diff --git a/supervisor/mounts/mount.py b/supervisor/mounts/mount.py index 61b69b416..59b7a7665 100644 --- a/supervisor/mounts/mount.py +++ b/supervisor/mounts/mount.py @@ -40,7 +40,7 @@ from ..exceptions import ( ) from ..resolution.const import ContextType, IssueType from ..resolution.data import Issue -from ..utils.sentry import capture_exception +from ..utils.sentry import async_capture_exception from .const import ( ATTR_PATH, ATTR_READ_ONLY, @@ -208,7 +208,7 @@ class Mount(CoreSysAttributes, ABC): try: self._state = await self.unit.get_active_state() except DBusError as err: - capture_exception(err) + await async_capture_exception(err) raise MountError( f"Could not get active state of mount due to: {err!s}" ) from err @@ -221,7 +221,7 @@ class Mount(CoreSysAttributes, ABC): self._unit = None self._state = None except DBusError as err: - capture_exception(err) + await async_capture_exception(err) raise MountError(f"Could not get mount unit due to: {err!s}") from err return self.unit diff --git a/supervisor/os/data_disk.py b/supervisor/os/data_disk.py index 40be89d24..52cb93b30 100644 --- a/supervisor/os/data_disk.py +++ b/supervisor/os/data_disk.py @@ -26,7 +26,7 @@ from ..jobs.const import JobCondition, JobExecutionLimit from ..jobs.decorator import Job from ..resolution.checks.disabled_data_disk import CheckDisabledDataDisk from ..resolution.checks.multiple_data_disks import CheckMultipleDataDisks -from ..utils.sentry import capture_exception +from ..utils.sentry import async_capture_exception from .const import ( FILESYSTEM_LABEL_DATA_DISK, FILESYSTEM_LABEL_DISABLED_DATA_DISK, @@ -337,7 +337,7 @@ class DataDisk(CoreSysAttributes): try: await block_device.format(FormatType.GPT) except DBusError as err: - capture_exception(err) + await async_capture_exception(err) raise HassOSDataDiskError( f"Could not format {new_disk.id}: {err!s}", _LOGGER.error ) from err @@ -354,7 +354,7 @@ class DataDisk(CoreSysAttributes): 0, 0, LINUX_DATA_PARTITION_GUID, PARTITION_NAME_EXTERNAL_DATA_DISK ) except DBusError as err: - capture_exception(err) + await async_capture_exception(err) raise HassOSDataDiskError( f"Could not create new data partition: {err!s}", _LOGGER.error ) from err diff --git a/supervisor/os/manager.py b/supervisor/os/manager.py index e894c1fa2..4906544e6 100644 --- a/supervisor/os/manager.py +++ b/supervisor/os/manager.py @@ -24,7 +24,7 @@ from ..exceptions import ( from ..jobs.const import JobCondition, JobExecutionLimit from ..jobs.decorator import Job from ..resolution.const import UnhealthyReason -from ..utils.sentry import capture_exception +from ..utils.sentry import async_capture_exception from .data_disk import DataDisk _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -385,7 +385,7 @@ class OSManager(CoreSysAttributes): RaucState.ACTIVE, self.get_slot_name(boot_name) ) except DBusError as err: - capture_exception(err) + await async_capture_exception(err) raise HassOSSlotUpdateError( f"Can't mark {boot_name} as active!", _LOGGER.error ) from err diff --git a/supervisor/plugins/audio.py b/supervisor/plugins/audio.py index 2911e7241..98f0b3f12 100644 --- a/supervisor/plugins/audio.py +++ b/supervisor/plugins/audio.py @@ -27,7 +27,7 @@ from ..jobs.const import JobExecutionLimit from ..jobs.decorator import Job from ..resolution.const import UnhealthyReason from ..utils.json import write_json_file -from ..utils.sentry import capture_exception +from ..utils.sentry import async_capture_exception from .base import PluginBase from .const import ( FILE_HASSIO_AUDIO, @@ -163,7 +163,7 @@ class PluginAudio(PluginBase): await self.instance.install(self.version) except DockerError as err: _LOGGER.error("Repair of Audio failed") - capture_exception(err) + await async_capture_exception(err) def pulse_client(self, input_profile=None, output_profile=None) -> str: """Generate an /etc/pulse/client.conf data.""" diff --git a/supervisor/plugins/base.py b/supervisor/plugins/base.py index 1b4b76944..b1252817f 100644 --- a/supervisor/plugins/base.py +++ b/supervisor/plugins/base.py @@ -15,7 +15,7 @@ from ..docker.interface import DockerInterface from ..docker.monitor import DockerContainerStateEvent from ..exceptions import DockerError, PluginError from ..utils.common import FileConfiguration -from ..utils.sentry import capture_exception +from ..utils.sentry import async_capture_exception from .const import WATCHDOG_MAX_ATTEMPTS, WATCHDOG_RETRY_SECONDS _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -129,7 +129,7 @@ class PluginBase(ABC, FileConfiguration, CoreSysAttributes): except PluginError as err: attempts = attempts + 1 _LOGGER.error("Watchdog restart of %s plugin failed!", self.slug) - capture_exception(err) + await async_capture_exception(err) else: break diff --git a/supervisor/plugins/cli.py b/supervisor/plugins/cli.py index d5d1d530d..d6758e46f 100644 --- a/supervisor/plugins/cli.py +++ b/supervisor/plugins/cli.py @@ -17,7 +17,7 @@ from ..docker.stats import DockerStats from ..exceptions import CliError, CliJobError, CliUpdateError, DockerError from ..jobs.const import JobExecutionLimit from ..jobs.decorator import Job -from ..utils.sentry import capture_exception +from ..utils.sentry import async_capture_exception from .base import PluginBase from .const import ( FILE_HASSIO_CLI, @@ -114,7 +114,7 @@ class PluginCli(PluginBase): await self.instance.install(self.version) except DockerError as err: _LOGGER.error("Repair of HA cli failed") - capture_exception(err) + await async_capture_exception(err) @Job( name="plugin_cli_restart_after_problem", diff --git a/supervisor/plugins/dns.py b/supervisor/plugins/dns.py index 3664ef32d..ca8a58c52 100644 --- a/supervisor/plugins/dns.py +++ b/supervisor/plugins/dns.py @@ -33,7 +33,7 @@ from ..jobs.const import JobExecutionLimit from ..jobs.decorator import Job from ..resolution.const import ContextType, IssueType, SuggestionType, UnhealthyReason from ..utils.json import write_json_file -from ..utils.sentry import capture_exception +from ..utils.sentry import async_capture_exception from ..validate import dns_url from .base import PluginBase from .const import ( @@ -410,7 +410,7 @@ class PluginDns(PluginBase): await self.instance.install(self.version) except DockerError as err: _LOGGER.error("Repair of CoreDNS failed") - capture_exception(err) + await async_capture_exception(err) def _write_resolv(self, resolv_conf: Path) -> None: """Update/Write resolv.conf file.""" diff --git a/supervisor/plugins/manager.py b/supervisor/plugins/manager.py index 4efc0b9ff..987e32aa9 100644 --- a/supervisor/plugins/manager.py +++ b/supervisor/plugins/manager.py @@ -7,7 +7,7 @@ from typing import Self from ..coresys import CoreSys, CoreSysAttributes from ..exceptions import HassioError from ..resolution.const import ContextType, IssueType, SuggestionType -from ..utils.sentry import capture_exception +from ..utils.sentry import async_capture_exception from .audio import PluginAudio from .base import PluginBase from .cli import PluginCli @@ -80,7 +80,7 @@ class PluginManager(CoreSysAttributes): reference=plugin.slug, suggestions=[SuggestionType.EXECUTE_REPAIR], ) - capture_exception(err) + await async_capture_exception(err) # Exit if supervisor out of date. Plugins can't update until then if self.sys_supervisor.need_update: @@ -114,7 +114,7 @@ class PluginManager(CoreSysAttributes): ) except Exception as err: # pylint: disable=broad-except _LOGGER.warning("Can't update plugin %s: %s", plugin.slug, err) - capture_exception(err) + await async_capture_exception(err) async def repair(self) -> None: """Repair Supervisor plugins.""" @@ -132,4 +132,4 @@ class PluginManager(CoreSysAttributes): await plugin.stop() except Exception as err: # pylint: disable=broad-except _LOGGER.warning("Can't stop plugin %s: %s", plugin.slug, err) - capture_exception(err) + await async_capture_exception(err) diff --git a/supervisor/plugins/multicast.py b/supervisor/plugins/multicast.py index ebcd3debe..9e0c22ac5 100644 --- a/supervisor/plugins/multicast.py +++ b/supervisor/plugins/multicast.py @@ -19,7 +19,7 @@ from ..exceptions import ( ) from ..jobs.const import JobExecutionLimit from ..jobs.decorator import Job -from ..utils.sentry import capture_exception +from ..utils.sentry import async_capture_exception from .base import PluginBase from .const import ( FILE_HASSIO_MULTICAST, @@ -109,7 +109,7 @@ class PluginMulticast(PluginBase): await self.instance.install(self.version) except DockerError as err: _LOGGER.error("Repair of Multicast failed") - capture_exception(err) + await async_capture_exception(err) @Job( name="plugin_multicast_restart_after_problem", diff --git a/supervisor/plugins/observer.py b/supervisor/plugins/observer.py index c2246d0cb..1d8b7fe76 100644 --- a/supervisor/plugins/observer.py +++ b/supervisor/plugins/observer.py @@ -22,7 +22,7 @@ from ..exceptions import ( ) from ..jobs.const import JobExecutionLimit from ..jobs.decorator import Job -from ..utils.sentry import capture_exception +from ..utils.sentry import async_capture_exception from .base import PluginBase from .const import ( FILE_HASSIO_OBSERVER, @@ -121,7 +121,7 @@ class PluginObserver(PluginBase): await self.instance.install(self.version) except DockerError as err: _LOGGER.error("Repair of HA observer failed") - capture_exception(err) + await async_capture_exception(err) @Job( name="plugin_observer_restart_after_problem", diff --git a/supervisor/resolution/check.py b/supervisor/resolution/check.py index 019b354e2..fd476613b 100644 --- a/supervisor/resolution/check.py +++ b/supervisor/resolution/check.py @@ -7,7 +7,7 @@ from typing import Any from ..const import ATTR_CHECKS from ..coresys import CoreSys, CoreSysAttributes from ..exceptions import ResolutionNotFound -from ..utils.sentry import capture_exception +from ..utils.sentry import async_capture_exception from .checks.base import CheckBase from .validate import get_valid_modules @@ -64,6 +64,6 @@ class ResolutionCheck(CoreSysAttributes): await check() except Exception as err: # pylint: disable=broad-except _LOGGER.error("Error during processing %s: %s", check.issue, err) - capture_exception(err) + await async_capture_exception(err) _LOGGER.info("System checks complete") diff --git a/supervisor/resolution/checks/dns_server.py b/supervisor/resolution/checks/dns_server.py index 792b89bf4..55f377e72 100644 --- a/supervisor/resolution/checks/dns_server.py +++ b/supervisor/resolution/checks/dns_server.py @@ -10,7 +10,7 @@ from ...const import CoreState from ...coresys import CoreSys from ...jobs.const import JobCondition, JobExecutionLimit from ...jobs.decorator import Job -from ...utils.sentry import capture_exception +from ...utils.sentry import async_capture_exception from ..const import DNS_CHECK_HOST, ContextType, IssueType from .base import CheckBase @@ -42,7 +42,7 @@ class CheckDNSServer(CheckBase): ContextType.DNS_SERVER, reference=dns_servers[i], ) - capture_exception(results[i]) + await async_capture_exception(results[i]) @Job(name="check_dns_server_approve", conditions=[JobCondition.INTERNET_SYSTEM]) async def approve_check(self, reference: str | None = None) -> bool: diff --git a/supervisor/resolution/checks/dns_server_ipv6.py b/supervisor/resolution/checks/dns_server_ipv6.py index ae589b40f..ab3d7bb1a 100644 --- a/supervisor/resolution/checks/dns_server_ipv6.py +++ b/supervisor/resolution/checks/dns_server_ipv6.py @@ -10,7 +10,7 @@ from ...const import CoreState from ...coresys import CoreSys from ...jobs.const import JobCondition, JobExecutionLimit from ...jobs.decorator import Job -from ...utils.sentry import capture_exception +from ...utils.sentry import async_capture_exception from ..const import DNS_CHECK_HOST, DNS_ERROR_NO_DATA, ContextType, IssueType from .base import CheckBase @@ -47,7 +47,7 @@ class CheckDNSServerIPv6(CheckBase): ContextType.DNS_SERVER, reference=dns_servers[i], ) - capture_exception(results[i]) + await async_capture_exception(results[i]) @Job( name="check_dns_server_ipv6_approve", conditions=[JobCondition.INTERNET_SYSTEM] diff --git a/supervisor/resolution/checks/free_space.py b/supervisor/resolution/checks/free_space.py index 6fb6343d7..4b6763841 100644 --- a/supervisor/resolution/checks/free_space.py +++ b/supervisor/resolution/checks/free_space.py @@ -23,7 +23,7 @@ class CheckFreeSpace(CheckBase): async def run_check(self) -> None: """Run check if not affected by issue.""" - if self.sys_host.info.free_space > MINIMUM_FREE_SPACE_THRESHOLD: + if await self.sys_host.info.free_space() > MINIMUM_FREE_SPACE_THRESHOLD: return suggestions: list[SuggestionType] = [] @@ -45,7 +45,7 @@ class CheckFreeSpace(CheckBase): async def approve_check(self, reference: str | None = None) -> bool: """Approve check if it is affected by issue.""" - if self.sys_host.info.free_space > MINIMUM_FREE_SPACE_THRESHOLD: + if await self.sys_host.info.free_space() > MINIMUM_FREE_SPACE_THRESHOLD: return False return True diff --git a/supervisor/resolution/evaluate.py b/supervisor/resolution/evaluate.py index 80eeee484..8929ec269 100644 --- a/supervisor/resolution/evaluate.py +++ b/supervisor/resolution/evaluate.py @@ -5,7 +5,7 @@ import logging from ..coresys import CoreSys, CoreSysAttributes from ..exceptions import ResolutionNotFound -from ..utils.sentry import capture_exception +from ..utils.sentry import async_capture_exception from .const import UnhealthyReason, UnsupportedReason from .evaluations.base import EvaluateBase from .validate import get_valid_modules @@ -64,7 +64,7 @@ class ResolutionEvaluation(CoreSysAttributes): _LOGGER.warning( "Error during processing %s: %s", evaluation.reason, err ) - capture_exception(err) + await async_capture_exception(err) if any(reason in self.sys_resolution.unsupported for reason in UNHEALTHY): self.sys_resolution.unhealthy = UnhealthyReason.DOCKER diff --git a/supervisor/resolution/fixup.py b/supervisor/resolution/fixup.py index c929246c5..cddc80fec 100644 --- a/supervisor/resolution/fixup.py +++ b/supervisor/resolution/fixup.py @@ -6,7 +6,7 @@ import logging from ..coresys import CoreSys, CoreSysAttributes from ..jobs.const import JobCondition from ..jobs.decorator import Job -from ..utils.sentry import capture_exception +from ..utils.sentry import async_capture_exception from .data import Issue, Suggestion from .fixups.base import FixupBase from .validate import get_valid_modules @@ -55,7 +55,7 @@ class ResolutionFixup(CoreSysAttributes): await fix() except Exception as err: # pylint: disable=broad-except _LOGGER.warning("Error during processing %s: %s", fix.suggestion, err) - capture_exception(err) + await async_capture_exception(err) _LOGGER.info("System autofix complete") diff --git a/supervisor/resolution/notify.py b/supervisor/resolution/notify.py index 294d29d79..1bd91b97c 100644 --- a/supervisor/resolution/notify.py +++ b/supervisor/resolution/notify.py @@ -36,7 +36,7 @@ class ResolutionNotify(CoreSysAttributes): messages.append( { "title": "Available space is less than 1GB!", - "message": f"Available space is {self.sys_host.info.free_space}GB, see https://www.home-assistant.io/more-info/free-space for more information.", + "message": f"Available space is {await self.sys_host.info.free_space()}GB, see https://www.home-assistant.io/more-info/free-space for more information.", "notification_id": "supervisor_issue_free_space", } ) diff --git a/supervisor/supervisor.py b/supervisor/supervisor.py index 0b8a65274..4e0d01b96 100644 --- a/supervisor/supervisor.py +++ b/supervisor/supervisor.py @@ -36,7 +36,7 @@ from .jobs.const import JobCondition, JobExecutionLimit from .jobs.decorator import Job from .resolution.const import ContextType, IssueType, UnhealthyReason from .utils.codenotary import calc_checksum -from .utils.sentry import capture_exception +from .utils.sentry import async_capture_exception _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -219,7 +219,7 @@ class Supervisor(CoreSysAttributes): self.sys_resolution.create_issue( IssueType.UPDATE_FAILED, ContextType.SUPERVISOR ) - capture_exception(err) + await async_capture_exception(err) raise SupervisorUpdateError( f"Update of Supervisor failed: {err!s}", _LOGGER.critical ) from err diff --git a/supervisor/utils/dbus.py b/supervisor/utils/dbus.py index b24628d65..a5bc4d102 100644 --- a/supervisor/utils/dbus.py +++ b/supervisor/utils/dbus.py @@ -35,7 +35,7 @@ from ..exceptions import ( DBusTimeoutError, HassioNotSupportedError, ) -from .sentry import capture_exception +from .sentry import async_capture_exception _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -124,7 +124,7 @@ class DBus: ) raise DBus.from_dbus_error(err) from None except Exception as err: # pylint: disable=broad-except - capture_exception(err) + await async_capture_exception(err) raise DBusFatalError(str(err)) from err def _add_interfaces(self): diff --git a/supervisor/utils/log_format.py b/supervisor/utils/log_format.py index c73dbe3bf..503f8d6ef 100644 --- a/supervisor/utils/log_format.py +++ b/supervisor/utils/log_format.py @@ -3,8 +3,6 @@ import logging import re -from .sentry import capture_exception - _LOGGER: logging.Logger = logging.getLogger(__name__) RE_BIND_FAILED = re.compile( @@ -13,13 +11,11 @@ RE_BIND_FAILED = re.compile( def format_message(message: str) -> str: - """Return a formated message if it's known.""" - try: - match = RE_BIND_FAILED.match(message) - if match: - return f"Port '{match.group(1)}' is already in use by something else on the host." - except TypeError as err: - _LOGGER.error("The type of message is not a string - %s", err) - capture_exception(err) + """Return a formatted message if it's known.""" + match = RE_BIND_FAILED.match(message) + if match: + return ( + f"Port '{match.group(1)}' is already in use by something else on the host." + ) return message diff --git a/supervisor/utils/sentry.py b/supervisor/utils/sentry.py index da6fa0190..aae6118ed 100644 --- a/supervisor/utils/sentry.py +++ b/supervisor/utils/sentry.py @@ -1,5 +1,6 @@ """Utilities for sentry.""" +import asyncio from functools import partial import logging from typing import Any @@ -46,19 +47,43 @@ def init_sentry(coresys: CoreSys) -> None: def capture_event(event: dict[str, Any], only_once: str | None = None): - """Capture an event and send to sentry.""" + """Capture an event and send to sentry. + + Must be called in executor. + """ if sentry_sdk.is_initialized(): if only_once and only_once not in only_once_events: only_once_events.add(only_once) sentry_sdk.capture_event(event) +async def async_capture_event(event: dict[str, Any], only_once: str | None = None): + """Capture an event and send to sentry. + + Safe to call from event loop. + """ + await asyncio.get_running_loop().run_in_executor( + None, capture_event, event, only_once + ) + + def capture_exception(err: Exception) -> None: - """Capture an exception and send to sentry.""" + """Capture an exception and send to sentry. + + Must be called in executor. + """ if sentry_sdk.is_initialized(): sentry_sdk.capture_exception(err) +async def async_capture_exception(err: Exception) -> None: + """Capture an exception and send to sentry. + + Safe to call in event loop. + """ + await asyncio.get_running_loop().run_in_executor(None, capture_exception, err) + + def close_sentry() -> None: """Close the current sentry client. diff --git a/tests/api/test_supervisor.py b/tests/api/test_supervisor.py index 06735137c..330818aac 100644 --- a/tests/api/test_supervisor.py +++ b/tests/api/test_supervisor.py @@ -216,7 +216,7 @@ async def test_api_supervisor_fallback_log_capture( "No systemd-journal-gatewayd Unix socket available!" ) - with patch("supervisor.api.capture_exception") as capture_exception: + with patch("supervisor.api.async_capture_exception") as capture_exception: await api_client.get("/supervisor/logs") capture_exception.assert_not_called() @@ -224,7 +224,7 @@ async def test_api_supervisor_fallback_log_capture( journald_logs.side_effect = HassioError("Something bad happened!") - with patch("supervisor.api.capture_exception") as capture_exception: + with patch("supervisor.api.async_capture_exception") as capture_exception: await api_client.get("/supervisor/logs") capture_exception.assert_called_once() diff --git a/tests/homeassistant/test_home_assistant_watchdog.py b/tests/homeassistant/test_home_assistant_watchdog.py index ff5e117c5..9208c3aaf 100644 --- a/tests/homeassistant/test_home_assistant_watchdog.py +++ b/tests/homeassistant/test_home_assistant_watchdog.py @@ -141,7 +141,7 @@ async def test_home_assistant_watchdog_rebuild_on_failure(coresys: CoreSys) -> N time=1, ), ) - await asyncio.sleep(0) + await asyncio.sleep(0.1) start.assert_called_once() rebuild.assert_called_once() diff --git a/tests/host/test_info.py b/tests/host/test_info.py index 7a25b4c44..9d8398183 100644 --- a/tests/host/test_info.py +++ b/tests/host/test_info.py @@ -5,10 +5,10 @@ from unittest.mock import patch from supervisor.host.info import InfoCenter -def test_host_free_space(coresys): +async def test_host_free_space(coresys): """Test host free space.""" info = InfoCenter(coresys) with patch("shutil.disk_usage", return_value=(42, 42, 2 * (1024.0**3))): - free = info.free_space + free = await info.free_space() assert free == 2.0 diff --git a/tests/utils/test_log_format.py b/tests/utils/test_log_format.py index f0daf8d4c..be1f90462 100644 --- a/tests/utils/test_log_format.py +++ b/tests/utils/test_log_format.py @@ -19,9 +19,3 @@ def test_format_message_port_alternative(): format_message(message) == "Port '80' is already in use by something else on the host." ) - - -def test_exeption(): - """Tests the exception handling.""" - message = b"byte" - assert format_message(message) == message