Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions supervisor/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
# Enable fast zlib before importing supervisor
zlib_fast.enable()

from supervisor import bootstrap # pylint: disable=wrong-import-position # noqa: E402
from supervisor.utils.logging import ( # pylint: disable=wrong-import-position # noqa: E402
activate_log_queue_handler,
)
# pylint: disable=wrong-import-position
from supervisor import bootstrap # noqa: E402
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__)

Expand Down Expand Up @@ -52,6 +54,8 @@ def run_os_startup_check_cleanup() -> None:
_LOGGER.info("Initializing Supervisor setup")
coresys = loop.run_until_complete(bootstrap.initialize_coresys())
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(bootstrap.supervisor_debugger(coresys))
Expand Down
8 changes: 8 additions & 0 deletions supervisor/api/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,11 @@ class BootSlot(StrEnum):

A = "A"
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"
21 changes: 20 additions & 1 deletion supervisor/api/supervisor.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
ATTR_CPU_PERCENT,
ATTR_DEBUG,
ATTR_DEBUG_BLOCK,
ATTR_DETECT_BLOCKING_IO,
ATTR_DIAGNOSTICS,
ATTR_FORCE_SECURITY,
ATTR_HEALTHY,
Expand Down Expand Up @@ -47,10 +48,15 @@
from ..coresys import CoreSysAttributes
from ..exceptions import APIError
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.validate import validate_timezone
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

_LOGGER: logging.Logger = logging.getLogger(__name__)
Expand All @@ -69,6 +75,7 @@
vol.Optional(ATTR_CONTENT_TRUST): vol.Boolean(),
vol.Optional(ATTR_FORCE_SECURITY): vol.Boolean(),
vol.Optional(ATTR_AUTO_UPDATE): vol.Boolean(),
vol.Optional(ATTR_DETECT_BLOCKING_IO): vol.Coerce(DetectBlockingIO),
}
)

Expand Down Expand Up @@ -101,6 +108,7 @@ async def info(self, request: web.Request) -> dict[str, Any]:
ATTR_DEBUG_BLOCK: self.sys_config.debug_block,
ATTR_DIAGNOSTICS: self.sys_config.diagnostics,
ATTR_AUTO_UPDATE: self.sys_updater.auto_update,
ATTR_DETECT_BLOCKING_IO: blockbuster_enabled(),
# Depricated
ATTR_WAIT_BOOT: self.sys_config.wait_boot,
ATTR_ADDONS: [
Expand Down Expand Up @@ -160,6 +168,17 @@ async def options(self, request: web.Request) -> None:
if ATTR_AUTO_UPDATE in body:
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
if ATTR_WAIT_BOOT in body:
self.sys_config.wait_boot = body[ATTR_WAIT_BOOT]
Expand Down
11 changes: 11 additions & 0 deletions supervisor/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
ATTR_ADDONS_CUSTOM_LIST,
ATTR_DEBUG,
ATTR_DEBUG_BLOCK,
ATTR_DETECT_BLOCKING_IO,
ATTR_DIAGNOSTICS,
ATTR_IMAGE,
ATTR_LAST_BOOT,
Expand Down Expand Up @@ -142,6 +143,16 @@ def debug_block(self, value: bool) -> None:
"""Set debug wait mode."""
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
def diagnostics(self) -> bool | None:
"""Return bool if diagnostics is set otherwise None."""
Expand Down
1 change: 1 addition & 0 deletions supervisor/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@
ATTR_DEPLOYMENT = "deployment"
ATTR_DESCRIPTON = "description"
ATTR_DETACHED = "detached"
ATTR_DETECT_BLOCKING_IO = "detect_blocking_io"
ATTR_DEVICES = "devices"
ATTR_DEVICETREE = "devicetree"
ATTR_DIAGNOSTICS = "diagnostics"
Expand Down
35 changes: 35 additions & 0 deletions supervisor/utils/blockbuster.py
Original file line number Diff line number Diff line change
@@ -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()
2 changes: 2 additions & 0 deletions supervisor/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
ATTR_CONTENT_TRUST,
ATTR_DEBUG,
ATTR_DEBUG_BLOCK,
ATTR_DETECT_BLOCKING_IO,
ATTR_DIAGNOSTICS,
ATTR_DISPLAYNAME,
ATTR_DNS,
Expand Down Expand Up @@ -162,6 +163,7 @@ def validate_repository(repository: str) -> str:
vol.Optional(ATTR_DEBUG, 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_DETECT_BLOCKING_IO, default=False): vol.Boolean(),
},
extra=vol.REMOVE_EXTRA,
)
Expand Down
45 changes: 45 additions & 0 deletions tests/api/test_supervisor.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
"""Test Supervisor API."""

# pylint: disable=protected-access
import time
from unittest.mock import MagicMock, patch

from aiohttp.test_utils import TestClient
from blockbuster import BlockingError
import pytest

from supervisor.coresys import CoreSys
Expand Down Expand Up @@ -247,3 +249,46 @@ async def test_api_supervisor_options_timezone(
assert resp.status == 200

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)
6 changes: 5 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,12 @@


@pytest.fixture(autouse=True)
def blockbuster() -> BlockBuster:
def blockbuster(request: pytest.FixtureRequest) -> BlockBuster | None:
"""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
# 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
Expand Down