From dddcb8e2999b02eacd3d46648672cea303a69f1f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 4 Aug 2020 16:59:19 -1000 Subject: [PATCH] Add a 60s timeout to shell_command to prevent processes from building up (#38491) If a process never ended, there was not timeout and they would build up in the background until Home Assistant crashed. --- .../components/shell_command/__init__.py | 19 +++++++++++++++- tests/components/shell_command/test_init.py | 22 ++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shell_command/__init__.py b/homeassistant/components/shell_command/__init__.py index dc9fd8769d6..bce980035dc 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -12,6 +12,8 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType DOMAIN = "shell_command" +COMMAND_TIMEOUT = 60 + _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( @@ -74,7 +76,22 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: ) process = await create_process - stdout_data, stderr_data = await process.communicate() + try: + stdout_data, stderr_data = await asyncio.wait_for( + process.communicate(), COMMAND_TIMEOUT + ) + except asyncio.TimeoutError: + _LOGGER.exception( + "Timed out running command: `%s`, after: %ss", cmd, COMMAND_TIMEOUT + ) + if process: + try: + await process.kill() + except TypeError: + pass + del process + + return if stdout_data: _LOGGER.debug( diff --git a/tests/components/shell_command/test_init.py b/tests/components/shell_command/test_init.py index 76f81ea72df..7019d22fac8 100644 --- a/tests/components/shell_command/test_init.py +++ b/tests/components/shell_command/test_init.py @@ -6,7 +6,7 @@ from typing import Tuple import unittest from homeassistant.components import shell_command -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component, setup_component from tests.async_mock import Mock, patch from tests.common import get_test_home_assistant @@ -178,3 +178,23 @@ class TestShellCommand(unittest.TestCase): self.hass.block_till_done() assert mock_output.call_count == 1 assert test_phrase.encode() + b"\n" == mock_output.call_args_list[0][0][-1] + + +async def test_do_no_run_forever(hass, caplog): + """Test subprocesses terminate after the timeout.""" + + with patch.object(shell_command, "COMMAND_TIMEOUT", 0.001): + assert await async_setup_component( + hass, + shell_command.DOMAIN, + {shell_command.DOMAIN: {"test_service": "sleep 10000"}}, + ) + await hass.async_block_till_done() + + await hass.services.async_call( + shell_command.DOMAIN, "test_service", blocking=True + ) + await hass.async_block_till_done() + + assert "Timed out" in caplog.text + assert "sleep 10000" in caplog.text