File open calls to executor (#5678)

This commit is contained in:
Mike Degatano 2025-02-28 03:56:59 -05:00 committed by GitHub
parent dfed251c7a
commit 2274de969f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 136 additions and 76 deletions

View File

@ -2,6 +2,7 @@
import asyncio import asyncio
from collections.abc import Awaitable from collections.abc import Awaitable
from pathlib import Path
from typing import Any from typing import Any
from aiohttp import web from aiohttp import web
@ -68,6 +69,15 @@ SCHEMA_ADD_REPOSITORY = vol.Schema(
) )
def _read_static_file(path: Path) -> Any:
"""Read in a static file asset for API output.
Must be run in executor.
"""
with path.open("r") as asset:
return asset.read()
class APIStore(CoreSysAttributes): class APIStore(CoreSysAttributes):
"""Handle RESTful API for store functions.""" """Handle RESTful API for store functions."""
@ -233,8 +243,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}!")
with addon.path_icon.open("rb") as png: return await self.sys_run_in_executor(_read_static_file, addon.path_icon)
return png.read()
@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:
@ -243,8 +252,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}!")
with addon.path_logo.open("rb") as png: return await self.sys_run_in_executor(_read_static_file, addon.path_logo)
return png.read()
@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:
@ -258,8 +266,7 @@ class APIStore(CoreSysAttributes):
if not addon.with_changelog: if not addon.with_changelog:
return f"No changelog found for add-on {addon.slug}!" return f"No changelog found for add-on {addon.slug}!"
with addon.path_changelog.open("r") as changelog: return await self.sys_run_in_executor(_read_static_file, addon.path_changelog)
return changelog.read()
@api_process_raw(CONTENT_TYPE_TEXT) @api_process_raw(CONTENT_TYPE_TEXT)
async def addons_addon_documentation(self, request: web.Request) -> str: async def addons_addon_documentation(self, request: web.Request) -> str:
@ -273,8 +280,9 @@ class APIStore(CoreSysAttributes):
if not addon.with_documentation: if not addon.with_documentation:
return f"No documentation found for add-on {addon.slug}!" return f"No documentation found for add-on {addon.slug}!"
with addon.path_documentation.open("r") as documentation: return await self.sys_run_in_executor(
return documentation.read() _read_static_file, addon.path_documentation
)
@api_process @api_process
async def repositories_list(self, request: web.Request) -> list[dict[str, Any]]: async def repositories_list(self, request: web.Request) -> list[dict[str, Any]]:

View File

@ -71,7 +71,9 @@ class AppArmorControl(CoreSysAttributes):
async def load_profile(self, profile_name: str, profile_file: Path) -> None: async def load_profile(self, profile_name: str, profile_file: Path) -> None:
"""Load/Update a new/exists profile into AppArmor.""" """Load/Update a new/exists profile into AppArmor."""
if not validate_profile(profile_name, profile_file): if not await self.sys_run_in_executor(
validate_profile, profile_name, profile_file
):
raise HostAppArmorError( raise HostAppArmorError(
f"AppArmor profile '{profile_name}' is not valid", _LOGGER.error f"AppArmor profile '{profile_name}' is not valid", _LOGGER.error
) )

View File

