Skip to content

Commit

Permalink
File open calls to executor (#5678)
Browse files Browse the repository at this point in the history
  • Loading branch information
mdegat01 authored Feb 28, 2025
1 parent dfed251 commit 2274de9
Show file tree
Hide file tree
Showing 10 changed files with 136 additions and 76 deletions.
24 changes: 16 additions & 8 deletions supervisor/api/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import asyncio
from collections.abc import Awaitable
from pathlib import Path
from typing import Any

from aiohttp import web
Expand Down Expand Up @@ -68,6 +69,15 @@
)


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."""

Expand Down Expand Up @@ -233,8 +243,7 @@ async def addons_addon_icon(self, request: web.Request) -> bytes:
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:
Expand All @@ -243,8 +252,7 @@ async def addons_addon_logo(self, request: web.Request) -> bytes:
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:
Expand All @@ -258,8 +266,7 @@ async def addons_addon_changelog(self, request: web.Request) -> str:
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:
Expand All @@ -273,8 +280,9 @@ async def addons_addon_documentation(self, request: web.Request) -> str:
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]]:
Expand Down
4 changes: 3 additions & 1 deletion supervisor/host/apparmor.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ async def load(self) -> None:

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
)
Expand Down
50 changes: 30 additions & 20 deletions supervisor/mounts/mount.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,21 +278,25 @@ async def mount(self) -> None:
"""Mount using systemd."""
# If supervisor can see where it will mount, ensure there's an empty folder there
if self.local_where:
if not self.local_where.exists():
_LOGGER.info(
"Creating folder for mount: %s", self.local_where.as_posix()
)
self.local_where.mkdir(parents=True)
elif not self.local_where.is_dir():
raise MountInvalidError(
f"Cannot mount {self.name} at {self.local_where.as_posix()} as it is not a directory",
_LOGGER.error,
)
elif any(self.local_where.iterdir()):
raise MountInvalidError(
f"Cannot mount {self.name} at {self.local_where.as_posix()} because it is not empty",
_LOGGER.error,
)

def ensure_empty_folder() -> None:
if not self.local_where.exists():
_LOGGER.info(
"Creating folder for mount: %s", self.local_where.as_posix()
)
self.local_where.mkdir(parents=True)
elif not self.local_where.is_dir():
raise MountInvalidError(
f"Cannot mount {self.name} at {self.local_where.as_posix()} as it is not a directory",
_LOGGER.error,
)
elif any(self.local_where.iterdir()):
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:
options = (
Expand Down Expand Up @@ -488,17 +492,23 @@ def path_extern_credentials(self) -> PurePath:
async def mount(self) -> None:
"""Mount using systemd."""
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:
cred_file.write(f"username={self.username}\npassword={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}"
)

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()


Expand Down
7 changes: 5 additions & 2 deletions supervisor/os/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,12 +217,15 @@ async def _download_raucb(self, url: str, raucb: Path) -> None:
)

# 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)

Expand Down
5 changes: 4 additions & 1 deletion supervisor/store/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions supervisor/store/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ def maintainer(self) -> str:
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

Expand Down Expand Up @@ -104,7 +107,7 @@ async def load(self) -> None:

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()

Expand Down
15 changes: 12 additions & 3 deletions supervisor/utils/apparmor.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@


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:
Expand Down Expand Up @@ -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 = []

Expand Down
15 changes: 12 additions & 3 deletions supervisor/utils/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,21 @@


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
raise ConfigurationFileError(f"{path!s}/{filename}.({filetypes}) does not exist!")


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)

Expand All @@ -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)

Expand Down
10 changes: 8 additions & 2 deletions supervisor/utils/yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@


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 {}
Expand All @@ -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)
Expand Down
75 changes: 41 additions & 34 deletions tests/mounts/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -111,40 +112,46 @@ 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 == [
(
"mnt-data-supervisor-mounts-backup_test.mount",
"fail",
[
["Options", Variant("s", "noserverino,guest")],
["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",
[
["Options", Variant("s", "soft,timeo=200")],
["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",
[
["Options", Variant("s", "bind")],
["Description", Variant("s", "Supervisor bind mount: bind_media_test")],
["What", Variant("s", "/mnt/data/supervisor/mounts/media_test")],
],
[],
),
]
assert unorderable_list_difference(
systemd_service.StartTransientUnit.calls,
[
(
"mnt-data-supervisor-mounts-backup_test.mount",
"fail",
[
["Options", Variant("s", "noserverino,guest")],
["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",
[
["Options", Variant("s", "soft,timeo=200")],
["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",
[
["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(
Expand Down

0 comments on commit 2274de9

Please sign in to comment.