mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 12:47:08 +00:00
Go2rtc server start is waiting until we got the api listen stdout line (#129391)
This commit is contained in:
parent
6c664e7ba9
commit
13416825b1
@ -5,9 +5,12 @@ import logging
|
|||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
_TERMINATE_TIMEOUT = 5
|
_TERMINATE_TIMEOUT = 5
|
||||||
|
_SETUP_TIMEOUT = 30
|
||||||
|
_SUCCESSFUL_BOOT_MESSAGE = "INF [api] listen addr=127.0.0.1:1984"
|
||||||
|
|
||||||
# Default configuration for HA
|
# Default configuration for HA
|
||||||
# - Api is listening only on localhost
|
# - Api is listening only on localhost
|
||||||
@ -34,14 +37,6 @@ def _create_temp_file() -> str:
|
|||||||
return file.name
|
return file.name
|
||||||
|
|
||||||
|
|
||||||
async def _log_output(process: asyncio.subprocess.Process) -> None:
|
|
||||||
"""Log the output of the process."""
|
|
||||||
assert process.stdout is not None
|
|
||||||
|
|
||||||
async for line in process.stdout:
|
|
||||||
_LOGGER.debug(line[:-1].decode().strip())
|
|
||||||
|
|
||||||
|
|
||||||
class Server:
|
class Server:
|
||||||
"""Go2rtc server."""
|
"""Go2rtc server."""
|
||||||
|
|
||||||
@ -50,12 +45,15 @@ class Server:
|
|||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._binary = binary
|
self._binary = binary
|
||||||
self._process: asyncio.subprocess.Process | None = None
|
self._process: asyncio.subprocess.Process | None = None
|
||||||
|
self._startup_complete = asyncio.Event()
|
||||||
|
|
||||||
async def start(self) -> None:
|
async def start(self) -> None:
|
||||||
"""Start the server."""
|
"""Start the server."""
|
||||||
_LOGGER.debug("Starting go2rtc server")
|
_LOGGER.debug("Starting go2rtc server")
|
||||||
config_file = await self._hass.async_add_executor_job(_create_temp_file)
|
config_file = await self._hass.async_add_executor_job(_create_temp_file)
|
||||||
|
|
||||||
|
self._startup_complete.clear()
|
||||||
|
|
||||||
self._process = await asyncio.create_subprocess_exec(
|
self._process = await asyncio.create_subprocess_exec(
|
||||||
self._binary,
|
self._binary,
|
||||||
"-c",
|
"-c",
|
||||||
@ -66,9 +64,30 @@ class Server:
|
|||||||
)
|
)
|
||||||
|
|
||||||
self._hass.async_create_background_task(
|
self._hass.async_create_background_task(
|
||||||
_log_output(self._process), "Go2rtc log output"
|
self._log_output(self._process), "Go2rtc log output"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with asyncio.timeout(_SETUP_TIMEOUT):
|
||||||
|
await self._startup_complete.wait()
|
||||||
|
except TimeoutError as err:
|
||||||
|
msg = "Go2rtc server didn't start correctly"
|
||||||
|
_LOGGER.exception(msg)
|
||||||
|
await self.stop()
|
||||||
|
raise HomeAssistantError("Go2rtc server didn't start correctly") from err
|
||||||
|
|
||||||
|
async def _log_output(self, process: asyncio.subprocess.Process) -> None:
|
||||||
|
"""Log the output of the process."""
|
||||||
|
assert process.stdout is not None
|
||||||
|
|
||||||
|
async for line in process.stdout:
|
||||||
|
msg = line[:-1].decode().strip()
|
||||||
|
_LOGGER.debug(msg)
|
||||||
|
if not self._startup_complete.is_set() and msg.endswith(
|
||||||
|
_SUCCESSFUL_BOOT_MESSAGE
|
||||||
|
):
|
||||||
|
self._startup_complete.set()
|
||||||
|
|
||||||
async def stop(self) -> None:
|
async def stop(self) -> None:
|
||||||
"""Stop the server."""
|
"""Stop the server."""
|
||||||
if self._process:
|
if self._process:
|
||||||
|
@ -35,17 +35,39 @@ def ws_client() -> Generator[Mock]:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def server_start() -> Generator[AsyncMock]:
|
def server_stdout() -> list[str]:
|
||||||
"""Mock start of a go2rtc server."""
|
"""Server stdout lines."""
|
||||||
with (
|
return [
|
||||||
patch(f"{GO2RTC_PATH}.server.asyncio.create_subprocess_exec") as mock_subproc,
|
"09:00:03.466 INF go2rtc platform=linux/amd64 revision=780f378 version=1.9.5",
|
||||||
patch(
|
"09:00:03.466 INF config path=/tmp/go2rtc.yaml",
|
||||||
f"{GO2RTC_PATH}.server.Server.start", wraps=Server.start, autospec=True
|
"09:00:03.467 INF [rtsp] listen addr=:8554",
|
||||||
) as mock_server_start,
|
"09:00:03.467 INF [api] listen addr=127.0.0.1:1984",
|
||||||
):
|
"09:00:03.467 INF [webrtc] listen addr=:8555/tcp",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_create_subprocess(server_stdout: list[str]) -> Generator[AsyncMock]:
|
||||||
|
"""Mock create_subprocess_exec."""
|
||||||
|
with patch(f"{GO2RTC_PATH}.server.asyncio.create_subprocess_exec") as mock_subproc:
|
||||||
subproc = AsyncMock()
|
subproc = AsyncMock()
|
||||||
subproc.terminate = Mock()
|
subproc.terminate = Mock()
|
||||||
|
subproc.kill = Mock()
|
||||||
|
subproc.returncode = None
|
||||||
|
# Simulate process output
|
||||||
|
subproc.stdout.__aiter__.return_value = iter(
|
||||||
|
[f"{entry}\n".encode() for entry in server_stdout]
|
||||||
|
)
|
||||||
mock_subproc.return_value = subproc
|
mock_subproc.return_value = subproc
|
||||||
|
yield mock_subproc
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def server_start(mock_create_subprocess: AsyncMock) -> Generator[AsyncMock]:
|
||||||
|
"""Mock start of a go2rtc server."""
|
||||||
|
with patch(
|
||||||
|
f"{GO2RTC_PATH}.server.Server.start", wraps=Server.start, autospec=True
|
||||||
|
) as mock_server_start:
|
||||||
yield mock_server_start
|
yield mock_server_start
|
||||||
|
|
||||||
|
|
||||||
@ -61,7 +83,7 @@ def server_stop() -> Generator[AsyncMock]:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def server(server_start, server_stop) -> Generator[AsyncMock]:
|
def server(server_start: AsyncMock, server_stop: AsyncMock) -> Generator[AsyncMock]:
|
||||||
"""Mock a go2rtc server."""
|
"""Mock a go2rtc server."""
|
||||||
with patch(f"{GO2RTC_PATH}.Server", wraps=Server) as mock_server:
|
with patch(f"{GO2RTC_PATH}.Server", wraps=Server) as mock_server:
|
||||||
yield mock_server
|
yield mock_server
|
||||||
|
@ -4,12 +4,13 @@ import asyncio
|
|||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
import logging
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
from unittest.mock import MagicMock, Mock, patch
|
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.go2rtc.server import Server
|
from homeassistant.components.go2rtc.server import Server
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
TEST_BINARY = "/bin/go2rtc"
|
TEST_BINARY = "/bin/go2rtc"
|
||||||
|
|
||||||
@ -31,37 +32,18 @@ def mock_tempfile() -> Generator[Mock]:
|
|||||||
yield file
|
yield file
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def mock_process() -> Generator[MagicMock]:
|
|
||||||
"""Fixture to mock subprocess.Popen."""
|
|
||||||
with patch(
|
|
||||||
"homeassistant.components.go2rtc.server.asyncio.create_subprocess_exec"
|
|
||||||
) as mock_popen:
|
|
||||||
mock_popen.return_value.terminate = MagicMock()
|
|
||||||
mock_popen.return_value.kill = MagicMock()
|
|
||||||
mock_popen.return_value.returncode = None
|
|
||||||
yield mock_popen
|
|
||||||
|
|
||||||
|
|
||||||
async def test_server_run_success(
|
async def test_server_run_success(
|
||||||
mock_process: MagicMock,
|
mock_create_subprocess: AsyncMock,
|
||||||
|
server_stdout: list[str],
|
||||||
server: Server,
|
server: Server,
|
||||||
caplog: pytest.LogCaptureFixture,
|
caplog: pytest.LogCaptureFixture,
|
||||||
mock_tempfile: Mock,
|
mock_tempfile: Mock,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test that the server runs successfully."""
|
"""Test that the server runs successfully."""
|
||||||
# Simulate process output
|
|
||||||
mock_process.return_value.stdout.__aiter__.return_value = iter(
|
|
||||||
[
|
|
||||||
b"log line 1\n",
|
|
||||||
b"log line 2\n",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
await server.start()
|
await server.start()
|
||||||
|
|
||||||
# Check that Popen was called with the right arguments
|
# Check that Popen was called with the right arguments
|
||||||
mock_process.assert_called_once_with(
|
mock_create_subprocess.assert_called_once_with(
|
||||||
TEST_BINARY,
|
TEST_BINARY,
|
||||||
"-c",
|
"-c",
|
||||||
"test.yaml",
|
"test.yaml",
|
||||||
@ -83,7 +65,7 @@ webrtc:
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
# Check that server read the log lines
|
# Check that server read the log lines
|
||||||
for entry in ("log line 1", "log line 2"):
|
for entry in server_stdout:
|
||||||
assert (
|
assert (
|
||||||
"homeassistant.components.go2rtc.server",
|
"homeassistant.components.go2rtc.server",
|
||||||
logging.DEBUG,
|
logging.DEBUG,
|
||||||
@ -91,31 +73,74 @@ webrtc:
|
|||||||
) in caplog.record_tuples
|
) in caplog.record_tuples
|
||||||
|
|
||||||
await server.stop()
|
await server.stop()
|
||||||
mock_process.return_value.terminate.assert_called_once()
|
mock_create_subprocess.return_value.terminate.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("mock_tempfile")
|
@pytest.mark.usefixtures("mock_tempfile")
|
||||||
async def test_server_run_process_timeout(
|
async def test_server_timeout_on_stop(
|
||||||
mock_process: MagicMock, server: Server
|
mock_create_subprocess: MagicMock, server: Server
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test server run where the process takes too long to terminate."""
|
"""Test server run where the process takes too long to terminate."""
|
||||||
mock_process.return_value.stdout.__aiter__.return_value = iter(
|
# Start server thread
|
||||||
[
|
await server.start()
|
||||||
b"log line 1\n",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
async def sleep() -> None:
|
async def sleep() -> None:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
# Simulate timeout
|
# Simulate timeout
|
||||||
mock_process.return_value.wait.side_effect = sleep
|
mock_create_subprocess.return_value.wait.side_effect = sleep
|
||||||
|
|
||||||
with patch("homeassistant.components.go2rtc.server._TERMINATE_TIMEOUT", new=0.1):
|
with patch("homeassistant.components.go2rtc.server._TERMINATE_TIMEOUT", new=0.1):
|
||||||
# Start server thread
|
|
||||||
await server.start()
|
|
||||||
await server.stop()
|
await server.stop()
|
||||||
|
|
||||||
# Ensure terminate and kill were called due to timeout
|
# Ensure terminate and kill were called due to timeout
|
||||||
mock_process.return_value.terminate.assert_called_once()
|
mock_create_subprocess.return_value.terminate.assert_called_once()
|
||||||
mock_process.return_value.kill.assert_called_once()
|
mock_create_subprocess.return_value.kill.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"server_stdout",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"09:00:03.466 INF go2rtc platform=linux/amd64 revision=780f378 version=1.9.5",
|
||||||
|
"09:00:03.466 INF config path=/tmp/go2rtc.yaml",
|
||||||
|
]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.usefixtures("mock_tempfile")
|
||||||
|
async def test_server_failed_to_start(
|
||||||
|
mock_create_subprocess: MagicMock,
|
||||||
|
server_stdout: list[str],
|
||||||
|
server: Server,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test server, where an exception is raised if the expected log entry was not received until the timeout."""
|
||||||
|
with (
|
||||||
|
patch("homeassistant.components.go2rtc.server._SETUP_TIMEOUT", new=0.1),
|
||||||
|
pytest.raises(HomeAssistantError, match="Go2rtc server didn't start correctly"),
|
||||||
|
):
|
||||||
|
await server.start()
|
||||||
|
|
||||||
|
# Verify go2rtc binary stdout was logged
|
||||||
|
for entry in server_stdout:
|
||||||
|
assert (
|
||||||
|
"homeassistant.components.go2rtc.server",
|
||||||
|
logging.DEBUG,
|
||||||
|
entry,
|
||||||
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
assert (
|
||||||
|
"homeassistant.components.go2rtc.server",
|
||||||
|
logging.ERROR,
|
||||||
|
"Go2rtc server didn't start correctly",
|
||||||
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
# Check that Popen was called with the right arguments
|
||||||
|
mock_create_subprocess.assert_called_once_with(
|
||||||
|
TEST_BINARY,
|
||||||
|
"-c",
|
||||||
|
"test.yaml",
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
close_fds=False,
|
||||||
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user