@ -278,21 +278,25 @@ class Mount(CoreSysAttributes, ABC):
"""Mount using systemd.""" """Mount using systemd."""
# If supervisor can see where it will mount, ensure there's an empty folder there # If supervisor can see where it will mount, ensure there's an empty folder there
if self.local_where: if self.local_where:
if not self.local_where.exists():
_LOGGER.info( def ensure_empty_folder() -> None:
"Creating folder for mount: %s", self.local_where.as_posix() if not self.local_where.exists():
) _LOGGER.info(
self.local_where.mkdir(parents=True) "Creating folder for mount: %s", self.local_where.as_posix()
elif not self.local_where.is_dir(): )
raise MountInvalidError( self.local_where.mkdir(parents=True)
f"Cannot mount {self.name} at {self.local_where.as_posix()} as it is not a directory", elif not self.local_where.is_dir():
_LOGGER.error, raise MountInvalidError(
) f"Cannot mount {self.name} at {self.local_where.as_posix()} as it is not a directory",
elif any(self.local_where.iterdir()): _LOGGER.error,
raise MountInvalidError( )
f"Cannot mount {self.name} at {self.local_where.as_posix()} because it is not empty", elif any(self.local_where.iterdir()):
_LOGGER.error, raise MountInvalidError(
) f"Cannot mount {self.name} at {self.local_where.as_posix()} because it is not empty",
_LOGGER.error,
)
await self.sys_run_in_executor(ensure_empty_folder)
try: try:
options = ( options = (
@ -488,17 +492,23 @@ class CIFSMount(NetworkMount):
async def mount(self) -> None: async def mount(self) -> None:
"""Mount using systemd.""" """Mount using systemd."""
if self.username and self.password: if self.username and self.password:
if not self.path_credentials.exists():
self.path_credentials.touch(mode=0o600)
with self.path_credentials.open(mode="w") as cred_file: def write_credentials() -> None:
cred_file.write(f"username={self.username}\npassword={self.password}") if not self.path_credentials.exists():
self.path_credentials.touch(mode=0o600)
with self.path_credentials.open(mode="w") as cred_file:
cred_file.write(
f"username={self.username}\npassword={self.password}"
)
await self.sys_run_in_executor(write_credentials)
await super().mount() await super().mount()
async def unmount(self) -> None: async def unmount(self) -> None:
"""Unmount using systemd.""" """Unmount using systemd."""
self.path_credentials.unlink(missing_ok=True) await self.sys_run_in_executor(self.path_credentials.unlink, missing_ok=True)
await super().unmount() await super().unmount()

View File

@ -217,12 +217,15 @@ class OSManager(CoreSysAttributes):
) )
# Download RAUCB file # Download RAUCB file
with raucb.open("wb") as ota_file: ota_file = await self.sys_run_in_executor(raucb.open, "wb")
try:
while True: while True:
chunk = await request.content.read(1_048_576) chunk = await request.content.read(1_048_576)
if not chunk: if not chunk:
break break
ota_file.write(chunk) await self.sys_run_in_executor(ota_file.write, chunk)
finally:
await self.sys_run_in_executor(ota_file.close)
_LOGGER.info("Completed download of OTA update file %s", raucb) _LOGGER.info("Completed download of OTA update file %s", raucb)

View File

@ -74,7 +74,10 @@ def _read_addon_translations(addon_path: Path) -> dict:
def _read_git_repository(path: Path) -> ProcessedRepository | None: def _read_git_repository(path: Path) -> ProcessedRepository | None:
"""Process a custom repository folder.""" """Process a custom repository folder.
Must be run in executor.
"""
slug = extract_hash_from_path(path) slug = extract_hash_from_path(path)
# exists repository json # exists repository json

View File

@ -74,7 +74,10 @@ class Repository(CoreSysAttributes):
return self.data.get(ATTR_MAINTAINER, UNKNOWN) return self.data.get(ATTR_MAINTAINER, UNKNOWN)
def validate(self) -> bool: def validate(self) -> bool:
"""Check if store is valid.""" """Check if store is valid.
Must be run in executor.
"""
if self.type != StoreType.GIT: if self.type != StoreType.GIT:
return True return True
@ -104,7 +107,7 @@ class Repository(CoreSysAttributes):
async def update(self) -> bool: async def update(self) -> bool:
"""Update add-on repository.""" """Update add-on repository."""
if not self.validate(): if not await self.sys_run_in_executor(self.validate):
return False return False
return self.type == StoreType.LOCAL or await self.git.pull() return self.type == StoreType.LOCAL or await self.git.pull()

View File

@ -12,7 +12,10 @@ RE_PROFILE = re.compile(r"^profile ([^ ]+).*$")
def get_profile_name(profile_file: Path) -> str: def get_profile_name(profile_file: Path) -> str:
"""Read the profile name from file.""" """Read the profile name from file.
Must be run in executor.
"""
profiles = set() profiles = set()
try: try:
@ -42,14 +45,20 @@ def get_profile_name(profile_file: Path) -> str:
def validate_profile(profile_name: str, profile_file: Path) -> bool: def validate_profile(profile_name: str, profile_file: Path) -> bool:
"""Check if profile from file is valid with profile name.""" """Check if profile from file is valid with profile name.
Must be run in executor.
"""
if profile_name == get_profile_name(profile_file): if profile_name == get_profile_name(profile_file):
return True return True
return False return False
def adjust_profile(profile_name: str, profile_file: Path, profile_new: Path) -> None: def adjust_profile(profile_name: str, profile_file: Path, profile_new: Path) -> None:
"""Fix the profile name.""" """Fix the profile name.
Must be run in executor.
"""
org_profile = get_profile_name(profile_file) org_profile = get_profile_name(profile_file)
profile_data = [] profile_data = []

View File

