3535from typing import Tuple
3636from typing import Union
3737
38+ try :
39+ import importlib .metadata as importlib_metadata
40+ except ModuleNotFoundError :
41+ import importlib_metadata
42+
3843from launch import Action
3944from launch import LaunchContext
4045from launch import SomeSubstitutionsType
5257from launch_ros .utilities import normalize_parameters
5358from launch_ros .utilities import normalize_remap_rules
5459from launch_ros .utilities import prefix_namespace
60+ from launch_ros .utilities import plugin_support
5561
5662from rclpy .validate_namespace import validate_namespace
5763from rclpy .validate_node_name import validate_node_name
6470from ..remap_rule_type import SomeRemapRules
6571
6672
73+ class NodeActionExtension :
74+ """
75+ The extension point for launch_ros node action extensions.
76+
77+ The following properties must be defined:
78+ * `NAME` (will be set to the entry point name)
79+
80+ The following methods may be defined:
81+ * `command_extension`
82+ * `execute`
83+ """
84+
85+ NAME = None
86+ EXTENSION_POINT_VERSION = '0.1'
87+
88+ def __init__ (self ):
89+ super (NodeActionExtension , self ).__init__ ()
90+ plugin_support .satisfies_version (self .EXTENSION_POINT_VERSION , '^0.1' )
91+
92+ def prepare_for_execute (self , context , ros_specific_arguments , node_action ):
93+ """
94+ Perform any actions prior to the node's process being launched.
95+
96+ `context` is the context within which the launch is taking place,
97+ containing amongst other things the command line arguments provided by
98+ the user.
99+
100+ `ros_specific_arguments` is a dictionary of command line arguments that
101+ will be passed to the executable and are specific to ROS.
102+
103+ `node_action` is the Node action instance that is calling the
104+ extension.
105+
106+ This method must return a tuple of command line additions as a list of
107+ launch.substitutions.TextSubstitution objects, and
108+ `ros_specific_arguments` with any modifications made to it. If no
109+ modifications are made, it should return
110+ `[], ros_specific_arguments`.
111+ """
112+ return [], ros_specific_arguments
113+
114+
67115class Node :
68116 """Describes a ROS node."""
69117
@@ -146,6 +194,7 @@ def __init__(
146194 self .__substitutions_performed = False
147195
148196 self .__logger = launch .logging .get_logger (__name__ )
197+ self .__extensions = get_extensions (self .__logger )
149198
150199 @property
151200 def node_name (self ):
@@ -218,14 +267,14 @@ def _get_parameter_rule(self, param: 'Parameter', context: LaunchContext):
218267 return f'{ self .__expanded_node_name } :{ name } :={ yaml .dump (value )} '
219268
220269 def prepare (self , context : LaunchContext , executable : Executable , action : Action ) -> None :
221- self ._perform_substitutions (context , executable .cmd )
270+ self ._perform_substitutions (context , executable .cmd , action )
222271
223272 # Prepare any traits which may be defined for this node
224273 if self .__traits is not None :
225274 for trait in self .__traits :
226275 trait .prepare (self , context , action )
227276
228- def _perform_substitutions (self , context : LaunchContext , cmd : List ) -> None :
277+ def _perform_substitutions (self , context : LaunchContext , cmd : List , action : Action ) -> None :
229278 try :
230279 if self .__substitutions_performed :
231280 # This function may have already been called by a subclass' `execute`, for example.
@@ -332,6 +381,14 @@ def _perform_substitutions(self, context: LaunchContext, cmd: List) -> None:
332381 ros_specific_arguments ['ns' ] = '{}__ns:={}' .format (
333382 original_name_prefix , self .__expanded_node_namespace
334383 )
384+ # Give extensions a chance to prepare for execution
385+ for extension in self .__extensions .values ():
386+ cmd_extension , ros_specific_arguments = extension .prepare_for_execute (
387+ context ,
388+ ros_specific_arguments ,
389+ action
390+ )
391+ cmd .extend (cmd_extension )
335392 context .extend_locals ({'ros_specific_arguments' : ros_specific_arguments })
336393
337394 if self .is_node_name_fully_specified ():
@@ -343,3 +400,56 @@ def _perform_substitutions(self, context: LaunchContext, cmd: List) -> None:
343400 'there are now at least {} nodes with the name {} created within this '
344401 'launch context' .format (node_name_count , self .node_name )
345402 )
403+
404+
405+ def instantiate_extension (
406+ group_name ,
407+ extension_name ,
408+ extension_class ,
409+ extensions ,
410+ logger ,
411+ * ,
412+ unique_instance = False
413+ ):
414+ if not unique_instance and extension_class in extensions :
415+ return extensions [extension_name ]
416+ try :
417+ extension_instance = extension_class ()
418+ except plugin_support .PluginException as e : # noqa: F841
419+ logger .warning (
420+ f"Failed to instantiate '{ group_name } ' extension "
421+ f"'{ extension_name } ': { e } " )
422+ return None
423+ except Exception as e : # noqa: F841
424+ logger .error (
425+ f"Failed to instantiate '{ group_name } ' extension "
426+ f"'{ extension_name } ': { e } " )
427+ return None
428+ if not unique_instance :
429+ extensions [extension_name ] = extension_instance
430+ return extension_instance
431+
432+
433+ def get_extensions (logger ):
434+ group_name = 'launch_ros.node_action'
435+ entry_points = {}
436+ for entry_point in importlib_metadata .entry_points ().get (group_name , []):
437+ entry_points [entry_point .name ] = entry_point
438+ extension_types = {}
439+ for entry_point in entry_points :
440+ try :
441+ extension_type = entry_points [entry_point ].load ()
442+ except Exception as e : # noqa: F841
443+ logger .warning (f"Failed to load entry point '{ entry_point .name } ': { e } " )
444+ continue
445+ extension_types [entry_points [entry_point ].name ] = extension_type
446+
447+ extensions = {}
448+ for extension_name , extension_class in extension_types .items ():
449+ extension_instance = instantiate_extension (
450+ group_name , extension_name , extension_class , extensions , logger )
451+ if extension_instance is None :
452+ continue
453+ extension_instance .NAME = extension_name
454+ extensions [extension_name ] = extension_instance
455+ return extensions
0 commit comments