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
from collections.abc import Awaitable
from pathlib import Path
from typing import Any
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):
"""Handle RESTful API for store functions."""
@ -233,8 +243,7 @@ class APIStore(CoreSysAttributes):
if not addon.with_icon:
raise APIError(f"No icon found for add-on {addon.slug}!")
with addon.path_icon.open("rb") as png:
return png.read()
return await self.sys_run_in_executor(_read_static_file, addon.path_icon)
@api_process_raw(CONTENT_TYPE_PNG)
async def addons_addon_logo(self, request: web.Request) -> bytes:
@ -243,8 +252,7 @@ class APIStore(CoreSysAttributes):
if not addon.with_logo:
raise APIError(f"No logo found for add-on {addon.slug}!")
with addon.path_logo.open("rb") as png:
return png.read()
return await self.sys_run_in_executor(_read_static_file, addon.path_logo)
@api_process_raw(CONTENT_TYPE_TEXT)
async def addons_addon_changelog(self, request: web.Request) -> str:
@ -258,8 +266,7 @@ class APIStore(CoreSysAttributes):
if not addon.with_changelog:
return f"No changelog found for add-on {addon.slug}!"
with addon.path_changelog.open("r") as changelog:
return changelog.read()
return await self.sys_run_in_executor(_read_static_file, addon.path_changelog)
@api_process_raw(CONTENT_TYPE_TEXT)
async def addons_addon_documentation(self, request: web.Request) -> str:
@ -273,8 +280,9 @@ class APIStore(CoreSysAttributes):
if not addon.with_documentation:
return f"No documentation found for add-on {addon.slug}!"
with addon.path_documentation.open("r") as documentation:
return documentation.read()
return await self.sys_run_in_executor(
_read_static_file, addon.path_documentation
)
@api_process
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:
"""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(
f"AppArmor profile '{profile_name}' is not valid", _LOGGER.error
)

View File

@ -278,6 +278,8 @@ class Mount(CoreSysAttributes, ABC):
"""Mount using systemd."""
# If supervisor can see where it will mount, ensure there's an empty folder there
if self.local_where:
def ensure_empty_folder() -> None:
if not self.local_where.exists():
_LOGGER.info(
"Creating folder for mount: %s", self.local_where.as_posix()
@ -294,6 +296,8 @@ class Mount(CoreSysAttributes, ABC):
_LOGGER.error,
)
await self.sys_run_in_executor(ensure_empty_folder)
try:
options = (
[(DBUS_ATTR_OPTIONS, Variant("s", ",".join(self.options)))]
@ -488,17 +492,23 @@ class CIFSMount(NetworkMount):
async def mount(self) -> None:
"""Mount using systemd."""
if self.username and self.password:
def write_credentials() -> None:
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}")
cred_file.write(
f"username={self.username}\npassword={self.password}"
)
await self.sys_run_in_executor(write_credentials)
await super().mount()
async def unmount(self) -> None:
"""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()

View File

@ -217,12 +217,15 @@ class OSManager(CoreSysAttributes):
)
# Download RAUCB file
with raucb.open("wb") as ota_file:
ota_file = await self.sys_run_in_executor(raucb.open, "wb")
try:
while True:
chunk = await request.content.read(1_048_576)
if not chunk:
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)

View File

@ -74,7 +74,10 @@ def _read_addon_translations(addon_path: Path) -> dict:
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)
# exists repository json

View File

@ -74,7 +74,10 @@ class Repository(CoreSysAttributes):
return self.data.get(ATTR_MAINTAINER, UNKNOWN)
def validate(self) -> bool:
"""Check if store is valid."""
"""Check if store is valid.
Must be run in executor.
"""
if self.type != StoreType.GIT:
return True
@ -104,7 +107,7 @@ class Repository(CoreSysAttributes):
async def update(self) -> bool:
"""Update add-on repository."""
if not self.validate():
if not await self.sys_run_in_executor(self.validate):
return False
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:
"""Read the profile name from file."""
"""Read the profile name from file.
Must be run in executor.
"""
profiles = set()
try:
@ -42,14 +45,20 @@ def get_profile_name(profile_file: Path) -> str:
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):
return True
return False
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)
profile_data = []

View File

@ -19,7 +19,10 @@ _DEFAULT: dict[str, Any] = {}
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}.*"):
if file.suffix in filetypes:
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:
"""Read JSON or YAML file."""
"""Read JSON or YAML file.
Must be run in executor.
"""
if path.suffix == ".json":
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:
"""Write JSON or YAML file."""
"""Write JSON or YAML file.
Must be run in executor.
"""
if path.suffix == ".json":
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:
"""Read YAML file from path."""
"""Read YAML file from path.
Must be run in executor.
"""
try:
with open(path, encoding="utf-8") as yaml_file:
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:
"""Write a YAML file."""
"""Write a YAML file.
Must be run in executor.
"""
try:
with atomic_write(path, overwrite=True) as fp:
dump(data, fp, Dumper=Dumper)

View File

@ -3,6 +3,7 @@
import json
import os
from pathlib import Path
from unittest.util import unorderable_list_difference
from dbus_fast import DBusError, ErrorType, Variant
from dbus_fast.aio.message_bus import MessageBus
@ -111,7 +112,9 @@ async def test_load(
assert media_test.local_where.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",
@ -139,12 +142,16 @@ async def test_load(
"fail",
[
["Options", Variant("s", "bind")],
["Description", Variant("s", "Supervisor bind mount: bind_media_test")],
[
"Description",
Variant("s", "Supervisor bind mount: bind_media_test"),
],
["What", Variant("s", "/mnt/data/supervisor/mounts/media_test")],
],
[],
),
]
],
) == ([], [])
async def test_load_share_mount(