Skip to content

Commit fa52bfd

Browse files
committed
Merge remote-tracking branch 'upstream/main'
2 parents 4a1b3a4 + e1c9c8b commit fa52bfd

File tree

122 files changed

+1011
-581
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

122 files changed

+1011
-581
lines changed

.github/workflows/builder.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ jobs:
106106
107107
- name: Build wheels
108108
if: needs.init.outputs.requirements == 'true'
109-
uses: home-assistant/wheels@2024.11.0
109+
uses: home-assistant/wheels@2025.02.0
110110
with:
111111
abi: cp313
112112
tag: musllinux_1_2

.github/workflows/sentry.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
- name: Check out code from GitHub
1313
uses: actions/[email protected]
1414
- name: Sentry Release
15-
uses: getsentry/action-release@v1.10.4
15+
uses: getsentry/action-release@v3.1.0
1616
env:
1717
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
1818
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}

requirements.txt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,19 @@ aiohttp==3.11.13
33
atomicwrites-homeassistant==1.4.1
44
attrs==25.1.0
55
awesomeversion==24.6.0
6+
blockbuster==1.5.23
67
brotli==1.1.0
78
ciso8601==2.3.2
89
colorlog==6.9.0
910
cpe==1.3.1
10-
cryptography==44.0.1
11-
debugpy==1.8.12
11+
cryptography==44.0.2
12+
debugpy==1.8.13
1213
deepmerge==2.0
1314
dirhash==0.5.0
1415
docker==7.1.0
1516
faust-cchardet==2.1.19
1617
gitpython==3.1.44
17-
jinja2==3.1.5
18+
jinja2==3.1.6
1819
orjson==3.10.12
1920
pulsectl==24.12.0
2021
pyudev==0.24.3
@@ -24,6 +25,6 @@ securetar==2025.2.1
2425
sentry-sdk==2.22.0
2526
setuptools==75.8.2
2627
voluptuous==0.15.2
27-
dbus-fast==2.34.0
28+
dbus-fast==2.37.0
2829
typing_extensions==4.12.2
2930
zlib-fast==0.2.1

requirements_tests.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ pytest-aiohttp==1.1.0
66
pytest-asyncio==0.25.2
77
pytest-cov==6.0.0
88
pytest-timeout==2.3.1
9-
pytest==8.3.4
10-
ruff==0.9.8
9+
pytest==8.3.5
10+
ruff==0.9.9
1111
time-machine==2.16.0
1212
typing_extensions==4.12.2
1313
urllib3==2.3.0

supervisor/addons/addon.py

Lines changed: 47 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,7 @@ def __init__(self, coresys: CoreSys, slug: str):
140140
super().__init__(coresys, slug)
141141
self.instance: DockerAddon = DockerAddon(coresys, self)
142142
self._state: AddonState = AddonState.UNKNOWN
143-
self._manual_stop: bool = (
144-
self.sys_hardware.helper.last_boot != self.sys_config.last_boot
145-
)
143+
self._manual_stop: bool = False
146144
self._listeners: list[EventListener] = []
147145
self._startup_event = asyncio.Event()
148146
self._startup_task: asyncio.Task | None = None
@@ -216,6 +214,10 @@ def in_progress(self) -> bool:
216214

217215
async def load(self) -> None:
218216
"""Async initialize of object."""
217+
self._manual_stop = (
218+
await self.sys_hardware.helper.last_boot() != self.sys_config.last_boot
219+
)
220+
219221
if self.is_detached:
220222
await super().refresh_path_cache()
221223

@@ -720,7 +722,7 @@ async def write_options(self) -> None:
720722

