Skip to content

Commit ca47ab4

Browse files
committed
Update is_idle function to also consider async tasks outside of the main loop. Fixes #747
Signed-off-by: Cornelius Krupp <[email protected]>
1 parent 6dde20d commit ca47ab4

File tree

2 files changed

+52
-2
lines changed

2 files changed

+52
-2
lines changed

launch/launch/launch_service.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def __init__(
9494
# it being set to None by run() as it exits.
9595
self.__loop_from_run_thread_lock = threading.RLock()
9696
self.__loop_from_run_thread = None
97-
self.__this_task: Optional[asyncio.Future[None]] = None
97+
self.__this_task: Optional[asyncio.Task[None]] = None
9898

9999
# Used to indicate when shutdown() has been called.
100100
self.__shutting_down = False
@@ -156,7 +156,14 @@ def _prune_and_count_context_completion_futures(self) -> int:
156156
def _is_idle(self) -> bool:
157157
number_of_entity_future_pairs = self._prune_and_count_entity_future_pairs()
158158
number_of_entity_future_pairs += self._prune_and_count_context_completion_futures()
159-
return number_of_entity_future_pairs == 0 and self.__context._event_queue.empty()
159+
if self.event_loop is not None and self.__this_task is not None:
160+
tasks = asyncio.all_tasks(self.event_loop)
161+
tasks.remove(self.__this_task)
162+
else:
163+
tasks = set()
164+
return (number_of_entity_future_pairs == 0 and
165+
self.__context._event_queue.empty() and
166+
len(tasks) == 0)
160167

161168
@contextlib.contextmanager
162169
def _prepare_run_loop(

launch/test/launch/test_execute_local.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,46 @@ def test_execute_process_with_output_dictionary():
178178
ls = LaunchService()
179179
ls.include_launch_description(ld)
180180
assert 0 == ls.run()
181+
182+
183+
def test_execute_process_with_shutdown_on_error():
184+
exited_processes = 0
185+
186+
def on_exit(event, context):
187+
nonlocal exited_processes
188+
print(event, context)
189+
exited_processes += 1
190+
191+
executable_1 = ExecuteLocal(
192+
process_description=Executable(
193+
cmd=[sys.executable, '-c', 'while True: pass']
194+
),
195+
output={'stdout': 'screen', 'stderr': 'screen'},
196+
on_exit=on_exit,
197+
)
198+
executable_2 = ExecuteLocal(
199+
process_description=Executable(
200+
cmd=[sys.executable, '-c', 'while True: pass']
201+
),
202+
output={'stdout': 'screen', 'stderr': 'screen'},
203+
on_exit=on_exit,
204+
)
205+
206+
# It's slightly tricky to coerce the standard implementation to fail in
207+
# this way. However, launch_ros's Node class can fail similar to this and
208+
# this case therefore needs to be handled correctly.
209+
class ExecutableThatFails(ExecuteLocal):
210+
def execute(self, context):
211+
raise Exception('Execute Local failed')
212+
213+
executable_invalid = ExecutableThatFails(
214+
process_description=Executable(
215+
cmd=['fake_process_that_doesnt_exists']
216+
),
217+
output={'stdout': 'screen', 'stderr': 'screen'},
218+
)
219+
ld = LaunchDescription([executable_1, executable_2, executable_invalid])
220+
ls = LaunchService()
221+
ls.include_launch_description(ld)
222+
assert ls.run() == 1
223+
assert exited_processes == 2

0 commit comments

Comments
 (0)