1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-20 02:48:57 +00:00

Create the go2rtc unix socket inside a temporary folder (#157742)

This commit is contained in:
Robert Resch
2025-12-02 13:35:39 +01:00
committed by GitHub
parent 3bccb4b89c
commit 2c72cd94f2
8 changed files with 73 additions and 25 deletions

View File

@@ -6,6 +6,7 @@ from dataclasses import dataclass
import logging import logging
from secrets import token_hex from secrets import token_hex
import shutil import shutil
from tempfile import mkdtemp
from aiohttp import BasicAuth, ClientSession, UnixConnector from aiohttp import BasicAuth, ClientSession, UnixConnector
from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError
@@ -62,11 +63,11 @@ from .const import (
CONF_DEBUG_UI, CONF_DEBUG_UI,
DEBUG_UI_URL_MESSAGE, DEBUG_UI_URL_MESSAGE,
DOMAIN, DOMAIN,
HA_MANAGED_UNIX_SOCKET,
HA_MANAGED_URL, HA_MANAGED_URL,
RECOMMENDED_VERSION, RECOMMENDED_VERSION,
) )
from .server import Server from .server import Server
from .util import get_go2rtc_unix_socket_path
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -154,10 +155,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
auth = BasicAuth(username, password) auth = BasicAuth(username, password)
# HA will manage the binary # HA will manage the binary
temp_dir = mkdtemp(prefix="go2rtc-")
# Manually created session (not using the helper) needs to be closed manually # Manually created session (not using the helper) needs to be closed manually
# See on_stop listener below # See on_stop listener below
session = ClientSession( session = ClientSession(
connector=UnixConnector(path=HA_MANAGED_UNIX_SOCKET), auth=auth connector=UnixConnector(path=get_go2rtc_unix_socket_path(temp_dir)),
auth=auth,
) )
server = Server( server = Server(
hass, hass,
@@ -166,6 +169,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
enable_ui=domain_config.get(CONF_DEBUG_UI, False), enable_ui=domain_config.get(CONF_DEBUG_UI, False),
username=username, username=username,
password=password, password=password,
working_dir=temp_dir,
) )
try: try:
await server.start() await server.start()

View File

@@ -6,7 +6,6 @@ CONF_DEBUG_UI = "debug_ui"
DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time." DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time."
HA_MANAGED_API_PORT = 11984 HA_MANAGED_API_PORT = 11984
HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/" HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/"
HA_MANAGED_UNIX_SOCKET = "/run/go2rtc.sock"
# When changing this version, also update the corresponding SHA hash (_GO2RTC_SHA) # When changing this version, also update the corresponding SHA hash (_GO2RTC_SHA)
# in script/hassfest/docker.py. # in script/hassfest/docker.py.
RECOMMENDED_VERSION = "1.9.12" RECOMMENDED_VERSION = "1.9.12"

View File

