Skip to content

Commit 55ca523

Browse files
authored
Support non-interactive launch.LaunchService runs (#475)
Signed-off-by: Michel Hidalgo <michel@ekumenlabs.com>
1 parent f6d6e1c commit 55ca523

File tree

5 files changed

+79
-3
lines changed

5 files changed

+79
-3
lines changed

launch/launch/actions/execute_process.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -577,9 +577,10 @@ def __flush_buffers(self, event, context):
577577
self.__stderr_buffer.truncate(0)
578578

579579
def __on_shutdown(self, event: Event, context: LaunchContext) -> Optional[SomeActionsType]:
580+
due_to_sigint = cast(Shutdown, event).due_to_sigint
580581
return self._shutdown_process(
581582
context,
582-
send_sigint=(not cast(Shutdown, event).due_to_sigint),
583+
send_sigint=not due_to_sigint or context.noninteractive,
583584
)
584585

585586
def __get_shutdown_timer_actions(self) -> List[Action]:

launch/launch/launch_context.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,21 @@
3333
class LaunchContext:
3434
"""Runtime context used by various launch entities when being visited or executed."""
3535

36-
def __init__(self, *, argv: Optional[Iterable[Text]] = None) -> None:
36+
def __init__(
37+
self,
38+
*,
39+
argv: Optional[Iterable[Text]] = None,
40+
noninteractive: bool = False
41+
) -> None:
3742
"""
3843
Create a LaunchContext.
3944
4045
:param: argv stored in the context for access by the entities, None results in []
46+
:param: noninteractive if True (not default), this service will assume it has
47+
no terminal associated e.g. it is being executed from a non interactive script
4148
"""
4249
self.__argv = argv if argv is not None else []
50+
self.__noninteractive = noninteractive
4351

4452
self._event_queue = asyncio.Queue() # type: asyncio.Queue
4553
self._event_handlers = collections.deque() # type: collections.deque
@@ -63,6 +71,11 @@ def argv(self):
6371
"""Getter for argv."""
6472
return self.__argv
6573

74+
@property
75+
def noninteractive(self):
76+
"""Getter for noninteractive."""
77+
return self.__noninteractive
78+
6679
def _set_is_shutdown(self, state: bool) -> None:
6780
self.__is_shutdown = state
6881

launch/launch/launch_service.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def __init__(
5656
self,
5757
*,
5858
argv: Optional[Iterable[Text]] = None,
59+
noninteractive: bool = False,
5960
debug: bool = False
6061
) -> None:
6162
"""
@@ -67,6 +68,8 @@ def __init__(
6768
outside of the main-thread.
6869
6970
:param: argv stored in the context for access by the entities, None results in []
71+
:param: noninteractive if True (not default), this service will assume it has
72+
no terminal associated e.g. it is being executed from a non interactive script
7073
:param: debug if True (not default), asyncio the logger are seutp for debug
7174
"""
7275
# Setup logging and debugging.
@@ -82,7 +85,7 @@ def __init__(
8285
install_signal_handlers()
8386

8487
# Setup context and register a built-in event handler for bootstrapping.
85-
self.__context = LaunchContext(argv=self.__argv)
88+
self.__context = LaunchContext(argv=self.__argv, noninteractive=noninteractive)
8689
self.__context.register_event_handler(OnIncludeLaunchDescription())
8790
self.__context.register_event_handler(OnShutdown(on_shutdown=self.__on_shutdown))
8891

launch/test/launch/test_execute_process.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,20 @@
1515
"""Tests for the ExecuteProcess Action."""
1616

1717
import os
18+
import platform
19+
import signal
1820
import sys
1921

2022
from launch import LaunchDescription
2123
from launch import LaunchService
24+
from launch.actions.emit_event import EmitEvent
2225
from launch.actions.execute_process import ExecuteProcess
2326
from launch.actions.opaque_function import OpaqueFunction
27+
from launch.actions.register_event_handler import RegisterEventHandler
2428
from launch.actions.shutdown_action import Shutdown
2529
from launch.actions.timer_action import TimerAction
30+
from launch.event_handlers.on_process_start import OnProcessStart
31+
from launch.events.shutdown import Shutdown as ShutdownEvent
2632

2733
import pytest
2834

@@ -88,6 +94,50 @@ def on_exit_function(context):
8894
assert on_exit_function.called
8995

9096

97+
def test_execute_process_shutdown():
98+
"""Test shutting down a process in (non)interactive settings."""
99+
def on_exit(event, ctx):
100+
on_exit.returncode = event.returncode
101+
102+
def generate_launch_description():
103+
process_action = ExecuteProcess(
104+
cmd=[sys.executable, '-c', 'import signal; signal.pause()'],
105+
sigterm_timeout='1', # shorten timeouts
106+
on_exit=on_exit
107+
)
108+
# Launch process and emit shutdown event as if
109+
# launch had received a SIGINT
110+
return LaunchDescription([
111+
process_action,
112+
RegisterEventHandler(event_handler=OnProcessStart(
113+
target_action=process_action,
114+
on_start=[
115+
EmitEvent(event=ShutdownEvent(
116+
reason='none',
117+
due_to_sigint=True
118+
))
119+
]
120+
))
121+
])
122+
123+
ls = LaunchService(noninteractive=True)
124+
ls.include_launch_description(generate_launch_description())
125+
assert 0 == ls.run()
126+
if platform.system() != 'Windows':
127+
assert on_exit.returncode == -signal.SIGINT # Got SIGINT
128+
else:
129+
assert on_exit.returncode != 0 # Process terminated
130+
131+
ls = LaunchService() # interactive
132+
ls.include_launch_description(generate_launch_description())
133+
assert 0 == ls.run()
134+
if platform.system() != 'Windows':
135+
# Assume interactive Ctrl+C (i.e. SIGINT to process group)
136+
assert on_exit.returncode == -signal.SIGTERM # Got SIGTERM
137+
else:
138+
assert on_exit.returncode != 0 # Process terminated
139+
140+
91141
def test_execute_process_with_respawn():
92142
"""Test launching a process with a respawn and respawn_delay attribute."""
93143
def on_exit_callback(event, context):

launch/test/launch/test_launch_context.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@ def test_launch_context_get_argv():
3838
assert lc.argv == []
3939

4040

41+
def test_launch_context_get_noninteractive():
42+
"""Test the getting of noninteractive flag in the LaunchContext class."""
43+
lc = LaunchContext(noninteractive=True)
44+
assert lc.noninteractive
45+
46+
lc = LaunchContext()
47+
assert not lc.noninteractive
48+
49+
4150
def test_launch_context_get_set_asyncio_loop():
4251
"""Test the getting and settings for asyncio_loop in the LaunchContext class."""
4352
lc = LaunchContext()

0 commit comments

Comments
 (0)