From 324266a4e6df754d0ebb7ef1dc36964f0477b785 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 11 Mar 2024 10:48:11 -1000 Subject: [PATCH] Fix race in script stop that could cause async_stop to hang forever (#113089) --- homeassistant/helpers/script.py | 9 ++++++++- tests/helpers/test_script.py | 19 ++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index da710bd7e24..c246d625b3c 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -392,6 +392,7 @@ class _ScriptRun: self._context = context self._log_exceptions = log_exceptions self._step = -1 + self._started = False self._stop = asyncio.Event() self._stopped = asyncio.Event() self._conversation_response: str | None | UndefinedType = UNDEFINED @@ -420,6 +421,7 @@ class _ScriptRun: async def async_run(self) -> ScriptRunResult | None: """Run script.""" + self._started = True # Push the script to the script execution stack if (script_stack := script_stack_cv.get()) is None: script_stack = [] @@ -501,7 +503,12 @@ class _ScriptRun: async def async_stop(self) -> None: """Stop script run.""" self._stop.set() - await self._stopped.wait() + # If the script was never started + # the stopped event will never be + # set because the script will never + # start running + if self._started: + await self._stopped.wait() def _handle_exception( self, exception: Exception, continue_on_error: bool, log_exceptions: bool diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index b58b03fa9a9..114a90d39fc 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -4344,7 +4344,7 @@ async def test_script_mode_queued_cancel(hass: HomeAssistant) -> None: await task2 assert script_obj.is_running - assert script_obj.runs == 1 + assert script_obj.runs == 2 with pytest.raises(asyncio.CancelledError): task1.cancel() @@ -5621,3 +5621,20 @@ async def test_conversation_response_not_set_subscript_if( "1/if/condition/0": [{"result": {"result": var == 1, "entities": []}}], } assert_action_trace(expected_trace) + + +async def test_stopping_run_before_starting( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test stopping a script run before its started.""" + sequence = cv.SCRIPT_SCHEMA( + [ + {"wait_template": "{{ 'on' == 'off' }}"}, + ] + ) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + # Tested directly because we are checking for a race in the internals + # where the script is stopped before it is started. Previously this + # would hang indefinitely. + run = script._ScriptRun(hass, script_obj, {}, None, True) + await run.async_stop()