From 151d4bdd73a3ae4b3b81543b703316604f03b255 Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Thu, 27 Feb 2025 11:58:55 -0500 Subject: [PATCH] Temporary directory to executor (#5673) * Move temporary directory usage to executor * Use temp_folder.name in Path constructor --- supervisor/addons/addon.py | 14 +++++++-- supervisor/api/backups.py | 57 +++++++++++++++++++++++------------- supervisor/supervisor.py | 46 +++++++++++++++++------------ supervisor/utils/__init__.py | 6 +++- 4 files changed, 81 insertions(+), 42 deletions(-) diff --git a/supervisor/addons/addon.py b/supervisor/addons/addon.py index 3a28678d0..2d5dee5b9 100644 --- a/supervisor/addons/addon.py +++ b/supervisor/addons/addon.py @@ -977,11 +977,21 @@ class Addon(AddonModel): return # Need install/update - with TemporaryDirectory(dir=self.sys_config.path_tmp) as tmp_folder: - profile_file = Path(tmp_folder, "apparmor.txt") + tmp_folder: TemporaryDirectory | None = None + def install_update_profile() -> Path: + nonlocal tmp_folder + tmp_folder = TemporaryDirectory(dir=self.sys_config.path_tmp) + profile_file = Path(tmp_folder.name, "apparmor.txt") adjust_profile(self.slug, self.path_apparmor, profile_file) + return profile_file + + try: + profile_file = await self.sys_run_in_executor(install_update_profile) await self.sys_host.apparmor.load_profile(self.slug, profile_file) + finally: + if tmp_folder: + await self.sys_run_in_executor(tmp_folder.cleanup) async def uninstall_apparmor(self) -> None: """Remove AppArmor profile for Add-on.""" diff --git a/supervisor/api/backups.py b/supervisor/api/backups.py index 76c096ff6..715c262b4 100644 --- a/supervisor/api/backups.py +++ b/supervisor/api/backups.py @@ -5,6 +5,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable import errno +from io import IOBase import logging from pathlib import Path import re @@ -518,29 +519,28 @@ class APIBackups(CoreSysAttributes): except vol.Invalid as ex: raise APIError(humanize_error(filename, ex)) from None - with TemporaryDirectory(dir=tmp_path.as_posix()) as temp_dir: - tar_file = Path(temp_dir, "backup.tar") + temp_dir: TemporaryDirectory | None = None + backup_file_stream: IOBase | None = None + + def open_backup_file() -> Path: + nonlocal temp_dir, backup_file_stream + temp_dir = TemporaryDirectory(dir=tmp_path.as_posix()) + tar_file = Path(temp_dir.name, "backup.tar") + backup_file_stream = tar_file.open("wb") + return tar_file + + def close_backup_file() -> None: + if backup_file_stream: + backup_file_stream.close() + if temp_dir: + temp_dir.cleanup() + + try: reader = await request.multipart() contents = await reader.next() - try: - with tar_file.open("wb") as backup: - while True: - chunk = await contents.read_chunk() - if not chunk: - break - backup.write(chunk) - - except OSError as err: - if err.errno == errno.EBADMSG and location in { - LOCATION_CLOUD_BACKUP, - None, - }: - self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE - _LOGGER.error("Can't write new backup file: %s", err) - return False - - except asyncio.CancelledError: - return False + tar_file = await self.sys_run_in_executor(open_backup_file) + while chunk := await contents.read_chunk(size=2**16): + await self.sys_run_in_executor(backup_file_stream.write, chunk) backup = await asyncio.shield( self.sys_backups.import_backup( @@ -550,6 +550,21 @@ class APIBackups(CoreSysAttributes): additional_locations=locations, ) ) + except OSError as err: + if err.errno == errno.EBADMSG and location in { + LOCATION_CLOUD_BACKUP, + None, + }: + self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE + _LOGGER.error("Can't write new backup file: %s", err) + return False + + except asyncio.CancelledError: + return False + + finally: + if temp_dir or backup: + await self.sys_run_in_executor(close_backup_file) if backup: return {ATTR_SLUG: backup.slug} diff --git a/supervisor/supervisor.py b/supervisor/supervisor.py index c5b855573..0b8a65274 100644 --- a/supervisor/supervisor.py +++ b/supervisor/supervisor.py @@ -158,25 +158,35 @@ class Supervisor(CoreSysAttributes): ) from err # Load - with TemporaryDirectory(dir=self.sys_config.path_tmp) as tmp_dir: - profile_file = Path(tmp_dir, "apparmor.txt") - try: - profile_file.write_text(data, encoding="utf-8") - except OSError as err: - if err.errno == errno.EBADMSG: - self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE - raise SupervisorAppArmorError( - f"Can't write temporary profile: {err!s}", _LOGGER.error - ) from err + temp_dir: TemporaryDirectory | None = None - try: - await self.sys_host.apparmor.load_profile( - "hassio-supervisor", profile_file - ) - except HostAppArmorError as err: - raise SupervisorAppArmorError( - "Can't update AppArmor profile!", _LOGGER.error - ) from err + def write_profile() -> Path: + nonlocal temp_dir + temp_dir = TemporaryDirectory(dir=self.sys_config.path_tmp) + profile_file = Path(temp_dir.name, "apparmor.txt") + profile_file.write_text(data, encoding="utf-8") + return profile_file + + try: + profile_file = await self.sys_run_in_executor(write_profile) + + await self.sys_host.apparmor.load_profile("hassio-supervisor", profile_file) + + except OSError as err: + if err.errno == errno.EBADMSG: + self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE + raise SupervisorAppArmorError( + f"Can't write temporary profile: {err!s}", _LOGGER.error + ) from err + + except HostAppArmorError as err: + raise SupervisorAppArmorError( + "Can't update AppArmor profile!", _LOGGER.error + ) from err + + finally: + if temp_dir: + await self.sys_run_in_executor(temp_dir.cleanup) async def update(self, version: AwesomeVersion | None = None) -> None: """Update Supervisor version.""" diff --git a/supervisor/utils/__init__.py b/supervisor/utils/__init__.py index e402850da..cce78850f 100644 --- a/supervisor/utils/__init__.py +++ b/supervisor/utils/__init__.py @@ -90,6 +90,7 @@ def remove_folder( Is needed to avoid issue with: - CAP_DAC_OVERRIDE - CAP_DAC_READ_SEARCH + Must be run in executor. """ find_args = [] if content_only: @@ -114,7 +115,10 @@ def remove_folder_with_excludes( excludes: list[str], tmp_dir: Path | None = None, ) -> None: - """Remove folder with excludes.""" + """Remove folder with excludes. + + Must be run in executor. + """ with TemporaryDirectory(dir=tmp_dir) as temp_path: temp_path = Path(temp_path) moved_files: list[Path] = []