@ -19,7 +19,10 @@ _DEFAULT: dict[str, Any] = {}
def find_one_filetype(path: Path, filename: str, filetypes: list[str]) -> Path: def find_one_filetype(path: Path, filename: str, filetypes: list[str]) -> Path:
"""Find first file matching filetypes.""" """Find first file matching filetypes.
Must be run in executor.
"""
for file in path.glob(f"**/{filename}.*"): for file in path.glob(f"**/{filename}.*"):
if file.suffix in filetypes: if file.suffix in filetypes:
return file return file
@ -27,7 +30,10 @@ def find_one_filetype(path: Path, filename: str, filetypes: list[str]) -> Path:
def read_json_or_yaml_file(path: Path) -> dict: def read_json_or_yaml_file(path: Path) -> dict:
"""Read JSON or YAML file.""" """Read JSON or YAML file.
Must be run in executor.
"""
if path.suffix == ".json": if path.suffix == ".json":
return read_json_file(path) return read_json_file(path)
@ -38,7 +44,10 @@ def read_json_or_yaml_file(path: Path) -> dict:
def write_json_or_yaml_file(path: Path, data: dict) -> None: def write_json_or_yaml_file(path: Path, data: dict) -> None:
"""Write JSON or YAML file.""" """Write JSON or YAML file.
Must be run in executor.
"""
if path.suffix == ".json": if path.suffix == ".json":
return write_json_file(path, data) return write_json_file(path, data)

View File

@ -17,7 +17,10 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
def read_yaml_file(path: Path) -> dict: def read_yaml_file(path: Path) -> dict:
"""Read YAML file from path.""" """Read YAML file from path.
Must be run in executor.
"""
try: try:
with open(path, encoding="utf-8") as yaml_file: with open(path, encoding="utf-8") as yaml_file:
return load(yaml_file, Loader=SafeLoader) or {} return load(yaml_file, Loader=SafeLoader) or {}
@ -29,7 +32,10 @@ def read_yaml_file(path: Path) -> dict:
def write_yaml_file(path: Path, data: dict) -> None: def write_yaml_file(path: Path, data: dict) -> None:
"""Write a YAML file.""" """Write a YAML file.
Must be run in executor.
"""
try: try:
with atomic_write(path, overwrite=True) as fp: with atomic_write(path, overwrite=True) as fp:
dump(data, fp, Dumper=Dumper) dump(data, fp, Dumper=Dumper)

View File

@ -3,6 +3,7 @@
import json import json
import os import os
from pathlib import Path from pathlib import Path
from unittest.util import unorderable_list_difference
from dbus_fast import DBusError, ErrorType, Variant from dbus_fast import DBusError, ErrorType, Variant
from dbus_fast.aio.message_bus import MessageBus from dbus_fast.aio.message_bus import MessageBus
@ -111,40 +112,46 @@ async def test_load(
assert media_test.local_where.is_dir() assert media_test.local_where.is_dir()
assert (coresys.config.path_media / "media_test").is_dir() assert (coresys.config.path_media / "media_test").is_dir()
assert systemd_service.StartTransientUnit.calls == [ assert unorderable_list_difference(
( systemd_service.StartTransientUnit.calls,
"mnt-data-supervisor-mounts-backup_test.mount", [
"fail", (
[ "mnt-data-supervisor-mounts-backup_test.mount",
["Options", Variant("s", "noserverino,guest")], "fail",
["Type", Variant("s", "cifs")], [
["Description", Variant("s", "Supervisor cifs mount: backup_test")], ["Options", Variant("s", "noserverino,guest")],
["What", Variant("s", "//backup.local/backups")], ["Type", Variant("s", "cifs")],
], ["Description", Variant("s", "Supervisor cifs mount: backup_test")],
[], ["What", Variant("s", "//backup.local/backups")],
), ],
( [],
"mnt-data-supervisor-mounts-media_test.mount", ),
"fail", (
[ "mnt-data-supervisor-mounts-media_test.mount",
["Options", Variant("s", "soft,timeo=200")], "fail",
["Type", Variant("s", "nfs")], [
["Description", Variant("s", "Supervisor nfs mount: media_test")], ["Options", Variant("s", "soft,timeo=200")],
["What", Variant("s", "media.local:/media")], ["Type", Variant("s", "nfs")],
], ["Description", Variant("s", "Supervisor nfs mount: media_test")],
[], ["What", Variant("s", "media.local:/media")],
), ],
( [],
"mnt-data-supervisor-media-media_test.mount", ),
"fail", (
[ "mnt-data-supervisor-media-media_test.mount",
["Options", Variant("s", "bind")], "fail",
["Description", Variant("s", "Supervisor bind mount: bind_media_test")], [
["What", Variant("s", "/mnt/data/supervisor/mounts/media_test")], ["Options", Variant("s", "bind")],
], [
[], "Description",
), Variant("s", "Supervisor bind mount: bind_media_test"),
] ],
["What", Variant("s", "/mnt/data/supervisor/mounts/media_test")],
],
[],
),
],
) == ([], [])
async def test_load_share_mount( async def test_load_share_mount(