721723
try:
722724
options = self.schema.validate(self.options)
723-
write_json_file(self.path_options, options)
725+
await self.sys_run_in_executor(write_json_file, self.path_options, options)
724726
except vol.Invalid as ex:
725727
_LOGGER.error(
726728
"Add-on %s has invalid options: %s",
@@ -751,9 +753,12 @@ async def unload(self) -> None:
751753
for listener in self._listeners:
752754
self.sys_bus.remove_listener(listener)
753755

754-
if self.path_data.is_dir():
755-
_LOGGER.info("Removing add-on data folder %s", self.path_data)
756-
await remove_data(self.path_data)
756+
def remove_data_dir():
757+
if self.path_data.is_dir():
758+
_LOGGER.info("Removing add-on data folder %s", self.path_data)
759+
remove_data(self.path_data)
760+
761+
await self.sys_run_in_executor(remove_data_dir)
757762

758763
async def _check_ingress_port(self):
759764
"""Assign a ingress port if dynamic port selection is used."""
@@ -775,11 +780,14 @@ async def install(self) -> None:
775780
await self.sys_addons.data.install(self.addon_store)
776781
await self.load()
777782

778-
if not self.path_data.is_dir():
779-
_LOGGER.info(
780-
"Creating Home Assistant add-on data folder %s", self.path_data
781-
)
782-
self.path_data.mkdir()
783+
def setup_data():
784+
if not self.path_data.is_dir():
785+
_LOGGER.info(
786+
"Creating Home Assistant add-on data folder %s", self.path_data
787+
)
788+
self.path_data.mkdir()
789+
790+
await self.sys_run_in_executor(setup_data)
783791

784792
# Setup/Fix AppArmor profile
785793
await self.install_apparmor()
@@ -818,14 +826,17 @@ async def uninstall(
818826

819827
await self.unload()
820828

821-
# Remove config if present and requested
822-
if self.addon_config_used and remove_config:
823-
await remove_data(self.path_config)
829+
def cleanup_config_and_audio():
830+
# Remove config if present and requested
831+
if self.addon_config_used and remove_config:
832+
remove_data(self.path_config)
833+
834+
# Cleanup audio settings
835+
if self.path_pulse.exists():
836+
with suppress(OSError):
837+
self.path_pulse.unlink()
824838

825-
# Cleanup audio settings
826-
if self.path_pulse.exists():
827-
with suppress(OSError):
828-
self.path_pulse.unlink()
839+
await self.sys_run_in_executor(cleanup_config_and_audio)
829840

830841
# Cleanup AppArmor profile
831842
with suppress(HostAppArmorError):
@@ -938,19 +949,20 @@ async def rebuild(self) -> asyncio.Task | None:
938949
)
939950
return out
940951

941-
def write_pulse(self) -> None:
952+
async def write_pulse(self) -> None:
942953
"""Write asound config to file and return True on success."""
943954
pulse_config = self.sys_plugins.audio.pulse_client(
944955
input_profile=self.audio_input, output_profile=self.audio_output
945956
)
946957

947-
# Cleanup wrong maps
948-
if self.path_pulse.is_dir():
949-
shutil.rmtree(self.path_pulse, ignore_errors=True)
958+
def write_pulse_config():
959+
# Cleanup wrong maps
960+
if self.path_pulse.is_dir():
961+
shutil.rmtree(self.path_pulse, ignore_errors=True)
962+
self.path_pulse.write_text(pulse_config, encoding="utf-8")
950963

951-
# Write pulse config
952964
try:
953-
self.path_pulse.write_text(pulse_config, encoding="utf-8")
965+
await self.sys_run_in_executor(write_pulse_config)
954966
except OSError as err:
955967
if err.errno == errno.EBADMSG:
956968
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
@@ -965,7 +977,7 @@ def write_pulse(self) -> None:
965977
async def install_apparmor(self) -> None:
966978
"""Install or Update AppArmor profile for Add-on."""
967979
exists_local = self.sys_host.apparmor.exists(self.slug)
968-
exists_addon = self.path_apparmor.exists()
980+
exists_addon = await self.sys_run_in_executor(self.path_apparmor.exists)
969981

970982
# Nothing to do
971983
if not exists_local and not exists_addon:
@@ -1070,7 +1082,7 @@ async def start(self) -> asyncio.Task:
10701082

10711083
# Sound
10721084
if self.with_audio:
1073-
self.write_pulse()
1085+
await self.write_pulse()
10741086

10751087
def _check_addon_config_dir():
10761088
if self.path_config.is_dir():
@@ -1441,6 +1453,12 @@ def _extract_tarfile() -> tuple[TemporaryDirectory, dict[str, Any]]:
14411453
# Restore data and config
14421454
def _restore_data():
14431455
"""Restore data and config."""
1456+
_LOGGER.info("Restoring data and config for addon %s", self.slug)
1457+
if self.path_data.is_dir():
1458+
remove_data(self.path_data)
1459+
if self.path_config.is_dir():
1460+
remove_data(self.path_config)
1461+
14441462
temp_data = Path(tmp.name, "data")
14451463
if temp_data.is_dir():
14461464
shutil.copytree(temp_data, self.path_data, symlinks=True)
@@ -1453,12 +1471,6 @@ def _restore_data():
14531471
elif self.addon_config_used:
14541472
self.path_config.mkdir()
14551473

1456-
_LOGGER.info("Restoring data and config for addon %s", self.slug)
1457-
if self.path_data.is_dir():
1458-
await remove_data(self.path_data)
1459-
if self.path_config.is_dir():
1460-
await remove_data(self.path_config)
1461-
14621474
try:
14631475
await self.sys_run_in_executor(_restore_data)
14641476
except shutil.Error as err:
@@ -1468,7 +1480,7 @@ def _restore_data():
14681480

14691481
# Restore AppArmor
14701482
profile_file = Path(tmp.name, "apparmor.txt")
1471-
if profile_file.exists():
1483+
if await self.sys_run_in_executor(profile_file.exists):
14721484
try:
14731485
await self.sys_host.apparmor.load_profile(
14741486
self.slug, profile_file
@@ -1489,7 +1501,7 @@ def _restore_data():
14891501
if data[ATTR_STATE] == AddonState.STARTED:
14901502
wait_for_start = await self.start()
14911503
finally:
1492-
tmp.cleanup()
1504+
await self.sys_run_in_executor(tmp.cleanup)
14931505
_LOGGER.info("Finished restore for add-on %s", self.slug)
14941506
return wait_for_start
14951507

supervisor/addons/build.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,6 @@ def base_image(self) -> str:
8181
)
8282
return self._data[ATTR_BUILD_FROM][self.arch]
8383

84-
@property
85-
def dockerfile(self) -> Path:
86-
"""Return Dockerfile path."""
87-
if self.addon.path_location.joinpath(f"Dockerfile.{self.arch}").exists():
88-
return self.addon.path_location.joinpath(f"Dockerfile.{self.arch}")
89-
return self.addon.path_location.joinpath("Dockerfile")
90-
9184
@property
9285
def squash(self) -> bool:
9386
"""Return True or False if squash is active."""
@@ -103,25 +96,40 @@ def additional_labels(self) -> dict[str, str]:
10396
"""Return additional Docker labels."""
10497
return self._data[ATTR_LABELS]
10598

106-
@property
107-
def is_valid(self) -> bool:
99+
def get_dockerfile(self) -> Path:
100+
"""Return Dockerfile path.
101+
102+
Must be run in executor.
103+
"""
104+
if self.addon.path_location.joinpath(f"Dockerfile.{self.arch}").exists():
105+
return self.addon.path_location.joinpath(f"Dockerfile.{self.arch}")
106+
return self.addon.path_location.joinpath("Dockerfile")
107+
108+
async def is_valid(self) -> bool:
108109
"""Return true if the build env is valid."""
109-
try:
110+
111+
def build_is_valid() -> bool:
110112
return all(
111113
[
112114
self.addon.path_location.is_dir(),
113-
self.dockerfile.is_file(),
115+
self.get_dockerfile().is_file(),
114116
]
115117
)
118+
119+
try:
120+
return await self.sys_run_in_executor(build_is_valid)
116121
except HassioArchNotFound:
117122
return False
118123

119124
def get_docker_args(self, version: AwesomeVersion, image: str | None = None):
120-
"""Create a dict with Docker build arguments."""
125+
"""Create a dict with Docker build arguments.
126+
127+
Must be run in executor.
128+
"""
121129
args = {
122130
"path": str(self.addon.path_location),
123131
"tag": f"{image or self.addon.image}:{version!s}",
124-
"dockerfile": str(self.dockerfile),
132+
"dockerfile": str(self.get_dockerfile()),
125133
"pull": True,
126134
"forcerm": not self.sys_dev,
127135
"squash": self.squash,

supervisor/addons/utils.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
from __future__ import annotations
44

5-
import asyncio
65
import logging
76
from pathlib import Path
7+
import subprocess
88
from typing import TYPE_CHECKING
99

1010
from ..const import ROLE_ADMIN, ROLE_MANAGER, SECURITY_DISABLE, SECURITY_PROFILE
@@ -86,18 +86,20 @@ def rating_security(addon: AddonModel) -> int:
8686
return max(min(8, rating), 1)
8787

8888

89-
async def remove_data(folder: Path) -> None:
90-
"""Remove folder and reset privileged."""
89+
def remove_data(folder: Path) -> None:
90+
"""Remove folder and reset privileged.
91+
92+
Must be run in executor.
93+
"""
9194
try:
92-
proc = await asyncio.create_subprocess_exec(
93-
"rm", "-rf", str(folder), stdout=asyncio.subprocess.DEVNULL
95+
subprocess.run(
96+
["rm", "-rf", str(folder)], stdout=subprocess.DEVNULL, text=True, check=True
9497
)
95-
96-
_, error_msg = await proc.communicate()
9798
except OSError as err:
9899
error_msg = str(err)
100+
except subprocess.CalledProcessError as procerr:
101+
error_msg = procerr.stderr.strip()
99102
else:
100-
if proc.returncode == 0:
101-
return
103+
return
102104

103105
_LOGGER.error("Can't remove Add-on Data: %s", error_msg)

0 commit comments

Comments
 (0)