mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-19 07:06:30 +00:00
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:
parent
c01d788c4c
commit
582b128ad9
@ -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():
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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."""
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user