@@ -12,13 +12,13 @@ from go2rtc_client import Go2RtcRestClient
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from .const import HA_MANAGED_API_PORT, HA_MANAGED_UNIX_SOCKET, HA_MANAGED_URL from .const import HA_MANAGED_API_PORT, HA_MANAGED_URL
from .util import get_go2rtc_unix_socket_path
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_TERMINATE_TIMEOUT = 5 _TERMINATE_TIMEOUT = 5
_SETUP_TIMEOUT = 30 _SETUP_TIMEOUT = 30
_SUCCESSFUL_BOOT_MESSAGE = "INF [api] listen addr=" _SUCCESSFUL_BOOT_MESSAGE = "INF [api] listen addr="
_LOCALHOST_IP = "127.0.0.1"
_LOG_BUFFER_SIZE = 512 _LOG_BUFFER_SIZE = 512
_RESPAWN_COOLDOWN = 1 _RESPAWN_COOLDOWN = 1
@@ -122,7 +122,9 @@ def _format_list_for_yaml(items: tuple[str, ...]) -> str:
return f"[{formatted_items}]" return f"[{formatted_items}]"
def _create_temp_file(enable_ui: bool, username: str, password: str) -> str: def _create_temp_file(
enable_ui: bool, username: str, password: str, working_dir: str
) -> str:
"""Create temporary config file.""" """Create temporary config file."""
app_modules: tuple[str, ...] = _APP_MODULES app_modules: tuple[str, ...] = _APP_MODULES
api_paths: tuple[str, ...] = _API_ALLOW_PATHS api_paths: tuple[str, ...] = _API_ALLOW_PATHS
@@ -139,11 +141,13 @@ def _create_temp_file(enable_ui: bool, username: str, password: str) -> str:
# Set delete=False to prevent the file from being deleted when the file is closed # Set delete=False to prevent the file from being deleted when the file is closed
# Linux is clearing tmp folder on reboot, so no need to delete it manually # Linux is clearing tmp folder on reboot, so no need to delete it manually
with NamedTemporaryFile(prefix="go2rtc_", suffix=".yaml", delete=False) as file: with NamedTemporaryFile(
prefix="go2rtc_", suffix=".yaml", dir=working_dir, delete=False
) as file:
file.write( file.write(
_GO2RTC_CONFIG_FORMAT.format( _GO2RTC_CONFIG_FORMAT.format(
listen_config=listen_config, listen_config=listen_config,
unix_socket=HA_MANAGED_UNIX_SOCKET, unix_socket=get_go2rtc_unix_socket_path(working_dir),
app_modules=_format_list_for_yaml(app_modules), app_modules=_format_list_for_yaml(app_modules),
api_allow_paths=_format_list_for_yaml(api_paths), api_allow_paths=_format_list_for_yaml(api_paths),
username=username, username=username,
@@ -165,6 +169,7 @@ class Server:
enable_ui: bool = False, enable_ui: bool = False,
username: str, username: str,
password: str, password: str,
working_dir: str,
) -> None: ) -> None:
"""Initialize the server.""" """Initialize the server."""
self._hass = hass self._hass = hass
@@ -173,6 +178,7 @@ class Server:
self._enable_ui = enable_ui self._enable_ui = enable_ui
self._username = username self._username = username
self._password = password self._password = password
self._working_dir = working_dir
self._log_buffer: deque[str] = deque(maxlen=_LOG_BUFFER_SIZE) self._log_buffer: deque[str] = deque(maxlen=_LOG_BUFFER_SIZE)
self._process: asyncio.subprocess.Process | None = None self._process: asyncio.subprocess.Process | None = None
self._startup_complete = asyncio.Event() self._startup_complete = asyncio.Event()
@@ -190,7 +196,11 @@ class Server:
"""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( config_file = await self._hass.async_add_executor_job(
_create_temp_file, self._enable_ui, self._username, self._password _create_temp_file,
self._enable_ui,
self._username,
self._password,
self._working_dir,
) )
self._startup_complete.clear() self._startup_complete.clear()

View File

@@ -0,0 +1,12 @@
"""Go2rtc utility functions."""
from pathlib import Path
_HA_MANAGED_UNIX_SOCKET_FILE = "go2rtc.sock"
def get_go2rtc_unix_socket_path(path: str | Path) -> str:
"""Get the Go2rtc unix socket path."""
if not isinstance(path, Path):
path = Path(path)
return str(path / _HA_MANAGED_UNIX_SOCKET_FILE)

View File

@@ -1,6 +1,7 @@
"""Go2rtc test configuration.""" """Go2rtc test configuration."""
from collections.abc import Generator from collections.abc import Generator
from pathlib import Path
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
@@ -228,3 +229,15 @@ async def init_test_integration(
await hass.async_block_till_done() await hass.async_block_till_done()
return test_camera return test_camera
@pytest.fixture
def server_dir(tmp_path: Path) -> Generator[Path]:
"""Fixture to provide a temporary directory for the server."""
server_dir = tmp_path / "go2rtc"
server_dir.mkdir()
with patch(
"homeassistant.components.go2rtc.mkdtemp",
return_value=str(server_dir),
):
yield server_dir

View File

@@ -3,7 +3,7 @@
_CallList([ _CallList([
_Call( _Call(
tuple( tuple(
b'# This file is managed by Home Assistant\n# Do not edit it manually\n\napp:\n modules: ["api","exec","ffmpeg","http","mjpeg","onvif","rtmp","rtsp","srtp","webrtc","ws"]\n\napi:\n listen: ""\n unix_listen: "/run/go2rtc.sock"\n allow_paths: ["/","/api","/api/frame.jpeg","/api/schemes","/api/streams","/api/webrtc","/api/ws"]\n local_auth: true\n username: d2a0b844f4cdbe773702176c47c9a675eb0c56a0779b8f880cdb3b492ed3b1c1\n password: bc495d266a32e66ba69b9c72546e00101e04fb573f1bd08863fe4ad1aac02949\n\n# ffmpeg needs the exec module\n# Restrict execution to only ffmpeg binary\nexec:\n allow_paths:\n - ffmpeg\n\nrtsp:\n listen: "127.0.0.1:18554"\n\nwebrtc:\n listen: ":18555/tcp"\n ice_servers: []\n', b'# This file is managed by Home Assistant\n# Do not edit it manually\n\napp:\n modules: ["api","exec","ffmpeg","http","mjpeg","onvif","rtmp","rtsp","srtp","webrtc","ws"]\n\napi:\n listen: ""\n unix_listen: "/test/path/go2rtc.sock"\n allow_paths: ["/","/api","/api/frame.jpeg","/api/schemes","/api/streams","/api/webrtc","/api/ws"]\n local_auth: true\n username: d2a0b844f4cdbe773702176c47c9a675eb0c56a0779b8f880cdb3b492ed3b1c1\n password: bc495d266a32e66ba69b9c72546e00101e04fb573f1bd08863fe4ad1aac02949\n\n# ffmpeg needs the exec module\n# Restrict execution to only ffmpeg binary\nexec:\n allow_paths:\n - ffmpeg\n\nrtsp:\n listen: "127.0.0.1:18554"\n\nwebrtc:\n listen: ":18555/tcp"\n ice_servers: []\n',
), ),
dict({ dict({
}), }),
@@ -14,7 +14,7 @@
_CallList([ _CallList([
_Call( _Call(
tuple( tuple(
b'# This file is managed by Home Assistant\n# Do not edit it manually\n\napp:\n modules: ["api","exec","ffmpeg","http","mjpeg","onvif","rtmp","rtsp","srtp","webrtc","ws","debug"]\n\napi:\n listen: ":11984"\n unix_listen: "/run/go2rtc.sock"\n allow_paths: ["/","/api","/api/frame.jpeg","/api/schemes","/api/streams","/api/webrtc","/api/ws","/api/config","/api/log","/api/streams.dot"]\n local_auth: true\n username: user\n password: pass\n\n# ffmpeg needs the exec module\n# Restrict execution to only ffmpeg binary\nexec:\n allow_paths:\n - ffmpeg\n\nrtsp:\n listen: "127.0.0.1:18554"\n\nwebrtc:\n listen: ":18555/tcp"\n ice_servers: []\n', b'# This file is managed by Home Assistant\n# Do not edit it manually\n\napp:\n modules: ["api","exec","ffmpeg","http","mjpeg","onvif","rtmp","rtsp","srtp","webrtc","ws","debug"]\n\napi:\n listen: ":11984"\n unix_listen: "/test/path/go2rtc.sock"\n allow_paths: ["/","/api","/api/frame.jpeg","/api/schemes","/api/streams","/api/webrtc","/api/ws","/api/config","/api/log","/api/streams.dot"]\n local_auth: true\n username: user\n password: pass\n\n# ffmpeg needs the exec module\n# Restrict execution to only ffmpeg binary\nexec:\n allow_paths:\n - ffmpeg\n\nrtsp:\n listen: "127.0.0.1:18554"\n\nwebrtc:\n listen: ":18555/tcp"\n ice_servers: []\n',
), ),
dict({ dict({
}), }),

View File

@@ -2,6 +2,7 @@
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
import logging import logging
from pathlib import Path
from typing import NamedTuple from typing import NamedTuple
from unittest.mock import ANY, AsyncMock, Mock, patch from unittest.mock import ANY, AsyncMock, Mock, patch
@@ -39,9 +40,9 @@ from homeassistant.components.go2rtc.const import (
CONF_DEBUG_UI, CONF_DEBUG_UI,
DEBUG_UI_URL_MESSAGE, DEBUG_UI_URL_MESSAGE,
DOMAIN, DOMAIN,
HA_MANAGED_UNIX_SOCKET,
RECOMMENDED_VERSION, RECOMMENDED_VERSION,
) )
from homeassistant.components.go2rtc.util import get_go2rtc_unix_socket_path
from homeassistant.components.stream import Orientation from homeassistant.components.stream import Orientation
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME
@@ -239,6 +240,7 @@ async def test_setup_go_binary(
server_stop: Mock, server_stop: Mock,
init_test_integration: MockCamera, init_test_integration: MockCamera,
has_go2rtc_entry: bool, has_go2rtc_entry: bool,
server_dir: Path,
config: ConfigType, config: ConfigType,
ui_enabled: bool, ui_enabled: bool,
expected_username: str, expected_username: str,
@@ -255,6 +257,7 @@ async def test_setup_go_binary(
enable_ui=ui_enabled, enable_ui=ui_enabled,
username=expected_username, username=expected_username,
password=expected_password, password=expected_password,
working_dir=str(server_dir),
) )
call_kwargs = server.call_args[1] call_kwargs = server.call_args[1]
assert call_kwargs["username"] == expected_username assert call_kwargs["username"] == expected_username
@@ -1034,7 +1037,7 @@ async def test_stream_orientation_with_generic_camera(
"rest_client", "rest_client",
"server", "server",
) )
async def test_unix_socket_connection(hass: HomeAssistant) -> None: async def test_unix_socket_connection(hass: HomeAssistant, server_dir: Path) -> None:
"""Test Unix socket is used for HA-managed go2rtc instances.""" """Test Unix socket is used for HA-managed go2rtc instances."""
config = {DOMAIN: {}} config = {DOMAIN: {}}
@@ -1056,7 +1059,7 @@ async def test_unix_socket_connection(hass: HomeAssistant) -> None:
assert "connector" in call_kwargs assert "connector" in call_kwargs
connector = call_kwargs["connector"] connector = call_kwargs["connector"]
assert isinstance(connector, UnixConnector) assert isinstance(connector, UnixConnector)
assert connector.path == HA_MANAGED_UNIX_SOCKET assert connector.path == get_go2rtc_unix_socket_path(server_dir)
# Auth should be auto-generated when credentials are not explicitly configured # Auth should be auto-generated when credentials are not explicitly configured
assert "auth" in call_kwargs assert "auth" in call_kwargs
auth = call_kwargs["auth"] auth = call_kwargs["auth"]
@@ -1120,7 +1123,7 @@ async def test_basic_auth_with_custom_url(hass: HomeAssistant) -> None:
@pytest.mark.usefixtures("rest_client") @pytest.mark.usefixtures("rest_client")
async def test_basic_auth_with_debug_ui(hass: HomeAssistant) -> None: async def test_basic_auth_with_debug_ui(hass: HomeAssistant, server_dir: Path) -> None:
"""Test BasicAuth session is created when username and password are provided with debug_ui.""" """Test BasicAuth session is created when username and password are provided with debug_ui."""
config = { config = {
DOMAIN: { DOMAIN: {
@@ -1158,7 +1161,7 @@ async def test_basic_auth_with_debug_ui(hass: HomeAssistant) -> None:
assert "connector" in call_kwargs assert "connector" in call_kwargs
connector = call_kwargs["connector"] connector = call_kwargs["connector"]
assert isinstance(connector, UnixConnector) assert isinstance(connector, UnixConnector)
assert connector.path == HA_MANAGED_UNIX_SOCKET assert connector.path == get_go2rtc_unix_socket_path(server_dir)
assert "auth" in call_kwargs assert "auth" in call_kwargs
auth = call_kwargs["auth"] auth = call_kwargs["auth"]
assert isinstance(auth, BasicAuth) assert isinstance(auth, BasicAuth)

View File

@@ -3,6 +3,7 @@
import asyncio import asyncio
from collections.abc import Generator from collections.abc import Generator
import logging import logging
from pathlib import Path
import subprocess import subprocess
from unittest.mock import AsyncMock, MagicMock, Mock, patch from unittest.mock import AsyncMock, MagicMock, Mock, patch
@@ -47,16 +48,22 @@ def server(
enable_ui: bool, enable_ui: bool,
username: str, username: str,
password: str, password: str,
) -> Server: server_dir: Path,
) -> Generator[Server]:
"""Fixture to initialize the Server.""" """Fixture to initialize the Server."""
return Server( with patch(
hass, "homeassistant.components.go2rtc.server.get_go2rtc_unix_socket_path",
binary=TEST_BINARY, return_value="/test/path/go2rtc.sock",
session=mock_session, ):
enable_ui=enable_ui, yield Server(
username=username, hass,
password=password, binary=TEST_BINARY,
) session=mock_session,
enable_ui=enable_ui,
username=username,
password=password,
working_dir=str(server_dir),
)
@pytest.fixture @pytest.fixture