mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-17 06:06:29 +00:00
Add blockbuster option to API (#5746)
* Add blockbuster option to API * cache not lru_cache
This commit is contained in:
parent
ec721c41c1
commit
80f7f07341
@ -11,10 +11,12 @@ import zlib_fast
|
|||||||
# Enable fast zlib before importing supervisor
|
# Enable fast zlib before importing supervisor
|
||||||
zlib_fast.enable()
|
zlib_fast.enable()
|
||||||
|
|
||||||
from supervisor import bootstrap # pylint: disable=wrong-import-position # noqa: E402
|
# pylint: disable=wrong-import-position
|
||||||
from supervisor.utils.logging import ( # pylint: disable=wrong-import-position # noqa: E402
|
from supervisor import bootstrap # noqa: E402
|
||||||
activate_log_queue_handler,
|
from supervisor.utils.blockbuster import activate_blockbuster # noqa: E402
|
||||||
)
|
from supervisor.utils.logging import activate_log_queue_handler # noqa: E402
|
||||||
|
|
||||||
|
# pylint: enable=wrong-import-position
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -52,6 +54,8 @@ if __name__ == "__main__":
|
|||||||
_LOGGER.info("Initializing Supervisor setup")
|
_LOGGER.info("Initializing Supervisor setup")
|
||||||
coresys = loop.run_until_complete(bootstrap.initialize_coresys())
|
coresys = loop.run_until_complete(bootstrap.initialize_coresys())
|
||||||
loop.set_debug(coresys.config.debug)
|
loop.set_debug(coresys.config.debug)
|
||||||
|
if coresys.config.detect_blocking_io:
|
||||||
|
activate_blockbuster()
|
||||||
loop.run_until_complete(coresys.core.connect())
|
loop.run_until_complete(coresys.core.connect())
|
||||||
|
|
||||||
loop.run_until_complete(bootstrap.supervisor_debugger(coresys))
|
loop.run_until_complete(bootstrap.supervisor_debugger(coresys))
|
||||||
|
@ -80,3 +80,11 @@ class BootSlot(StrEnum):
|
|||||||
|
|
||||||
A = "A"
|
A = "A"
|
||||||
B = "B"
|
B = "B"
|
||||||
|
|
||||||
|
|
||||||
|
class DetectBlockingIO(StrEnum):
|
||||||
|
"""Enable/Disable detection for blocking I/O in event loop."""
|
||||||
|
|
||||||
|
OFF = "off"
|
||||||
|
ON = "on"
|
||||||
|
ON_AT_STARTUP = "on_at_startup"
|
||||||
|
@ -20,6 +20,7 @@ from ..const import (
|
|||||||
ATTR_CPU_PERCENT,
|
ATTR_CPU_PERCENT,
|
||||||
ATTR_DEBUG,
|
ATTR_DEBUG,
|
||||||
ATTR_DEBUG_BLOCK,
|
ATTR_DEBUG_BLOCK,
|
||||||
|
ATTR_DETECT_BLOCKING_IO,
|
||||||
ATTR_DIAGNOSTICS,
|
ATTR_DIAGNOSTICS,
|
||||||
ATTR_FORCE_SECURITY,
|
ATTR_FORCE_SECURITY,
|
||||||
ATTR_HEALTHY,
|
ATTR_HEALTHY,
|
||||||
@ -47,10 +48,15 @@ from ..const import (
|
|||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..exceptions import APIError
|
from ..exceptions import APIError
|
||||||
from ..store.validate import repositories
|
from ..store.validate import repositories
|
||||||
|
from ..utils.blockbuster import (
|
||||||
|
activate_blockbuster,
|
||||||
|
blockbuster_enabled,
|
||||||
|
deactivate_blockbuster,
|
||||||
|
)
|
||||||
from ..utils.sentry import close_sentry, init_sentry
|
from ..utils.sentry import close_sentry, init_sentry
|
||||||
from ..utils.validate import validate_timezone
|
from ..utils.validate import validate_timezone
|
||||||
from ..validate import version_tag, wait_boot
|
from ..validate import version_tag, wait_boot
|
||||||
from .const import CONTENT_TYPE_TEXT
|
from .const import CONTENT_TYPE_TEXT, DetectBlockingIO
|
||||||
from .utils import api_process, api_process_raw, api_validate
|
from .utils import api_process, api_process_raw, api_validate
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
@ -69,6 +75,7 @@ SCHEMA_OPTIONS = vol.Schema(
|
|||||||
vol.Optional(ATTR_CONTENT_TRUST): vol.Boolean(),
|
vol.Optional(ATTR_CONTENT_TRUST): vol.Boolean(),
|
||||||
vol.Optional(ATTR_FORCE_SECURITY): vol.Boolean(),
|
vol.Optional(ATTR_FORCE_SECURITY): vol.Boolean(),
|
||||||
vol.Optional(ATTR_AUTO_UPDATE): vol.Boolean(),
|
vol.Optional(ATTR_AUTO_UPDATE): vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_DETECT_BLOCKING_IO): vol.Coerce(DetectBlockingIO),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -101,6 +108,7 @@ class APISupervisor(CoreSysAttributes):
|
|||||||
ATTR_DEBUG_BLOCK: self.sys_config.debug_block,
|
ATTR_DEBUG_BLOCK: self.sys_config.debug_block,
|
||||||
ATTR_DIAGNOSTICS: self.sys_config.diagnostics,
|
ATTR_DIAGNOSTICS: self.sys_config.diagnostics,
|
||||||
ATTR_AUTO_UPDATE: self.sys_updater.auto_update,
|
ATTR_AUTO_UPDATE: self.sys_updater.auto_update,
|
||||||
|
ATTR_DETECT_BLOCKING_IO: blockbuster_enabled(),
|
||||||
# Depricated
|
# Depricated
|
||||||
ATTR_WAIT_BOOT: self.sys_config.wait_boot,
|
ATTR_WAIT_BOOT: self.sys_config.wait_boot,
|
||||||
ATTR_ADDONS: [
|
ATTR_ADDONS: [
|
||||||
@ -160,6 +168,17 @@ class APISupervisor(CoreSysAttributes):
|
|||||||
if ATTR_AUTO_UPDATE in body:
|
if ATTR_AUTO_UPDATE in body:
|
||||||
self.sys_updater.auto_update = body[ATTR_AUTO_UPDATE]
|
self.sys_updater.auto_update = body[ATTR_AUTO_UPDATE]
|
||||||
|
|
||||||
|
if detect_blocking_io := body.get(ATTR_DETECT_BLOCKING_IO):
|
||||||
|
if detect_blocking_io == DetectBlockingIO.ON_AT_STARTUP:
|
||||||
|
self.sys_config.detect_blocking_io = True
|
||||||
|
detect_blocking_io = DetectBlockingIO.ON
|
||||||
|
|
||||||
|
if detect_blocking_io == DetectBlockingIO.ON:
|
||||||
|
activate_blockbuster()
|
||||||
|
elif detect_blocking_io == DetectBlockingIO.OFF:
|
||||||
|
self.sys_config.detect_blocking_io = False
|
||||||
|
deactivate_blockbuster()
|
||||||
|
|
||||||
# Deprecated
|
# Deprecated
|
||||||
if ATTR_WAIT_BOOT in body:
|
if ATTR_WAIT_BOOT in body:
|
||||||
self.sys_config.wait_boot = body[ATTR_WAIT_BOOT]
|
self.sys_config.wait_boot = body[ATTR_WAIT_BOOT]
|
||||||
|
@ -12,6 +12,7 @@ from .const import (
|
|||||||
ATTR_ADDONS_CUSTOM_LIST,
|
ATTR_ADDONS_CUSTOM_LIST,
|
||||||
ATTR_DEBUG,
|
ATTR_DEBUG,
|
||||||
ATTR_DEBUG_BLOCK,
|
ATTR_DEBUG_BLOCK,
|
||||||
|
ATTR_DETECT_BLOCKING_IO,
|
||||||
ATTR_DIAGNOSTICS,
|
ATTR_DIAGNOSTICS,
|
||||||
ATTR_IMAGE,
|
ATTR_IMAGE,
|
||||||
ATTR_LAST_BOOT,
|
ATTR_LAST_BOOT,
|
||||||
@ -142,6 +143,16 @@ class CoreConfig(FileConfiguration):
|
|||||||
"""Set debug wait mode."""
|
"""Set debug wait mode."""
|
||||||
self._data[ATTR_DEBUG_BLOCK] = value
|
self._data[ATTR_DEBUG_BLOCK] = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def detect_blocking_io(self) -> bool:
|
||||||
|
"""Return True if blocking I/O in event loop detection enabled at startup."""
|
||||||
|
return self._data[ATTR_DETECT_BLOCKING_IO]
|
||||||
|
|
||||||
|
@detect_blocking_io.setter
|
||||||
|
def detect_blocking_io(self, value: bool) -> None:
|
||||||
|
"""Enable/Disable blocking I/O in event loop detection at startup."""
|
||||||
|
self._data[ATTR_DETECT_BLOCKING_IO] = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def diagnostics(self) -> bool | None:
|
def diagnostics(self) -> bool | None:
|
||||||
"""Return bool if diagnostics is set otherwise None."""
|
"""Return bool if diagnostics is set otherwise None."""
|
||||||
|
@ -152,6 +152,7 @@ ATTR_DEFAULT = "default"
|
|||||||
ATTR_DEPLOYMENT = "deployment"
|
ATTR_DEPLOYMENT = "deployment"
|
||||||
ATTR_DESCRIPTON = "description"
|
ATTR_DESCRIPTON = "description"
|
||||||
ATTR_DETACHED = "detached"
|
ATTR_DETACHED = "detached"
|
||||||
|
ATTR_DETECT_BLOCKING_IO = "detect_blocking_io"
|
||||||
ATTR_DEVICES = "devices"
|
ATTR_DEVICES = "devices"
|
||||||
ATTR_DEVICETREE = "devicetree"
|
ATTR_DEVICETREE = "devicetree"
|
||||||
ATTR_DIAGNOSTICS = "diagnostics"
|
ATTR_DIAGNOSTICS = "diagnostics"
|
||||||
|
35
supervisor/utils/blockbuster.py
Normal file
35
supervisor/utils/blockbuster.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
"""Activate and deactivate blockbuster for finding blocking I/O."""
|
||||||
|
|
||||||
|
from functools import cache
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from blockbuster import BlockBuster
|
||||||
|
|
||||||
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@cache
|
||||||
|
def _get_blockbuster() -> BlockBuster:
|
||||||
|
"""Get blockbuster instance."""
|
||||||
|
return BlockBuster()
|
||||||
|
|
||||||
|
|
||||||
|
def blockbuster_enabled() -> bool:
|
||||||
|
"""Return true if blockbuster detection is enabled."""
|
||||||
|
blockbuster = _get_blockbuster()
|
||||||
|
# We activate all or none so just check the first one
|
||||||
|
for _, fn in blockbuster.functions.items():
|
||||||
|
return fn.activated
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def activate_blockbuster() -> None:
|
||||||
|
"""Activate blockbuster detection."""
|
||||||
|
_LOGGER.info("Activating BlockBuster blocking I/O detection")
|
||||||
|
_get_blockbuster().activate()
|
||||||
|
|
||||||
|
|
||||||
|
def deactivate_blockbuster() -> None:
|
||||||
|
"""Deactivate blockbuster detection."""
|
||||||
|
_LOGGER.info("Deactivating BlockBuster blocking I/O detection")
|
||||||
|
_get_blockbuster().deactivate()
|
@ -15,6 +15,7 @@ from .const import (
|
|||||||
ATTR_CONTENT_TRUST,
|
ATTR_CONTENT_TRUST,
|
||||||
ATTR_DEBUG,
|
ATTR_DEBUG,
|
||||||
ATTR_DEBUG_BLOCK,
|
ATTR_DEBUG_BLOCK,
|
||||||
|
ATTR_DETECT_BLOCKING_IO,
|
||||||
ATTR_DIAGNOSTICS,
|
ATTR_DIAGNOSTICS,
|
||||||
ATTR_DISPLAYNAME,
|
ATTR_DISPLAYNAME,
|
||||||
ATTR_DNS,
|
ATTR_DNS,
|
||||||
@ -162,6 +163,7 @@ SCHEMA_SUPERVISOR_CONFIG = vol.Schema(
|
|||||||
vol.Optional(ATTR_DEBUG, default=False): vol.Boolean(),
|
vol.Optional(ATTR_DEBUG, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_DEBUG_BLOCK, default=False): vol.Boolean(),
|
vol.Optional(ATTR_DEBUG_BLOCK, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_DIAGNOSTICS, default=None): vol.Maybe(vol.Boolean()),
|
vol.Optional(ATTR_DIAGNOSTICS, default=None): vol.Maybe(vol.Boolean()),
|
||||||
|
vol.Optional(ATTR_DETECT_BLOCKING_IO, default=False): vol.Boolean(),
|
||||||
},
|
},
|
||||||
extra=vol.REMOVE_EXTRA,
|
extra=vol.REMOVE_EXTRA,
|
||||||
)
|
)
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
"""Test Supervisor API."""
|
"""Test Supervisor API."""
|
||||||
|
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
|
import time
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from aiohttp.test_utils import TestClient
|
from aiohttp.test_utils import TestClient
|
||||||
|
from blockbuster import BlockingError
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from supervisor.coresys import CoreSys
|
from supervisor.coresys import CoreSys
|
||||||
@ -247,3 +249,46 @@ async def test_api_supervisor_options_timezone(
|
|||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
|
|
||||||
assert coresys.timezone == "Europe/Zurich"
|
assert coresys.timezone == "Europe/Zurich"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("blockbuster", "option_value", "config_value"),
|
||||||
|
[("no_blockbuster", "on", False), ("no_blockbuster", "on_at_startup", True)],
|
||||||
|
indirect=["blockbuster"],
|
||||||
|
)
|
||||||
|
async def test_api_supervisor_options_blocking_io(
|
||||||
|
api_client: TestClient, coresys: CoreSys, option_value: str, config_value: bool
|
||||||
|
):
|
||||||
|
"""Test setting supervisor detect blocking io option."""
|
||||||
|
# This should not fail with a blocking error yet
|
||||||
|
time.sleep(0)
|
||||||
|
|
||||||
|
resp = await api_client.post(
|
||||||
|
"/supervisor/options", json={"detect_blocking_io": option_value}
|
||||||
|
)
|
||||||
|
assert resp.status == 200
|
||||||
|
|
||||||
|
resp = await api_client.get("/supervisor/info")
|
||||||
|
assert resp.status == 200
|
||||||
|
body = await resp.json()
|
||||||
|
assert body["data"]["detect_blocking_io"] is True
|
||||||
|
|
||||||
|
# This remains false because we only turned it on for current run of supervisor, not permanently
|
||||||
|
assert coresys.config.detect_blocking_io is config_value
|
||||||
|
|
||||||
|
with pytest.raises(BlockingError):
|
||||||
|
time.sleep(0)
|
||||||
|
|
||||||
|
resp = await api_client.post(
|
||||||
|
"/supervisor/options", json={"detect_blocking_io": "off"}
|
||||||
|
)
|
||||||
|
assert resp.status == 200
|
||||||
|
|
||||||
|
resp = await api_client.get("/supervisor/info")
|
||||||
|
assert resp.status == 200
|
||||||
|
body = await resp.json()
|
||||||
|
assert body["data"]["detect_blocking_io"] is False
|
||||||
|
assert coresys.config.detect_blocking_io is False
|
||||||
|
|
||||||
|
# This should not raise blocking error anymore
|
||||||
|
time.sleep(0)
|
||||||
|
@ -65,8 +65,12 @@ from .dbus_service_mocks.network_manager import NetworkManager as NetworkManager
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def blockbuster() -> BlockBuster:
|
def blockbuster(request: pytest.FixtureRequest) -> BlockBuster | None:
|
||||||
"""Raise for blocking I/O in event loop."""
|
"""Raise for blocking I/O in event loop."""
|
||||||
|
if getattr(request, "param", "") == "no_blockbuster":
|
||||||
|
yield None
|
||||||
|
return
|
||||||
|
|
||||||
# Only scanning supervisor code for now as that's our primary interest
|
# Only scanning supervisor code for now as that's our primary interest
|
||||||
# This will still raise for tests that call utilities in supervisor code that block
|
# This will still raise for tests that call utilities in supervisor code that block
|
||||||
# But it will ignore calls to libraries and such that do blocking I/O directly from tests
|
# But it will ignore calls to libraries and such that do blocking I/O directly from tests
|
||||||
|
Loading…
x
Reference in New Issue
Block a user