Finish migrating read_text to executor (#5698)

* Move read_text to executor

* switch to async_capture_exception

* Finish moving read_text to executor

* Cover read_bytes and some write_text calls as well

* Fix await issues

* Fix format_message
This commit is contained in:
Mike Degatano 2025-03-04 05:45:44 -05:00 committed by GitHub
parent c01d788c4c
commit 582b128ad9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 104 additions and 69 deletions

View File

@ -140,9 +140,7 @@ class Addon(AddonModel):
super().__init__(coresys, slug)
self.instance: DockerAddon = DockerAddon(coresys, self)
self._state: AddonState = AddonState.UNKNOWN
self._manual_stop: bool = (
self.sys_hardware.helper.last_boot != self.sys_config.last_boot
)
self._manual_stop: bool = False
self._listeners: list[EventListener] = []
self._startup_event = asyncio.Event()
self._startup_task: asyncio.Task | None = None
@ -216,6 +214,10 @@ class Addon(AddonModel):
async def load(self) -> None:
"""Async initialize of object."""
self._manual_stop = (
await self.sys_hardware.helper.last_boot() != self.sys_config.last_boot
)
if self.is_detached:
await super().refresh_path_cache()
@ -720,7 +722,7 @@ class Addon(AddonModel):
try:
options = self.schema.validate(self.options)
write_json_file(self.path_options, options)
await self.sys_run_in_executor(write_json_file, self.path_options, options)
except vol.Invalid as ex:
_LOGGER.error(
"Add-on %s has invalid options: %s",
@ -938,19 +940,20 @@ class Addon(AddonModel):
)
return out
def write_pulse(self) -> None:
async def write_pulse(self) -> None:
"""Write asound config to file and return True on success."""
pulse_config = self.sys_plugins.audio.pulse_client(
input_profile=self.audio_input, output_profile=self.audio_output
)
def write_pulse_config():
# Cleanup wrong maps
if self.path_pulse.is_dir():
shutil.rmtree(self.path_pulse, ignore_errors=True)
# Write pulse config
try:
self.path_pulse.write_text(pulse_config, encoding="utf-8")
try:
await self.sys_run_in_executor(write_pulse_config)
except OSError as err:
if err.errno == errno.EBADMSG:
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
@ -1070,7 +1073,7 @@ class Addon(AddonModel):
# Sound
if self.with_audio:
self.write_pulse()
await self.write_pulse()
def _check_addon_config_dir():
if self.path_config.is_dir():

View File

@ -50,7 +50,7 @@ class CpuArch(CoreSysAttributes):
async def load(self) -> None:
"""Load data and initialize default arch."""
try:
arch_data = read_json_file(ARCH_JSON)
arch_data = await self.sys_run_in_executor(read_json_file, ARCH_JSON)
except ConfigurationFileError:
_LOGGER.warning("Can't read arch json file from %s", ARCH_JSON)
return

View File

@ -223,7 +223,7 @@ class Core(CoreSysAttributes):
try:
# HomeAssistant is already running, only Supervisor restarted
if self.sys_hardware.helper.last_boot == self.sys_config.last_boot:
if await self.sys_hardware.helper.last_boot() == self.sys_config.last_boot:
_LOGGER.info("Detected Supervisor restart")
return
@ -362,7 +362,7 @@ class Core(CoreSysAttributes):
async def _update_last_boot(self):
"""Update last boot time."""
self.sys_config.last_boot = self.sys_hardware.helper.last_boot
self.sys_config.last_boot = await self.sys_hardware.helper.last_boot()
await self.sys_config.save_data()
async def _retrieve_whoami(self, with_ssl: bool) -> WhoamiData | None:

View File

@ -25,6 +25,7 @@ class HwHelper(CoreSysAttributes):
def __init__(self, coresys: CoreSys):
"""Init hardware object."""
self.coresys = coresys
self._last_boot: datetime | None = None
@property
def support_audio(self) -> bool:
@ -41,11 +42,15 @@ class HwHelper(CoreSysAttributes):
"""Return True if the device have USB ports."""
return bool(self.sys_hardware.filter_devices(subsystem=UdevSubsystem.USB))
@property
def last_boot(self) -> datetime | None:
async def last_boot(self) -> datetime | None:
"""Return last boot time."""
if self._last_boot:
return self._last_boot
try:
stats: str = _PROC_STAT.read_text(encoding="utf-8")
stats: str = await self.sys_run_in_executor(
_PROC_STAT.read_text, encoding="utf-8"
)
except OSError as err:
_LOGGER.error("Can't read stat data: %s", err)
return None
@ -56,7 +61,8 @@ class HwHelper(CoreSysAttributes):
_LOGGER.error("Can't found last boot time!")
return None
return datetime.fromtimestamp(int(found.group(1)), UTC)
self._last_boot = datetime.fromtimestamp(int(found.group(1)), UTC)
return self._last_boot
def hide_virtual_device(self, udev_device: pyudev.Device) -> bool:
"""Small helper to hide not needed Devices."""

View File

@ -342,7 +342,7 @@ class HomeAssistantCore(JobGroup):
await self.sys_homeassistant.save_data()
# Write audio settings
self.sys_homeassistant.write_pulse()
await self.sys_homeassistant.write_pulse()
try:
await self.instance.run(restore_job_id=self.sys_backups.current_restore)

View File

@ -313,19 +313,20 @@ class HomeAssistant(FileConfiguration, CoreSysAttributes):
BusEvent.HARDWARE_REMOVE_DEVICE, self._hardware_events
)
def write_pulse(self):
async def write_pulse(self):
"""Write asound config to file and return True on success."""
pulse_config = self.sys_plugins.audio.pulse_client(
input_profile=self.audio_input, output_profile=self.audio_output
)
def write_pulse_config():
# Cleanup wrong maps
if self.path_pulse.is_dir():
shutil.rmtree(self.path_pulse, ignore_errors=True)
# Write pulse config
try:
self.path_pulse.write_text(pulse_config, encoding="utf-8")
try:
await self.sys_run_in_executor(write_pulse_config)
except OSError as err:
if err.errno == errno.EBADMSG:
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE

