Skip to content

Commit 9a44ebe

Browse files
Provide copy of launch configs to TimerAction's entities (ros2#836) (ros2#878)
Signed-off-by: Christophe Bedard <[email protected]> Co-authored-by: Christophe Bedard <[email protected]>
1 parent e9ee893 commit 9a44ebe

File tree

2 files changed

+53
-3
lines changed

2 files changed

+53
-3
lines changed

launch/launch/actions/timer_action.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
import launch.logging
3131

3232
from .opaque_function import OpaqueFunction
33+
from .pop_launch_configurations import PopLaunchConfigurations
34+
from .push_launch_configurations import PushLaunchConfigurations
35+
from .reset_launch_configurations import ResetLaunchConfigurations
3336

3437
from ..action import Action
3538
from ..event_handler import EventHandler
@@ -54,6 +57,11 @@ class TimerAction(Action):
5457
Action that defers other entities until a period of time has passed, unless canceled.
5558
5659
All timers are "one-shot", in that they only fire one time and never again.
60+
61+
Entities executed after the given period of time can access the launch configurations that
62+
exist at the time that the timer action executed, but changes made by them will not persist.
63+
This is similar to grouping the entities in a :class:`launch.actions.GroupAction` with
64+
``scoped=True``.
5765
"""
5866

5967
def __init__(
@@ -84,6 +92,7 @@ def __init__(
8492
self.__period = type_utils.normalize_typed_substitution(period, float)
8593
self.__actions = actions
8694
self.__context_locals: Dict[Text, Any] = {}
95+
self.__context_launch_configuration: Dict[Any, Any] = {}
8796
self._completed_future: Optional[asyncio.Future] = None
8897
self.__canceled = False
8998
self._canceled_future: Optional[asyncio.Future] = None
@@ -139,7 +148,14 @@ def describe_conditional_sub_entities(self) -> List[Tuple[
139148
def handle(self, context: LaunchContext) -> Optional[SomeEntitiesType]:
140149
"""Handle firing of timer."""
141150
context.extend_locals(self.__context_locals)
142-
return self.__actions
151+
# Reset the launch configurations to the state they were in when the timer action was
152+
# executed, and make sure to push and pop them so that the changes don't persist and leak
153+
return [
154+
PushLaunchConfigurations(),
155+
ResetLaunchConfigurations(self.__context_launch_configuration),
156+
*self.__actions,
157+
PopLaunchConfigurations(),
158+
]
143159

144160
def cancel(self) -> None:
145161
"""
@@ -189,8 +205,11 @@ def execute(self, context: LaunchContext) -> Optional[List[LaunchDescriptionEnti
189205
))
190206
setattr(context, '_TimerAction__event_handler_has_been_installed', True)
191207

192-
# Capture the current context locals so the yielded actions can make use of them too.
193-
self.__context_locals = dict(context.get_locals_as_dict()) # Capture a copy
208+
# Capture the current context locals and launch configuration so the yielded actions can
209+
# make use of them too.
210+
# Make sure to capture copies
211+
self.__context_locals = dict(context.get_locals_as_dict())
212+
self.__context_launch_configuration = context.launch_configurations.copy()
194213
context.asyncio_loop.create_task(self._wait_to_fire_event(context))
195214

196215
# By default, the 'shutdown' event will cause timers to cancel so they don't hold up the

launch/test/launch/test_timer_action.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import launch
2020
import launch.actions
2121
import launch.event_handlers
22+
import launch.substitutions
2223

2324

2425
def test_multiple_launch_with_timers():
@@ -153,3 +154,33 @@ def test_timer_can_block_preemption():
153154
assert len(shutdown_reasons) == 2 # Should see 'shutdown' event twice because
154155
assert shutdown_reasons[0].reason == 'fast shutdown'
155156
assert shutdown_reasons[1].reason == 'slow shutdown'
157+
158+
159+
def test_timer_action_launch_configurations():
160+
# The timer action's entities should have access to the launch configurations at the time the
161+
# timer action executed
162+
ld = launch.LaunchDescription([
163+
launch.actions.GroupAction(
164+
# Causes the launch configurations to be reset after the timer action executes, which
165+
# would cause 'launch_arg' to not exist
166+
scoped=True,
167+
actions=[
168+
launch.actions.SetLaunchConfiguration('launch_arg', 'launch_arg_value'),
169+
launch.actions.TimerAction(
170+
period=1.0,
171+
actions=[
172+
launch.actions.LogInfo(
173+
msg=launch.substitutions.LaunchConfiguration('launch_arg'),
174+
),
175+
],
176+
),
177+
],
178+
),
179+
])
180+
181+
ls = launch.LaunchService()
182+
ls.include_launch_description(ld)
183+
assert 0 == ls.run()
184+
# However, we do not want the timer action's entities to affect the context, e.g., leak launch
185+
# configurations out of the GroupAction in this case
186+
assert 'launch_arg' not in ls.context.launch_configurations

0 commit comments

Comments
 (0)