35
35
from typing import Tuple
36
36
from typing import Union
37
37
38
+ try :
39
+ import importlib .metadata as importlib_metadata
40
+ except ModuleNotFoundError :
41
+ import importlib_metadata
42
+
38
43
from launch import Action
39
44
from launch import LaunchContext
40
45
from launch import SomeSubstitutionsType
52
57
from launch_ros .utilities import normalize_parameters
53
58
from launch_ros .utilities import normalize_remap_rules
54
59
from launch_ros .utilities import prefix_namespace
60
+ from launch_ros .utilities import plugin_support
55
61
56
62
from rclpy .validate_namespace import validate_namespace
57
63
from rclpy .validate_node_name import validate_node_name
64
70
from ..remap_rule_type import SomeRemapRules
65
71
66
72
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
+
67
115
class Node :
68
116
"""Describes a ROS node."""
69
117
@@ -146,6 +194,7 @@ def __init__(
146
194
self .__substitutions_performed = False
147
195
148
196
self .__logger = launch .logging .get_logger (__name__ )
197
+ self .__extensions = get_extensions (self .__logger )
149
198
150
199
@property
151
200
def node_name (self ):
@@ -218,14 +267,14 @@ def _get_parameter_rule(self, param: 'Parameter', context: LaunchContext):
218
267
return f'{ self .__expanded_node_name } :{ name } :={ yaml .dump (value )} '
219
268
220
269
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 )
222
271
223
272
# Prepare any traits which may be defined for this node
224
273
if self .__traits is not None :
225
274
for trait in self .__traits :
226
275
trait .prepare (self , context , action )
227
276
228
- def _perform_substitutions (self , context : LaunchContext , cmd : List ) -> None :
277
+ def _perform_substitutions (self , context : LaunchContext , cmd : List , action : Action ) -> None :
229
278
try :
230
279
if self .__substitutions_performed :
231
280
# 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:
332
381
ros_specific_arguments ['ns' ] = '{}__ns:={}' .format (
333
382
original_name_prefix , self .__expanded_node_namespace
334
383
)
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 )
335
392
context .extend_locals ({'ros_specific_arguments' : ros_specific_arguments })
336
393
337
394
if self .is_node_name_fully_specified ():
@@ -343,3 +400,56 @@ def _perform_substitutions(self, context: LaunchContext, cmd: List) -> None:
343
400
'there are now at least {} nodes with the name {} created within this '
344
401
'launch context' .format (node_name_count , self .node_name )
345
402
)
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