diff --git a/supervisor/addons/addon.py b/supervisor/addons/addon.py index 0f55f53828e..32222d7d9b6 100644 --- a/supervisor/addons/addon.py +++ b/supervisor/addons/addon.py @@ -395,7 +395,7 @@ def ingress_port(self) -> int | None: port = self.data[ATTR_INGRESS_PORT] if port == 0: - return self.sys_ingress.get_dynamic_port(self.slug) + raise RuntimeError(f"No port set for add-on {self.slug}") return port @property @@ -539,7 +539,7 @@ async def watchdog_application(self) -> bool: # TCP monitoring if s_prefix == "tcp": - return await self.sys_run_in_executor(check_port, self.ip_address, port) + return await check_port(self.ip_address, port) # lookup the correct protocol from config if t_proto: @@ -602,6 +602,16 @@ async def unload(self) -> None: _LOGGER.info("Removing add-on data folder %s", self.path_data) await remove_data(self.path_data) + async def _check_ingress_port(self): + """Assign a ingress port if dynamic port selection is used.""" + if not self.with_ingress: + return + + if self.data[ATTR_INGRESS_PORT] == 0: + self.data[ATTR_INGRESS_PORT] = await self.sys_ingress.get_dynamic_port( + self.slug + ) + @Job( name="addon_install", limit=JobExecutionLimit.GROUP_ONCE, @@ -630,6 +640,8 @@ async def install(self) -> None: self.sys_addons.data.uninstall(self) raise AddonsError() from err + await self._check_ingress_port() + # Add to addon manager self.sys_addons.local[self.slug] = self @@ -716,6 +728,7 @@ async def update(self) -> asyncio.Task | None: try: _LOGGER.info("Add-on '%s' successfully updated", self.slug) self.sys_addons.data.update(store) + await self._check_ingress_port() # Cleanup with suppress(DockerError): @@ -756,6 +769,7 @@ async def rebuild(self) -> asyncio.Task | None: raise AddonsError() from err self.sys_addons.data.update(self.addon_store) + await self._check_ingress_port() _LOGGER.info("Add-on '%s' successfully rebuilt", self.slug) finally: @@ -1199,6 +1213,7 @@ def _extract_tarfile(): _LOGGER.info("Restore/Update of image for addon %s", self.slug) with suppress(DockerError): await self.instance.update(version, restore_image) + self._check_ingress_port() # Restore data and config def _restore_data(): diff --git a/supervisor/homeassistant/api.py b/supervisor/homeassistant/api.py index 00c3a838529..566ba0d6d3b 100644 --- a/supervisor/homeassistant/api.py +++ b/supervisor/homeassistant/api.py @@ -140,8 +140,7 @@ async def get_api_state(self) -> str | None: return None # Check if port is up - if not await self.sys_run_in_executor( - check_port, + if not await check_port( self.sys_homeassistant.ip_address, self.sys_homeassistant.api_port, ): diff --git a/supervisor/ingress.py b/supervisor/ingress.py index e61ef3377c4..d0e15ff9570 100644 --- a/supervisor/ingress.py +++ b/supervisor/ingress.py @@ -154,7 +154,7 @@ def validate_session(self, session: str) -> bool: return True - def get_dynamic_port(self, addon_slug: str) -> int: + async def get_dynamic_port(self, addon_slug: str) -> int: """Get/Create a dynamic port from range.""" if addon_slug in self.ports: return self.ports[addon_slug] @@ -163,7 +163,7 @@ def get_dynamic_port(self, addon_slug: str) -> int: while ( port is None or port in self.ports.values() - or check_port(self.sys_docker.network.gateway, port) + or await check_port(self.sys_docker.network.gateway, port) ): port = random.randint(62000, 65500) diff --git a/supervisor/utils/__init__.py b/supervisor/utils/__init__.py index 1e49a5b5dc7..9f83f45f78b 100644 --- a/supervisor/utils/__init__.py +++ b/supervisor/utils/__init__.py @@ -39,20 +39,19 @@ async def wrap_api(api, *args, **kwargs): return wrap_api -def check_port(address: IPv4Address, port: int) -> bool: +async def check_port(address: IPv4Address, port: int) -> bool: """Check if port is mapped.""" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(0.5) + sock.setblocking(False) try: - result = sock.connect_ex((str(address), port)) - sock.close() - - # Check if the port is available - if result == 0: - return True - except OSError: - pass - return False + async with asyncio.timeout(0.5): + await asyncio.get_running_loop().sock_connect(sock, (str(address), port)) + except (OSError, TimeoutError): + return False + finally: + if sock is not None: + sock.close() + return True def check_exception_chain(err: Exception, object_type: Any) -> bool: diff --git a/tests/test_ingress.py b/tests/test_ingress.py index e7f9777b471..c698a18c386 100644 --- a/tests/test_ingress.py +++ b/tests/test_ingress.py @@ -50,15 +50,15 @@ async def test_save_on_unload(coresys: CoreSys): assert coresys.ingress.save_data.called -def test_dynamic_ports(coresys: CoreSys): +async def test_dynamic_ports(coresys: CoreSys): """Test dyanmic port handling.""" - port_test1 = coresys.ingress.get_dynamic_port("test1") + port_test1 = await coresys.ingress.get_dynamic_port("test1") assert port_test1 assert coresys.ingress.save_data.called - assert port_test1 == coresys.ingress.get_dynamic_port("test1") + assert port_test1 == await coresys.ingress.get_dynamic_port("test1") - port_test2 = coresys.ingress.get_dynamic_port("test2") + port_test2 = await coresys.ingress.get_dynamic_port("test2") assert port_test2 assert port_test2 != port_test1 diff --git a/tests/utils/test_check_port.py b/tests/utils/test_check_port.py index 63f3ca4b195..f365c0d78c1 100644 --- a/tests/utils/test_check_port.py +++ b/tests/utils/test_check_port.py @@ -1,14 +1,15 @@ -"""Check ports.""" -from ipaddress import ip_address - -from supervisor.utils import check_port - - -def test_exists_open_port(): - """Test a exists network port.""" - assert check_port(ip_address("8.8.8.8"), 53) - - -def test_not_exists_port(): - """Test a not exists network service.""" - assert not check_port(ip_address("192.0.2.1"), 53) +"""Check ports.""" +from ipaddress import ip_address + +from supervisor.coresys import CoreSys +from supervisor.utils import check_port + + +async def test_exists_open_port(coresys: CoreSys): + """Test a exists network port.""" + assert await check_port(ip_address("8.8.8.8"), 53) + + +async def test_not_exists_port(coresys: CoreSys): + """Test a not exists network service.""" + assert not await check_port(ip_address("192.0.2.1"), 53)