View File

@ -72,7 +72,9 @@ class LogsControl(CoreSysAttributes):
async def load(self) -> None:
"""Load log control."""
try:
self._default_identifiers = read_json_file(SYSLOG_IDENTIFIERS_JSON)
self._default_identifiers = await self.sys_run_in_executor(
read_json_file, SYSLOG_IDENTIFIERS_JSON
)
except ConfigurationFileError:
_LOGGER.warning(
"Can't read syslog identifiers json file from %s",

View File

@ -88,7 +88,9 @@ class PluginAudio(PluginBase):
# Initialize Client Template
try:
self.client_template = jinja2.Template(
PULSE_CLIENT_TMPL.read_text(encoding="utf-8")
await self.sys_run_in_executor(
PULSE_CLIENT_TMPL.read_text, encoding="utf-8"
)
)
except OSError as err:
if err.errno == errno.EBADMSG:
@ -100,9 +102,13 @@ class PluginAudio(PluginBase):
# Setup default asound config
asound = self.sys_config.path_audio.joinpath("asound")
def setup_default_asound():
if not asound.exists():
try:
shutil.copy(ASOUND_TMPL, asound)
try:
await self.sys_run_in_executor(setup_default_asound)
except OSError as err:
if err.errno == errno.EBADMSG:
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
@ -123,7 +129,7 @@ class PluginAudio(PluginBase):
async def restart(self) -> None:
"""Restart Audio plugin."""
_LOGGER.info("Restarting Audio plugin")
self._write_config()
await self._write_config()
try:
await self.instance.restart()
except DockerError as err:
@ -132,7 +138,7 @@ class PluginAudio(PluginBase):
async def start(self) -> None:
"""Run Audio plugin."""
_LOGGER.info("Starting Audio plugin")
self._write_config()
await self._write_config()
try:
await self.instance.run()
except DockerError as err:
@ -177,10 +183,11 @@ class PluginAudio(PluginBase):
default_sink=output_profile,
)
def _write_config(self):
async def _write_config(self):
"""Write pulse audio config."""
try:
write_json_file(
await self.sys_run_in_executor(
write_json_file,
self.pulse_audio_config,
{
"debug": self.sys_config.logging == LogLevel.DEBUG,

View File

@ -152,15 +152,16 @@ class PluginDns(PluginBase):
# Initialize CoreDNS Template
try:
self.resolv_template = jinja2.Template(
RESOLV_TMPL.read_text(encoding="utf-8")
await self.sys_run_in_executor(RESOLV_TMPL.read_text, encoding="utf-8")
)
except OSError as err:
if err.errno == errno.EBADMSG:
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
_LOGGER.error("Can't read resolve.tmpl: %s", err)
try:
self.hosts_template = jinja2.Template(
HOSTS_TMPL.read_text(encoding="utf-8")
await self.sys_run_in_executor(HOSTS_TMPL.read_text, encoding="utf-8")
)
except OSError as err:
if err.errno == errno.EBADMSG:
@ -171,7 +172,7 @@ class PluginDns(PluginBase):
await super().load()
# Update supervisor
self._write_resolv(HOST_RESOLV)
await self._write_resolv(HOST_RESOLV)
await self.sys_supervisor.check_connectivity()
async def install(self) -> None:
@ -195,7 +196,7 @@ class PluginDns(PluginBase):
async def restart(self) -> None:
"""Restart CoreDNS plugin."""
self._write_config()
await self._write_config()
_LOGGER.info("Restarting CoreDNS plugin")
try:
await self.instance.restart()
@ -204,7 +205,7 @@ class PluginDns(PluginBase):
async def start(self) -> None:
"""Run CoreDNS."""
self._write_config()
await self._write_config()
# Start Instance
_LOGGER.info("Starting CoreDNS plugin")
@ -273,7 +274,7 @@ class PluginDns(PluginBase):
else:
self._loop = False
def _write_config(self) -> None:
async def _write_config(self) -> None:
"""Write CoreDNS config."""
debug: bool = self.sys_config.logging == LogLevel.DEBUG
dns_servers: list[str] = []
@ -297,7 +298,8 @@ class PluginDns(PluginBase):
# Write config to plugin
try:
write_json_file(
await self.sys_run_in_executor(
write_json_file,
self.coredns_config,
{
"servers": dns_servers,
@ -412,7 +414,7 @@ class PluginDns(PluginBase):
_LOGGER.error("Repair of CoreDNS failed")
await async_capture_exception(err)
def _write_resolv(self, resolv_conf: Path) -> None:
async def _write_resolv(self, resolv_conf: Path) -> None:
"""Update/Write resolv.conf file."""
if not self.resolv_template:
_LOGGER.warning(
@ -427,7 +429,7 @@ class PluginDns(PluginBase):
# Write config back to resolv
try:
resolv_conf.write_text(data)
await self.sys_run_in_executor(resolv_conf.write_text, data)
except OSError as err:
if err.errno == errno.EBADMSG:
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE

View File

@ -36,6 +36,9 @@ class EvaluateAppArmor(EvaluateBase):
async def evaluate(self) -> None:
"""Run evaluation."""
try:
return _APPARMOR_KERNEL.read_text(encoding="utf-8").strip().upper() != "Y"
apparmor = await self.sys_run_in_executor(
_APPARMOR_KERNEL.read_text, encoding="utf-8"
)
except OSError:
return True
return apparmor.strip().upper() != "Y"

View File

@ -34,7 +34,13 @@ class EvaluateLxc(EvaluateBase):
async def evaluate(self):
"""Run evaluation."""
def check_lxc():
with suppress(OSError):
if "container=lxc" in Path("/proc/1/environ").read_text(encoding="utf-8"):
if "container=lxc" in Path("/proc/1/environ").read_text(
encoding="utf-8"
):
return True
return Path("/dev/lxd/sock").exists()
return await self.sys_run_in_executor(check_lxc)

View File

@ -48,7 +48,10 @@ json_loads = orjson.loads # pylint: disable=no-member
def write_json_file(jsonfile: Path, data: Any) -> None:
"""Write a JSON file."""
"""Write a JSON file.
Must be run in executor.
"""
try:
with atomic_write(jsonfile, overwrite=True) as fp:
fp.write(
@ -67,7 +70,10 @@ def write_json_file(jsonfile: Path, data: Any) -> None:
def read_json_file(jsonfile: Path) -> Any:
"""Read a JSON file and return a dict."""
"""Read a JSON file and return a dict.
Must be run in executor.
"""
try:
return json_loads(jsonfile.read_bytes())
except (OSError, ValueError, TypeError, UnicodeDecodeError) as err:

View File

@ -20,6 +20,7 @@ from supervisor.docker.addon import DockerAddon
from supervisor.docker.const import ContainerState
from supervisor.docker.monitor import DockerContainerStateEvent
from supervisor.exceptions import AddonsError, AddonsJobError, AudioUpdateError
from supervisor.hardware.helper import HwHelper
from supervisor.ingress import Ingress
from supervisor.store.repository import Repository
from supervisor.utils.dt import utcnow
@ -250,11 +251,7 @@ async def test_watchdog_during_attach(
with (
patch.object(Addon, "restart") as restart,
patch.object(
type(coresys.hardware.helper),
"last_boot",
new=PropertyMock(return_value=utcnow()),
),
patch.object(HwHelper, "last_boot", return_value=utcnow()),
patch.object(DockerAddon, "attach"),
patch.object(
DockerAddon,
@ -262,7 +259,9 @@ async def test_watchdog_during_attach(
return_value=ContainerState.STOPPED,
),
):
coresys.config.last_boot = coresys.hardware.helper.last_boot + boot_timedelta
coresys.config.last_boot = (
await coresys.hardware.helper.last_boot() + boot_timedelta
)
addon = Addon(coresys, store.slug)
coresys.addons.local[addon.slug] = addon
addon.watchdog = True
@ -739,7 +738,7 @@ async def test_local_example_ingress_port_set(
assert install_addon_example.ingress_port != 0
def test_addon_pulse_error(
async def test_addon_pulse_error(
coresys: CoreSys,
install_addon_example: Addon,
caplog: pytest.LogCaptureFixture,
@ -750,14 +749,14 @@ def test_addon_pulse_error(
"supervisor.addons.addon.Path.write_text", side_effect=(err := OSError())
):
err.errno = errno.EBUSY
install_addon_example.write_pulse()
await install_addon_example.write_pulse()
assert "can't write pulse/client.config" in caplog.text
assert coresys.core.healthy is True
caplog.clear()
err.errno = errno.EBADMSG
install_addon_example.write_pulse()
await install_addon_example.write_pulse()
assert "can't write pulse/client.config" in caplog.text
assert coresys.core.healthy is False

View File

@ -87,13 +87,13 @@ def test_hide_virtual_device(coresys: CoreSys):
assert coresys.hardware.helper.hide_virtual_device(udev_device)
def test_last_boot_error(coresys: CoreSys, caplog: LogCaptureFixture):
async def test_last_boot_error(coresys: CoreSys, caplog: LogCaptureFixture):
"""Test error reading last boot."""
with patch(
"supervisor.hardware.helper.Path.read_text", side_effect=(err := OSError())
):
err.errno = errno.EBADMSG
assert coresys.hardware.helper.last_boot is None
assert await coresys.hardware.helper.last_boot() is None
assert coresys.core.healthy is True
assert "Can't read stat data" in caplog.text

View File

@ -66,21 +66,21 @@ async def test_get_users_none(coresys: CoreSys, ha_ws_client: AsyncMock):
)
def test_write_pulse_error(coresys: CoreSys, caplog: pytest.LogCaptureFixture):
async def test_write_pulse_error(coresys: CoreSys, caplog: pytest.LogCaptureFixture):
"""Test errors writing pulse config."""
with patch(
"supervisor.homeassistant.module.Path.write_text",
side_effect=(err := OSError()),
):
err.errno = errno.EBUSY
coresys.homeassistant.write_pulse()
await coresys.homeassistant.write_pulse()
assert "can't write pulse/client.config" in caplog.text
assert coresys.core.healthy is True
caplog.clear()
err.errno = errno.EBADMSG
coresys.homeassistant.write_pulse()
await coresys.homeassistant.write_pulse()
assert "can't write pulse/client.config" in caplog.text
assert coresys.core.healthy is False