mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-20 23:56:30 +00:00
File open calls to executor (#5678)
This commit is contained in:
parent
dfed251c7a
commit
2274de969f
@ -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]]:
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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 = []
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user