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 <stefan@agner.ch>
This commit is contained in:
Mike Degatano 2025-03-01 10:02:43 -05:00 committed by GitHub
parent 12c951f62d
commit 86133f8ecd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 213 additions and 175 deletions

View File

@ -54,7 +54,7 @@ if __name__ == "__main__":
loop.set_debug(coresys.config.debug) loop.set_debug(coresys.config.debug)
loop.run_until_complete(coresys.core.connect()) loop.run_until_complete(coresys.core.connect())
bootstrap.supervisor_debugger(coresys) loop.run_until_complete(bootstrap.supervisor_debugger(coresys))
# Signal health startup for container # Signal health startup for container
run_os_startup_check_cleanup() run_os_startup_check_cleanup()

View File

@ -88,7 +88,7 @@ from ..store.addon import AddonStore
from ..utils import check_port from ..utils import check_port
from ..utils.apparmor import adjust_profile from ..utils.apparmor import adjust_profile
from ..utils.json import read_json_file, write_json_file 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 ( from .const import (
WATCHDOG_MAX_ATTEMPTS, WATCHDOG_MAX_ATTEMPTS,
WATCHDOG_RETRY_SECONDS, WATCHDOG_RETRY_SECONDS,
@ -1530,7 +1530,7 @@ class Addon(AddonModel):
except AddonsError as err: except AddonsError as err:
attempts = attempts + 1 attempts = attempts + 1
_LOGGER.error("Watchdog restart of addon %s failed!", self.name) _LOGGER.error("Watchdog restart of addon %s failed!", self.name)
capture_exception(err) await async_capture_exception(err)
else: else:
break break

View File

@ -23,7 +23,7 @@ from ..exceptions import (
from ..jobs.decorator import Job, JobCondition from ..jobs.decorator import Job, JobCondition
from ..resolution.const import ContextType, IssueType, SuggestionType from ..resolution.const import ContextType, IssueType, SuggestionType
from ..store.addon import AddonStore from ..store.addon import AddonStore
from ..utils.sentry import capture_exception from ..utils.sentry import async_capture_exception
from .addon import Addon from .addon import Addon
from .const import ADDON_UPDATE_CONDITIONS from .const import ADDON_UPDATE_CONDITIONS
from .data import AddonsData from .data import AddonsData
@ -170,7 +170,7 @@ class AddonManager(CoreSysAttributes):
await addon.stop() await addon.stop()
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
_LOGGER.warning("Can't stop Add-on %s: %s", addon.slug, err) _LOGGER.warning("Can't stop Add-on %s: %s", addon.slug, err)
capture_exception(err) await async_capture_exception(err)
@Job( @Job(
name="addon_manager_install", name="addon_manager_install",
@ -388,7 +388,7 @@ class AddonManager(CoreSysAttributes):
reference=addon.slug, reference=addon.slug,
suggestions=[SuggestionType.EXECUTE_REPAIR], suggestions=[SuggestionType.EXECUTE_REPAIR],
) )
capture_exception(err) await async_capture_exception(err)
else: else:
add_host_coros.append( add_host_coros.append(
self.sys_plugins.dns.add_host( self.sys_plugins.dns.add_host(

View File

@ -210,18 +210,6 @@ class AddonModel(JobGroup, ABC):
"""Return description of add-on.""" """Return description of add-on."""
return self.data[ATTR_DESCRIPTON] 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 @property
def repository(self) -> str: def repository(self) -> str:
"""Return repository of add-on.""" """Return repository of add-on."""
@ -646,6 +634,21 @@ class AddonModel(JobGroup, ABC):
"""Return breaking versions of addon.""" """Return breaking versions of addon."""
return self.data[ATTR_BREAKING_VERSIONS] 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]: def refresh_path_cache(self) -> Awaitable[None]:
"""Refresh cache of existing paths.""" """Refresh cache of existing paths."""

View File

@ -10,7 +10,7 @@ from aiohttp import web
from ..const import AddonState from ..const import AddonState
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import APIAddonNotInstalled, HostNotSupportedError from ..exceptions import APIAddonNotInstalled, HostNotSupportedError
from ..utils.sentry import capture_exception from ..utils.sentry import async_capture_exception
from .addons import APIAddons from .addons import APIAddons
from .audio import APIAudio from .audio import APIAudio
from .auth import APIAuth from .auth import APIAuth
@ -412,7 +412,7 @@ class RestAPI(CoreSysAttributes):
if not isinstance(err, HostNotSupportedError): if not isinstance(err, HostNotSupportedError):
# No need to capture HostNotSupportedError to Sentry, the cause # No need to capture HostNotSupportedError to Sentry, the cause
# is known and reported to the user using the resolution center. # 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 kwargs.pop("follow", None) # Follow is not supported for Docker logs
return await api_supervisor.logs(*args, **kwargs) return await api_supervisor.logs(*args, **kwargs)

View File

@ -212,7 +212,7 @@ class APIAddons(CoreSysAttributes):
ATTR_HOSTNAME: addon.hostname, ATTR_HOSTNAME: addon.hostname,
ATTR_DNS: addon.dns, ATTR_DNS: addon.dns,
ATTR_DESCRIPTON: addon.description, ATTR_DESCRIPTON: addon.description,
ATTR_LONG_DESCRIPTION: addon.long_description, ATTR_LONG_DESCRIPTION: await addon.long_description(),
ATTR_ADVANCED: addon.advanced, ATTR_ADVANCED: addon.advanced,
ATTR_STAGE: addon.stage, ATTR_STAGE: addon.stage,
ATTR_REPOSITORY: addon.repository, ATTR_REPOSITORY: addon.repository,

View File

@ -98,10 +98,10 @@ class APIHost(CoreSysAttributes):
ATTR_VIRTUALIZATION: self.sys_host.info.virtualization, ATTR_VIRTUALIZATION: self.sys_host.info.virtualization,
ATTR_CPE: self.sys_host.info.cpe, ATTR_CPE: self.sys_host.info.cpe,
ATTR_DEPLOYMENT: self.sys_host.info.deployment, ATTR_DEPLOYMENT: self.sys_host.info.deployment,
ATTR_DISK_FREE: self.sys_host.info.free_space, ATTR_DISK_FREE: await self.sys_host.info.free_space(),
ATTR_DISK_TOTAL: self.sys_host.info.total_space, ATTR_DISK_TOTAL: await self.sys_host.info.total_space(),
ATTR_DISK_USED: self.sys_host.info.used_space, ATTR_DISK_USED: await self.sys_host.info.used_space(),
ATTR_DISK_LIFE_TIME: self.sys_host.info.disk_life_time, ATTR_DISK_LIFE_TIME: await self.sys_host.info.disk_life_time(),
ATTR_FEATURES: self.sys_host.features, ATTR_FEATURES: self.sys_host.features,
ATTR_HOSTNAME: self.sys_host.info.hostname, ATTR_HOSTNAME: self.sys_host.info.hostname,
ATTR_LLMNR_HOSTNAME: self.sys_host.info.llmnr_hostname, ATTR_LLMNR_HOSTNAME: self.sys_host.info.llmnr_hostname,

View File

@ -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. """Read in a static file asset for API output.
Must be run in executor. Must be run in executor.
""" """
with path.open("r") as asset: with path.open("rb" if binary else "r") as asset:
return asset.read() return asset.read()
@ -109,7 +109,7 @@ class APIStore(CoreSysAttributes):
return self.sys_store.get(repository_slug) return self.sys_store.get(repository_slug)
def _generate_addon_information( async def _generate_addon_information(
self, addon: AddonStore, extended: bool = False self, addon: AddonStore, extended: bool = False
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Generate addon information.""" """Generate addon information."""
@ -156,7 +156,7 @@ class APIStore(CoreSysAttributes):
ATTR_HOST_NETWORK: addon.host_network, ATTR_HOST_NETWORK: addon.host_network,
ATTR_HOST_PID: addon.host_pid, ATTR_HOST_PID: addon.host_pid,
ATTR_INGRESS: addon.with_ingress, ATTR_INGRESS: addon.with_ingress,
ATTR_LONG_DESCRIPTION: addon.long_description, ATTR_LONG_DESCRIPTION: await addon.long_description(),
ATTR_RATING: rating_security(addon), ATTR_RATING: rating_security(addon),
ATTR_SIGNED: addon.signed, ATTR_SIGNED: addon.signed,
} }
@ -185,10 +185,12 @@ class APIStore(CoreSysAttributes):
async def store_info(self, request: web.Request) -> dict[str, Any]: async def store_info(self, request: web.Request) -> dict[str, Any]:
"""Return store information.""" """Return store information."""
return { return {
ATTR_ADDONS: [ ATTR_ADDONS: await asyncio.gather(
*[
self._generate_addon_information(self.sys_addons.store[addon]) self._generate_addon_information(self.sys_addons.store[addon])
for addon in self.sys_addons.store for addon in self.sys_addons.store
], ]
),
ATTR_REPOSITORIES: [ ATTR_REPOSITORIES: [
self._generate_repository_information(repository) self._generate_repository_information(repository)
for repository in self.sys_store.all 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]: async def addons_list(self, request: web.Request) -> dict[str, Any]:
"""Return all store add-ons.""" """Return all store add-ons."""
return { return {
ATTR_ADDONS: [ ATTR_ADDONS: await asyncio.gather(
*[
self._generate_addon_information(self.sys_addons.store[addon]) self._generate_addon_information(self.sys_addons.store[addon])
for addon in self.sys_addons.store for addon in self.sys_addons.store
] ]
)
} }
@api_process @api_process
@ -234,7 +238,7 @@ class APIStore(CoreSysAttributes):
async def addons_addon_info_wrapped(self, request: web.Request) -> dict[str, Any]: async def addons_addon_info_wrapped(self, request: web.Request) -> dict[str, Any]:
"""Return add-on information directly (not api).""" """Return add-on information directly (not api)."""
addon: AddonStore = self._extract_addon(request) 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) @api_process_raw(CONTENT_TYPE_PNG)
async def addons_addon_icon(self, request: web.Request) -> bytes: async def addons_addon_icon(self, request: web.Request) -> bytes:
@ -243,7 +247,7 @@ class APIStore(CoreSysAttributes):
if not addon.with_icon: if not addon.with_icon:
raise APIError(f"No icon found for add-on {addon.slug}!") 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) @api_process_raw(CONTENT_TYPE_PNG)
async def addons_addon_logo(self, request: web.Request) -> bytes: async def addons_addon_logo(self, request: web.Request) -> bytes:
@ -252,7 +256,7 @@ class APIStore(CoreSysAttributes):
if not addon.with_logo: if not addon.with_logo:
raise APIError(f"No logo found for add-on {addon.slug}!") 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) @api_process_raw(CONTENT_TYPE_TEXT)
async def addons_addon_changelog(self, request: web.Request) -> str: async def addons_addon_changelog(self, request: web.Request) -> str:

View File

@ -36,7 +36,7 @@ from ..resolution.const import UnhealthyReason
from ..utils.common import FileConfiguration from ..utils.common import FileConfiguration
from ..utils.dt import utcnow from ..utils.dt import utcnow
from ..utils.sentinel import DEFAULT from ..utils.sentinel import DEFAULT
from ..utils.sentry import capture_exception from ..utils.sentry import async_capture_exception
from .backup import Backup from .backup import Backup
from .const import ( from .const import (
DEFAULT_FREEZE_TIMEOUT, DEFAULT_FREEZE_TIMEOUT,
@ -525,7 +525,7 @@ class BackupManager(FileConfiguration, JobGroup):
return None return None
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
_LOGGER.exception("Backup %s error", backup.slug) _LOGGER.exception("Backup %s error", backup.slug)
capture_exception(err) await async_capture_exception(err)
self.sys_jobs.current.capture_error( self.sys_jobs.current.capture_error(
BackupError(f"Backup {backup.slug} error, see supervisor logs") BackupError(f"Backup {backup.slug} error, see supervisor logs")
) )
@ -718,7 +718,7 @@ class BackupManager(FileConfiguration, JobGroup):
raise raise
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
_LOGGER.exception("Restore %s error", backup.slug) _LOGGER.exception("Restore %s error", backup.slug)
capture_exception(err) await async_capture_exception(err)
raise BackupError( raise BackupError(
f"Restore {backup.slug} error, see supervisor logs" f"Restore {backup.slug} error, see supervisor logs"
) from err ) from err

View File

@ -1,6 +1,7 @@
"""Bootstrap Supervisor.""" """Bootstrap Supervisor."""
# ruff: noqa: T100 # ruff: noqa: T100
from importlib import import_module
import logging import logging
import os import os
import signal import signal
@ -306,12 +307,12 @@ def reg_signal(loop, coresys: CoreSys) -> None:
_LOGGER.warning("Could not bind to SIGINT") _LOGGER.warning("Could not bind to SIGINT")
def supervisor_debugger(coresys: CoreSys) -> None: async def supervisor_debugger(coresys: CoreSys) -> None:
"""Start debugger if needed.""" """Start debugger if needed."""
if not coresys.config.debug: if not coresys.config.debug:
return return
# pylint: disable=import-outside-toplevel
import debugpy debugpy = await coresys.run_in_executor(import_module, "debugpy")
_LOGGER.info("Initializing Supervisor debugger") _LOGGER.info("Initializing Supervisor debugger")

View File

@ -26,7 +26,7 @@ from .exceptions import (
from .homeassistant.core import LANDINGPAGE from .homeassistant.core import LANDINGPAGE
from .resolution.const import ContextType, IssueType, SuggestionType, UnhealthyReason from .resolution.const import ContextType, IssueType, SuggestionType, UnhealthyReason
from .utils.dt import utcnow 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 from .utils.whoami import WhoamiData, retrieve_whoami
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -172,7 +172,7 @@ class Core(CoreSysAttributes):
"Fatal error happening on load Task %s: %s", setup_task, err "Fatal error happening on load Task %s: %s", setup_task, err
) )
self.sys_resolution.unhealthy = UnhealthyReason.SETUP self.sys_resolution.unhealthy = UnhealthyReason.SETUP
capture_exception(err) await async_capture_exception(err)
# Set OS Agent diagnostics if needed # Set OS Agent diagnostics if needed
if ( if (
@ -189,7 +189,7 @@ class Core(CoreSysAttributes):
self.sys_config.diagnostics, self.sys_config.diagnostics,
err, err,
) )
capture_exception(err) await async_capture_exception(err)
# Evaluate the system # Evaluate the system
await self.sys_resolution.evaluate.evaluate_system() await self.sys_resolution.evaluate.evaluate_system()
@ -246,12 +246,12 @@ class Core(CoreSysAttributes):
await self.sys_homeassistant.core.start() await self.sys_homeassistant.core.start()
except HomeAssistantCrashError as err: except HomeAssistantCrashError as err:
_LOGGER.error("Can't start Home Assistant Core - rebuiling") _LOGGER.error("Can't start Home Assistant Core - rebuiling")
capture_exception(err) await async_capture_exception(err)
with suppress(HomeAssistantError): with suppress(HomeAssistantError):
await self.sys_homeassistant.core.rebuild() await self.sys_homeassistant.core.rebuild()
except HomeAssistantError as err: except HomeAssistantError as err:
capture_exception(err) await async_capture_exception(err)
else: else:
_LOGGER.info("Skipping start of Home Assistant") _LOGGER.info("Skipping start of Home Assistant")

View File

@ -15,7 +15,7 @@ from ...exceptions import (
HostNotSupportedError, HostNotSupportedError,
NetworkInterfaceNotFound, NetworkInterfaceNotFound,
) )
from ...utils.sentry import capture_exception from ...utils.sentry import async_capture_exception
from ..const import ( from ..const import (
DBUS_ATTR_CONNECTION_ENABLED, DBUS_ATTR_CONNECTION_ENABLED,
DBUS_ATTR_DEVICES, DBUS_ATTR_DEVICES,
@ -223,13 +223,13 @@ class NetworkManager(DBusInterfaceProxy):
device, device,
err, err,
) )
capture_exception(err) await async_capture_exception(err)
return return
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
_LOGGER.exception( _LOGGER.exception(
"Unkown error while processing %s: %s", device, err "Unkown error while processing %s: %s", device, err
) )
capture_exception(err) await async_capture_exception(err)
continue continue
# Skeep interface # Skeep interface

View File

@ -42,7 +42,7 @@ from ..hardware.data import Device
from ..jobs.const import JobCondition, JobExecutionLimit from ..jobs.const import JobCondition, JobExecutionLimit
from ..jobs.decorator import Job from ..jobs.decorator import Job
from ..resolution.const import CGROUP_V2_VERSION, ContextType, IssueType, SuggestionType 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 ( from .const import (
ENV_TIME, ENV_TIME,
ENV_TOKEN, ENV_TOKEN,
@ -606,7 +606,7 @@ class DockerAddon(DockerInterface):
) )
except CoreDNSError as err: except CoreDNSError as err:
_LOGGER.warning("Can't update DNS for %s", self.name) _LOGGER.warning("Can't update DNS for %s", self.name)
capture_exception(err) await async_capture_exception(err)
# Hardware Access # Hardware Access
if self.addon.static_devices: if self.addon.static_devices:
@ -787,7 +787,7 @@ class DockerAddon(DockerInterface):
await self.sys_plugins.dns.delete_host(self.addon.hostname) await self.sys_plugins.dns.delete_host(self.addon.hostname)
except CoreDNSError as err: except CoreDNSError as err:
_LOGGER.warning("Can't update DNS for %s", self.name) _LOGGER.warning("Can't update DNS for %s", self.name)
capture_exception(err) await async_capture_exception(err)
# Hardware # Hardware
if self._hw_listener: if self._hw_listener:

View File

@ -42,7 +42,7 @@ from ..jobs.const import JOB_GROUP_DOCKER_INTERFACE, JobExecutionLimit
from ..jobs.decorator import Job from ..jobs.decorator import Job
from ..jobs.job_group import JobGroup from ..jobs.job_group import JobGroup
from ..resolution.const import ContextType, IssueType, SuggestionType 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 .const import ContainerState, RestartPolicy
from .manager import CommandReturn from .manager import CommandReturn
from .monitor import DockerContainerStateEvent from .monitor import DockerContainerStateEvent
@ -278,7 +278,7 @@ class DockerInterface(JobGroup):
f"Can't install {image}:{version!s}: {err}", _LOGGER.error f"Can't install {image}:{version!s}: {err}", _LOGGER.error
) from err ) from err
except (docker.errors.DockerException, requests.RequestException) as err: except (docker.errors.DockerException, requests.RequestException) as err:
capture_exception(err) await async_capture_exception(err)
raise DockerError( raise DockerError(
f"Unknown error with {image}:{version!s} -> {err!s}", _LOGGER.error f"Unknown error with {image}:{version!s} -> {err!s}", _LOGGER.error
) from err ) from err
@ -394,7 +394,7 @@ class DockerInterface(JobGroup):
) )
except DockerNotFound as err: except DockerNotFound as err:
# If image is missing, capture the exception as this shouldn't happen # If image is missing, capture the exception as this shouldn't happen
capture_exception(err) await async_capture_exception(err)
raise raise
# Store metadata # Store metadata

View File

@ -49,17 +49,26 @@ class HwDisk(CoreSysAttributes):
return False return False
def get_disk_total_space(self, path: str | Path) -> float: 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) total, _, _ = shutil.disk_usage(path)
return round(total / (1024.0**3), 1) return round(total / (1024.0**3), 1)
def get_disk_used_space(self, path: str | Path) -> float: 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) _, used, _ = shutil.disk_usage(path)
return round(used / (1024.0**3), 1) return round(used / (1024.0**3), 1)
def get_disk_free_space(self, path: str | Path) -> float: 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) _, _, free = shutil.disk_usage(path)
return round(free / (1024.0**3), 1) return round(free / (1024.0**3), 1)
@ -113,7 +122,10 @@ class HwDisk(CoreSysAttributes):
return life_time_value * 10.0 return life_time_value * 10.0
def get_disk_life_time(self, path: str | Path) -> float: 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)) mount_source = self._get_mount_source(str(path))
if mount_source == "overlay": if mount_source == "overlay":
return None return None

View File

@ -33,7 +33,7 @@ from ..jobs.decorator import Job, JobCondition
from ..jobs.job_group import JobGroup from ..jobs.job_group import JobGroup
from ..resolution.const import ContextType, IssueType from ..resolution.const import ContextType, IssueType
from ..utils import convert_to_ascii from ..utils import convert_to_ascii
from ..utils.sentry import capture_exception from ..utils.sentry import async_capture_exception
from .const import ( from .const import (
LANDINGPAGE, LANDINGPAGE,
SAFE_MODE_FILENAME, SAFE_MODE_FILENAME,
@ -160,7 +160,7 @@ class HomeAssistantCore(JobGroup):
except (DockerError, JobException): except (DockerError, JobException):
pass pass
except Exception as err: # pylint: disable=broad-except 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") _LOGGER.warning("Failed to install landingpage, retrying after 30sec")
await asyncio.sleep(30) await asyncio.sleep(30)
@ -192,7 +192,7 @@ class HomeAssistantCore(JobGroup):
except (DockerError, JobException): except (DockerError, JobException):
pass pass
except Exception as err: # pylint: disable=broad-except 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") _LOGGER.warning("Error on Home Assistant installation. Retrying in 30sec")
await asyncio.sleep(30) await asyncio.sleep(30)
@ -557,7 +557,7 @@ class HomeAssistantCore(JobGroup):
try: try:
await self.start() await self.start()
except HomeAssistantError as err: except HomeAssistantError as err:
capture_exception(err) await async_capture_exception(err)
else: else:
break break
@ -569,7 +569,7 @@ class HomeAssistantCore(JobGroup):
except HomeAssistantError as err: except HomeAssistantError as err:
attempts = attempts + 1 attempts = attempts + 1
_LOGGER.error("Watchdog restart of Home Assistant failed!") _LOGGER.error("Watchdog restart of Home Assistant failed!")
capture_exception(err) await async_capture_exception(err)
else: else:
break break

View File

@ -102,39 +102,39 @@ class InfoCenter(CoreSysAttributes):
"""Return the boot timestamp.""" """Return the boot timestamp."""
return self.sys_dbus.systemd.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 @property
def virtualization(self) -> str | None: def virtualization(self) -> str | None:
"""Return virtualization hypervisor being used.""" """Return virtualization hypervisor being used."""
return self.sys_dbus.systemd.virtualization 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: async def get_dmesg(self) -> bytes:
"""Return host dmesg output.""" """Return host dmesg output."""
proc = await asyncio.create_subprocess_shell( proc = await asyncio.create_subprocess_shell(

View File

@ -20,7 +20,6 @@ from ..exceptions import HassioError, JobNotFound, JobStartException
from ..homeassistant.const import WSEvent from ..homeassistant.const import WSEvent
from ..utils.common import FileConfiguration from ..utils.common import FileConfiguration
from ..utils.dt import utcnow from ..utils.dt import utcnow
from ..utils.sentry import capture_exception
from .const import ATTR_IGNORE_CONDITIONS, FILE_CONFIG_JOBS, JobCondition from .const import ATTR_IGNORE_CONDITIONS, FILE_CONFIG_JOBS, JobCondition
from .validate import SCHEMA_JOBS_CONFIG from .validate import SCHEMA_JOBS_CONFIG
@ -191,9 +190,10 @@ class JobManager(FileConfiguration, CoreSysAttributes):
""" """
try: try:
return self.get_job(_CURRENT_JOB.get()) return self.get_job(_CURRENT_JOB.get())
except (LookupError, JobNotFound) as err: except (LookupError, JobNotFound):
capture_exception(err) raise RuntimeError(
raise RuntimeError("No job for the current asyncio task!") from None "No job for the current asyncio task!", _LOGGER.critical
) from None
@property @property
def is_job(self) -> bool: def is_job(self) -> bool:

View File

@ -18,7 +18,7 @@ from ..exceptions import (
) )
from ..host.const import HostFeature from ..host.const import HostFeature
from ..resolution.const import MINIMUM_FREE_SPACE_THRESHOLD, ContextType, IssueType 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 . import SupervisorJob
from .const import JobCondition, JobExecutionLimit from .const import JobCondition, JobExecutionLimit
from .job_group import JobGroup from .job_group import JobGroup
@ -313,7 +313,7 @@ class Job(CoreSysAttributes):
except Exception as err: except Exception as err:
_LOGGER.exception("Unhandled exception: %s", err) _LOGGER.exception("Unhandled exception: %s", err)
job.capture_error() job.capture_error()
capture_exception(err) await async_capture_exception(err)
raise JobException() from err raise JobException() from err
finally: finally:
self._release_exception_limits() self._release_exception_limits()
@ -373,13 +373,14 @@ class Job(CoreSysAttributes):
if ( if (
JobCondition.FREE_SPACE in used_conditions 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( coresys.sys_resolution.create_issue(
IssueType.FREE_SPACE, ContextType.SYSTEM IssueType.FREE_SPACE, ContextType.SYSTEM
) )
raise JobConditionException( 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: if JobCondition.INTERNET_SYSTEM in used_conditions:

View File

@ -80,7 +80,9 @@ def filter_data(coresys: CoreSys, event: dict, hint: dict) -> dict:
"arch": coresys.arch.default, "arch": coresys.arch.default,
"board": coresys.os.board, "board": coresys.os.board,
"deployment": coresys.host.info.deployment, "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, "host": coresys.host.info.operating_system,
"kernel": coresys.host.info.kernel, "kernel": coresys.host.info.kernel,
"machine": coresys.machine, "machine": coresys.machine,

View File

@ -19,7 +19,7 @@ from ..homeassistant.const import LANDINGPAGE
from ..jobs.decorator import Job, JobCondition, JobExecutionLimit from ..jobs.decorator import Job, JobCondition, JobExecutionLimit
from ..plugins.const import PLUGIN_UPDATE_CONDITIONS from ..plugins.const import PLUGIN_UPDATE_CONDITIONS
from ..utils.dt import utcnow from ..utils.dt import utcnow
from ..utils.sentry import capture_exception from ..utils.sentry import async_capture_exception
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -224,7 +224,7 @@ class Tasks(CoreSysAttributes):
await self.sys_homeassistant.core.restart() await self.sys_homeassistant.core.restart()
except HomeAssistantError as err: except HomeAssistantError as err:
if reanimate_fails == 0 or safe_mode: if reanimate_fails == 0 or safe_mode:
capture_exception(err) await async_capture_exception(err)
if safe_mode: if safe_mode:
_LOGGER.critical( _LOGGER.critical(
@ -341,7 +341,7 @@ class Tasks(CoreSysAttributes):
await (await addon.restart()) await (await addon.restart())
except AddonsError as err: except AddonsError as err:
_LOGGER.error("%s watchdog reanimation failed with %s", addon.slug, err) _LOGGER.error("%s watchdog reanimation failed with %s", addon.slug, err)
capture_exception(err) await async_capture_exception(err)
finally: finally:
self._cache[addon.slug] = 0 self._cache[addon.slug] = 0

View File

@ -18,7 +18,7 @@ from ..jobs.const import JobCondition
from ..jobs.decorator import Job from ..jobs.decorator import Job
from ..resolution.const import SuggestionType from ..resolution.const import SuggestionType
from ..utils.common import FileConfiguration from ..utils.common import FileConfiguration
from ..utils.sentry import capture_exception from ..utils.sentry import async_capture_exception
from .const import ( from .const import (
ATTR_DEFAULT_BACKUP_MOUNT, ATTR_DEFAULT_BACKUP_MOUNT,
ATTR_MOUNTS, ATTR_MOUNTS,
@ -177,7 +177,7 @@ class MountManager(FileConfiguration, CoreSysAttributes):
if mounts[i].failed_issue in self.sys_resolution.issues: if mounts[i].failed_issue in self.sys_resolution.issues:
continue continue
if not isinstance(errors[i], MountError): if not isinstance(errors[i], MountError):
capture_exception(errors[i]) await async_capture_exception(errors[i])
self.sys_resolution.add_issue( self.sys_resolution.add_issue(
evolve(mounts[i].failed_issue), evolve(mounts[i].failed_issue),

View File

@ -40,7 +40,7 @@ from ..exceptions import (
) )
from ..resolution.const import ContextType, IssueType from ..resolution.const import ContextType, IssueType
from ..resolution.data import Issue from ..resolution.data import Issue
from ..utils.sentry import capture_exception from ..utils.sentry import async_capture_exception
from .const import ( from .const import (
ATTR_PATH, ATTR_PATH,
ATTR_READ_ONLY, ATTR_READ_ONLY,
@ -208,7 +208,7 @@ class Mount(CoreSysAttributes, ABC):
try: try:
self._state = await self.unit.get_active_state() self._state = await self.unit.get_active_state()
except DBusError as err: except DBusError as err:
capture_exception(err) await async_capture_exception(err)
raise MountError( raise MountError(
f"Could not get active state of mount due to: {err!s}" f"Could not get active state of mount due to: {err!s}"
) from err ) from err
@ -221,7 +221,7 @@ class Mount(CoreSysAttributes, ABC):
self._unit = None self._unit = None
self._state = None self._state = None
except DBusError as err: 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 raise MountError(f"Could not get mount unit due to: {err!s}") from err
return self.unit return self.unit

View File

@ -26,7 +26,7 @@ from ..jobs.const import JobCondition, JobExecutionLimit
from ..jobs.decorator import Job from ..jobs.decorator import Job
from ..resolution.checks.disabled_data_disk import CheckDisabledDataDisk from ..resolution.checks.disabled_data_disk import CheckDisabledDataDisk
from ..resolution.checks.multiple_data_disks import CheckMultipleDataDisks from ..resolution.checks.multiple_data_disks import CheckMultipleDataDisks
from ..utils.sentry import capture_exception from ..utils.sentry import async_capture_exception
from .const import ( from .const import (
FILESYSTEM_LABEL_DATA_DISK, FILESYSTEM_LABEL_DATA_DISK,
FILESYSTEM_LABEL_DISABLED_DATA_DISK, FILESYSTEM_LABEL_DISABLED_DATA_DISK,
@ -337,7 +337,7 @@ class DataDisk(CoreSysAttributes):
try: try:
await block_device.format(FormatType.GPT) await block_device.format(FormatType.GPT)
except DBusError as err: except DBusError as err:
capture_exception(err) await async_capture_exception(err)
raise HassOSDataDiskError( raise HassOSDataDiskError(
f"Could not format {new_disk.id}: {err!s}", _LOGGER.error f"Could not format {new_disk.id}: {err!s}", _LOGGER.error
) from err ) from err
@ -354,7 +354,7 @@ class DataDisk(CoreSysAttributes):
0, 0, LINUX_DATA_PARTITION_GUID, PARTITION_NAME_EXTERNAL_DATA_DISK 0, 0, LINUX_DATA_PARTITION_GUID, PARTITION_NAME_EXTERNAL_DATA_DISK
) )
except DBusError as err: except DBusError as err:
capture_exception(err) await async_capture_exception(err)
raise HassOSDataDiskError( raise HassOSDataDiskError(
f"Could not create new data partition: {err!s}", _LOGGER.error f"Could not create new data partition: {err!s}", _LOGGER.error
) from err ) from err

View File

@ -24,7 +24,7 @@ from ..exceptions import (
from ..jobs.const import JobCondition, JobExecutionLimit from ..jobs.const import JobCondition, JobExecutionLimit
from ..jobs.decorator import Job from ..jobs.decorator import Job
from ..resolution.const import UnhealthyReason from ..resolution.const import UnhealthyReason
from ..utils.sentry import capture_exception from ..utils.sentry import async_capture_exception
from .data_disk import DataDisk from .data_disk import DataDisk
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -385,7 +385,7 @@ class OSManager(CoreSysAttributes):
RaucState.ACTIVE, self.get_slot_name(boot_name) RaucState.ACTIVE, self.get_slot_name(boot_name)
) )
except DBusError as err: except DBusError as err:
capture_exception(err) await async_capture_exception(err)
raise HassOSSlotUpdateError( raise HassOSSlotUpdateError(
f"Can't mark {boot_name} as active!", _LOGGER.error f"Can't mark {boot_name} as active!", _LOGGER.error
) from err ) from err

View File

@ -27,7 +27,7 @@ from ..jobs.const import JobExecutionLimit
from ..jobs.decorator import Job from ..jobs.decorator import Job
from ..resolution.const import UnhealthyReason from ..resolution.const import UnhealthyReason
from ..utils.json import write_json_file 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 .base import PluginBase
from .const import ( from .const import (
FILE_HASSIO_AUDIO, FILE_HASSIO_AUDIO,
@ -163,7 +163,7 @@ class PluginAudio(PluginBase):
await self.instance.install(self.version) await self.instance.install(self.version)
except DockerError as err: except DockerError as err:
_LOGGER.error("Repair of Audio failed") _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: def pulse_client(self, input_profile=None, output_profile=None) -> str:
"""Generate an /etc/pulse/client.conf data.""" """Generate an /etc/pulse/client.conf data."""

View File

@ -15,7 +15,7 @@ from ..docker.interface import DockerInterface
from ..docker.monitor import DockerContainerStateEvent from ..docker.monitor import DockerContainerStateEvent
from ..exceptions import DockerError, PluginError from ..exceptions import DockerError, PluginError
from ..utils.common import FileConfiguration 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 from .const import WATCHDOG_MAX_ATTEMPTS, WATCHDOG_RETRY_SECONDS
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -129,7 +129,7 @@ class PluginBase(ABC, FileConfiguration, CoreSysAttributes):
except PluginError as err: except PluginError as err:
attempts = attempts + 1 attempts = attempts + 1
_LOGGER.error("Watchdog restart of %s plugin failed!", self.slug) _LOGGER.error("Watchdog restart of %s plugin failed!", self.slug)
capture_exception(err) await async_capture_exception(err)
else: else:
break break

View File

@ -17,7 +17,7 @@ from ..docker.stats import DockerStats
from ..exceptions import CliError, CliJobError, CliUpdateError, DockerError from ..exceptions import CliError, CliJobError, CliUpdateError, DockerError
from ..jobs.const import JobExecutionLimit from ..jobs.const import JobExecutionLimit
from ..jobs.decorator import Job from ..jobs.decorator import Job
from ..utils.sentry import capture_exception from ..utils.sentry import async_capture_exception
from .base import PluginBase from .base import PluginBase
from .const import ( from .const import (
FILE_HASSIO_CLI, FILE_HASSIO_CLI,
@ -114,7 +114,7 @@ class PluginCli(PluginBase):
await self.instance.install(self.version) await self.instance.install(self.version)
except DockerError as err: except DockerError as err:
_LOGGER.error("Repair of HA cli failed") _LOGGER.error("Repair of HA cli failed")
capture_exception(err) await async_capture_exception(err)
@Job( @Job(
name="plugin_cli_restart_after_problem", name="plugin_cli_restart_after_problem",

View File

@ -33,7 +33,7 @@ from ..jobs.const import JobExecutionLimit
from ..jobs.decorator import Job from ..jobs.decorator import Job
from ..resolution.const import ContextType, IssueType, SuggestionType, UnhealthyReason from ..resolution.const import ContextType, IssueType, SuggestionType, UnhealthyReason
from ..utils.json import write_json_file 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 ..validate import dns_url
from .base import PluginBase from .base import PluginBase
from .const import ( from .const import (
@ -410,7 +410,7 @@ class PluginDns(PluginBase):
await self.instance.install(self.version) await self.instance.install(self.version)
except DockerError as err: except DockerError as err:
_LOGGER.error("Repair of CoreDNS failed") _LOGGER.error("Repair of CoreDNS failed")
capture_exception(err) await async_capture_exception(err)
def _write_resolv(self, resolv_conf: Path) -> None: def _write_resolv(self, resolv_conf: Path) -> None:
"""Update/Write resolv.conf file.""" """Update/Write resolv.conf file."""

View File

@ -7,7 +7,7 @@ from typing import Self
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import HassioError from ..exceptions import HassioError
from ..resolution.const import ContextType, IssueType, SuggestionType 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 .audio import PluginAudio
from .base import PluginBase from .base import PluginBase
from .cli import PluginCli from .cli import PluginCli
@ -80,7 +80,7 @@ class PluginManager(CoreSysAttributes):
reference=plugin.slug, reference=plugin.slug,
suggestions=[SuggestionType.EXECUTE_REPAIR], suggestions=[SuggestionType.EXECUTE_REPAIR],
) )
capture_exception(err) await async_capture_exception(err)
# Exit if supervisor out of date. Plugins can't update until then # Exit if supervisor out of date. Plugins can't update until then
if self.sys_supervisor.need_update: if self.sys_supervisor.need_update:
@ -114,7 +114,7 @@ class PluginManager(CoreSysAttributes):
) )
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
_LOGGER.warning("Can't update plugin %s: %s", plugin.slug, err) _LOGGER.warning("Can't update plugin %s: %s", plugin.slug, err)
capture_exception(err) await async_capture_exception(err)
async def repair(self) -> None: async def repair(self) -> None:
"""Repair Supervisor plugins.""" """Repair Supervisor plugins."""
@ -132,4 +132,4 @@ class PluginManager(CoreSysAttributes):
await plugin.stop() await plugin.stop()
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
_LOGGER.warning("Can't stop plugin %s: %s", plugin.slug, err) _LOGGER.warning("Can't stop plugin %s: %s", plugin.slug, err)
capture_exception(err) await async_capture_exception(err)

View File

@ -19,7 +19,7 @@ from ..exceptions import (
) )
from ..jobs.const import JobExecutionLimit from ..jobs.const import JobExecutionLimit
from ..jobs.decorator import Job from ..jobs.decorator import Job
from ..utils.sentry import capture_exception from ..utils.sentry import async_capture_exception
from .base import PluginBase from .base import PluginBase
from .const import ( from .const import (
FILE_HASSIO_MULTICAST, FILE_HASSIO_MULTICAST,
@ -109,7 +109,7 @@ class PluginMulticast(PluginBase):
await self.instance.install(self.version) await self.instance.install(self.version)
except DockerError as err: except DockerError as err:
_LOGGER.error("Repair of Multicast failed") _LOGGER.error("Repair of Multicast failed")
capture_exception(err) await async_capture_exception(err)
@Job( @Job(
name="plugin_multicast_restart_after_problem", name="plugin_multicast_restart_after_problem",

View File

@ -22,7 +22,7 @@ from ..exceptions import (
) )
from ..jobs.const import JobExecutionLimit from ..jobs.const import JobExecutionLimit
from ..jobs.decorator import Job from ..jobs.decorator import Job
from ..utils.sentry import capture_exception from ..utils.sentry import async_capture_exception
from .base import PluginBase from .base import PluginBase
from .const import ( from .const import (
FILE_HASSIO_OBSERVER, FILE_HASSIO_OBSERVER,
@ -121,7 +121,7 @@ class PluginObserver(PluginBase):
await self.instance.install(self.version) await self.instance.install(self.version)
except DockerError as err: except DockerError as err:
_LOGGER.error("Repair of HA observer failed") _LOGGER.error("Repair of HA observer failed")
capture_exception(err) await async_capture_exception(err)
@Job( @Job(
name="plugin_observer_restart_after_problem", name="plugin_observer_restart_after_problem",

View File

@ -7,7 +7,7 @@ from typing import Any
from ..const import ATTR_CHECKS from ..const import ATTR_CHECKS
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import ResolutionNotFound from ..exceptions import ResolutionNotFound
from ..utils.sentry import capture_exception from ..utils.sentry import async_capture_exception
from .checks.base import CheckBase from .checks.base import CheckBase
from .validate import get_valid_modules from .validate import get_valid_modules
@ -64,6 +64,6 @@ class ResolutionCheck(CoreSysAttributes):
await check() await check()
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
_LOGGER.error("Error during processing %s: %s", check.issue, err) _LOGGER.error("Error during processing %s: %s", check.issue, err)
capture_exception(err) await async_capture_exception(err)
_LOGGER.info("System checks complete") _LOGGER.info("System checks complete")

View File

@ -10,7 +10,7 @@ from ...const import CoreState
from ...coresys import CoreSys from ...coresys import CoreSys
from ...jobs.const import JobCondition, JobExecutionLimit from ...jobs.const import JobCondition, JobExecutionLimit
from ...jobs.decorator import Job 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 ..const import DNS_CHECK_HOST, ContextType, IssueType
from .base import CheckBase from .base import CheckBase
@ -42,7 +42,7 @@ class CheckDNSServer(CheckBase):
ContextType.DNS_SERVER, ContextType.DNS_SERVER,
reference=dns_servers[i], reference=dns_servers[i],
) )
capture_exception(results[i]) await async_capture_exception(results[i])
@Job(name="check_dns_server_approve", conditions=[JobCondition.INTERNET_SYSTEM]) @Job(name="check_dns_server_approve", conditions=[JobCondition.INTERNET_SYSTEM])
async def approve_check(self, reference: str | None = None) -> bool: async def approve_check(self, reference: str | None = None) -> bool:

View File

@ -10,7 +10,7 @@ from ...const import CoreState
from ...coresys import CoreSys from ...coresys import CoreSys
from ...jobs.const import JobCondition, JobExecutionLimit from ...jobs.const import JobCondition, JobExecutionLimit
from ...jobs.decorator import Job 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 ..const import DNS_CHECK_HOST, DNS_ERROR_NO_DATA, ContextType, IssueType
from .base import CheckBase from .base import CheckBase
@ -47,7 +47,7 @@ class CheckDNSServerIPv6(CheckBase):
ContextType.DNS_SERVER, ContextType.DNS_SERVER,
reference=dns_servers[i], reference=dns_servers[i],
) )
capture_exception(results[i]) await async_capture_exception(results[i])
@Job( @Job(
name="check_dns_server_ipv6_approve", conditions=[JobCondition.INTERNET_SYSTEM] name="check_dns_server_ipv6_approve", conditions=[JobCondition.INTERNET_SYSTEM]

View File

@ -23,7 +23,7 @@ class CheckFreeSpace(CheckBase):
async def run_check(self) -> None: async def run_check(self) -> None:
"""Run check if not affected by issue.""" """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 return
suggestions: list[SuggestionType] = [] suggestions: list[SuggestionType] = []
@ -45,7 +45,7 @@ class CheckFreeSpace(CheckBase):
async def approve_check(self, reference: str | None = None) -> bool: async def approve_check(self, reference: str | None = None) -> bool:
"""Approve check if it is affected by issue.""" """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 False
return True return True

View File

@ -5,7 +5,7 @@ import logging
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import ResolutionNotFound from ..exceptions import ResolutionNotFound
from ..utils.sentry import capture_exception from ..utils.sentry import async_capture_exception
from .const import UnhealthyReason, UnsupportedReason from .const import UnhealthyReason, UnsupportedReason
from .evaluations.base import EvaluateBase from .evaluations.base import EvaluateBase
from .validate import get_valid_modules from .validate import get_valid_modules
@ -64,7 +64,7 @@ class ResolutionEvaluation(CoreSysAttributes):
_LOGGER.warning( _LOGGER.warning(
"Error during processing %s: %s", evaluation.reason, err "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): if any(reason in self.sys_resolution.unsupported for reason in UNHEALTHY):
self.sys_resolution.unhealthy = UnhealthyReason.DOCKER self.sys_resolution.unhealthy = UnhealthyReason.DOCKER

View File

@ -6,7 +6,7 @@ import logging
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..jobs.const import JobCondition from ..jobs.const import JobCondition
from ..jobs.decorator import Job from ..jobs.decorator import Job
from ..utils.sentry import capture_exception from ..utils.sentry import async_capture_exception
from .data import Issue, Suggestion from .data import Issue, Suggestion
from .fixups.base import FixupBase from .fixups.base import FixupBase
from .validate import get_valid_modules from .validate import get_valid_modules
@ -55,7 +55,7 @@ class ResolutionFixup(CoreSysAttributes):
await fix() await fix()
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
_LOGGER.warning("Error during processing %s: %s", fix.suggestion, err) _LOGGER.warning("Error during processing %s: %s", fix.suggestion, err)
capture_exception(err) await async_capture_exception(err)
_LOGGER.info("System autofix complete") _LOGGER.info("System autofix complete")

View File

@ -36,7 +36,7 @@ class ResolutionNotify(CoreSysAttributes):
messages.append( messages.append(
{ {
"title": "Available space is less than 1GB!", "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", "notification_id": "supervisor_issue_free_space",
} }
) )

View File

@ -36,7 +36,7 @@ from .jobs.const import JobCondition, JobExecutionLimit
from .jobs.decorator import Job from .jobs.decorator import Job
from .resolution.const import ContextType, IssueType, UnhealthyReason from .resolution.const import ContextType, IssueType, UnhealthyReason
from .utils.codenotary import calc_checksum 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__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -219,7 +219,7 @@ class Supervisor(CoreSysAttributes):
self.sys_resolution.create_issue( self.sys_resolution.create_issue(
IssueType.UPDATE_FAILED, ContextType.SUPERVISOR IssueType.UPDATE_FAILED, ContextType.SUPERVISOR
) )
capture_exception(err) await async_capture_exception(err)
raise SupervisorUpdateError( raise SupervisorUpdateError(
f"Update of Supervisor failed: {err!s}", _LOGGER.critical f"Update of Supervisor failed: {err!s}", _LOGGER.critical
) from err ) from err

View File

@ -35,7 +35,7 @@ from ..exceptions import (
DBusTimeoutError, DBusTimeoutError,
HassioNotSupportedError, HassioNotSupportedError,
) )
from .sentry import capture_exception from .sentry import async_capture_exception
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -124,7 +124,7 @@ class DBus:
) )
raise DBus.from_dbus_error(err) from None raise DBus.from_dbus_error(err) from None
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
capture_exception(err) await async_capture_exception(err)
raise DBusFatalError(str(err)) from err raise DBusFatalError(str(err)) from err
def _add_interfaces(self): def _add_interfaces(self):

View File

@ -3,8 +3,6 @@
import logging import logging
import re import re
from .sentry import capture_exception
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
RE_BIND_FAILED = re.compile( RE_BIND_FAILED = re.compile(
@ -13,13 +11,11 @@ RE_BIND_FAILED = re.compile(
def format_message(message: str) -> str: def format_message(message: str) -> str:
"""Return a formated message if it's known.""" """Return a formatted message if it's known."""
try:
match = RE_BIND_FAILED.match(message) match = RE_BIND_FAILED.match(message)
if match: if match:
return f"Port '{match.group(1)}' is already in use by something else on the host." return (
except TypeError as err: f"Port '{match.group(1)}' is already in use by something else on the host."
_LOGGER.error("The type of message is not a string - %s", err) )
capture_exception(err)
return message return message

View File

@ -1,5 +1,6 @@
"""Utilities for sentry.""" """Utilities for sentry."""
import asyncio
from functools import partial from functools import partial
import logging import logging
from typing import Any 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): 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 sentry_sdk.is_initialized():
if only_once and only_once not in only_once_events: if only_once and only_once not in only_once_events:
only_once_events.add(only_once) only_once_events.add(only_once)
sentry_sdk.capture_event(event) 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: 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(): if sentry_sdk.is_initialized():
sentry_sdk.capture_exception(err) 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: def close_sentry() -> None:
"""Close the current sentry client. """Close the current sentry client.

View File

@ -216,7 +216,7 @@ async def test_api_supervisor_fallback_log_capture(
"No systemd-journal-gatewayd Unix socket available!" "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") await api_client.get("/supervisor/logs")
capture_exception.assert_not_called() 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!") 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") await api_client.get("/supervisor/logs")
capture_exception.assert_called_once() capture_exception.assert_called_once()

View File

@ -141,7 +141,7 @@ async def test_home_assistant_watchdog_rebuild_on_failure(coresys: CoreSys) -> N
time=1, time=1,
), ),
) )
await asyncio.sleep(0) await asyncio.sleep(0.1)
start.assert_called_once() start.assert_called_once()
rebuild.assert_called_once() rebuild.assert_called_once()

View File

@ -5,10 +5,10 @@ from unittest.mock import patch
from supervisor.host.info import InfoCenter from supervisor.host.info import InfoCenter
def test_host_free_space(coresys): async def test_host_free_space(coresys):
"""Test host free space.""" """Test host free space."""
info = InfoCenter(coresys) info = InfoCenter(coresys)
with patch("shutil.disk_usage", return_value=(42, 42, 2 * (1024.0**3))): 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 assert free == 2.0

View File

@ -19,9 +19,3 @@ def test_format_message_port_alternative():
format_message(message) format_message(message)
== "Port '80' is already in use by something else on the host." == "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