Skip to content

Commit 1566309

Browse files
Roger Strainmlanting
authored andcommitted
Refactor Node action into description classes
Distro A; OPSEC #4584 Signed-off-by: Roger Strain <[email protected]>
1 parent 6410f6c commit 1566309

File tree

9 files changed

+527
-243
lines changed

9 files changed

+527
-243
lines changed

launch_ros/launch_ros/actions/node.py

Lines changed: 24 additions & 237 deletions
Original file line numberDiff line numberDiff line change
@@ -14,57 +14,33 @@
1414

1515
"""Module for the Node action."""
1616

17-
import os
18-
import pathlib
19-
from tempfile import NamedTemporaryFile
2017
from typing import Dict
2118
from typing import Iterable
2219
from typing import List
2320
from typing import Optional
2421
from typing import Text # noqa: F401
2522
from typing import Tuple # noqa: F401
26-
from typing import Union
2723

2824
try:
2925
import importlib.metadata as importlib_metadata
3026
except ModuleNotFoundError:
3127
import importlib_metadata
3228

33-
from launch.action import Action
29+
from launch.actions import ExecuteLocal
3430
from launch.actions import ExecuteProcess
3531
from launch.frontend import Entity
3632
from launch.frontend import expose_action
3733
from launch.frontend import Parser
3834
from launch.frontend.type_utils import get_data_type_from_identifier
3935

4036
from launch.launch_context import LaunchContext
41-
import launch.logging
4237
from launch.some_substitutions_type import SomeSubstitutionsType
43-
from launch.substitutions import LocalSubstitution
44-
from launch.utilities import ensure_argument_type
45-
from launch.utilities import normalize_to_list_of_substitutions
46-
from launch.utilities import perform_substitutions
4738

39+
from launch_ros.descriptions import Node as NodeDescription
40+
from launch_ros.descriptions import ParameterFile
41+
from launch_ros.descriptions import RosExecutable
4842
from launch_ros.parameters_type import SomeParameters
4943
from launch_ros.remap_rule_type import SomeRemapRules
50-
from launch_ros.substitutions import ExecutableInPackage
51-
from launch_ros.utilities import add_node_name
52-
from launch_ros.utilities import evaluate_parameters
53-
from launch_ros.utilities import get_node_name_count
54-
from launch_ros.utilities import make_namespace_absolute
55-
from launch_ros.utilities import normalize_parameters
56-
from launch_ros.utilities import normalize_remap_rules
57-
from launch_ros.utilities import plugin_support
58-
from launch_ros.utilities import prefix_namespace
59-
60-
61-
from rclpy.validate_namespace import validate_namespace
62-
from rclpy.validate_node_name import validate_node_name
63-
64-
import yaml
65-
66-
from ..descriptions import Parameter
67-
from ..descriptions import ParameterFile
6844

6945

7046
class NodeActionExtension:
@@ -110,12 +86,9 @@ def prepare_for_execute(self, context, ros_specific_arguments, node_action):
11086

11187

11288
@expose_action('node')
113-
class Node(ExecuteProcess):
89+
class Node(ExecuteLocal):
11490
"""Action that executes a ROS node."""
11591

116-
UNSPECIFIED_NODE_NAME = '<node_name_unspecified>'
117-
UNSPECIFIED_NODE_NAMESPACE = '<node_namespace_unspecified>'
118-
11992
def __init__(
12093
self, *,
12194
executable: SomeSubstitutionsType,
@@ -198,46 +171,20 @@ def __init__(
198171
:param: ros_arguments list of ROS arguments for the node
199172
:param: arguments list of extra arguments for the node
200173
"""
201-
if package is not None:
202-
cmd = [ExecutableInPackage(package=package, executable=executable)]
203-
else:
204-
cmd = [executable]
205-
cmd += [] if arguments is None else arguments
206-
cmd += [] if ros_arguments is None else ['--ros-args'] + ros_arguments
207-
# Reserve space for ros specific arguments.
208-
# The substitutions will get expanded when the action is executed.
209-
cmd += ['--ros-args'] # Prepend ros specific arguments with --ros-args flag
210-
if name is not None:
211-
cmd += ['-r', LocalSubstitution(
212-
"ros_specific_arguments['name']", description='node name')]
213-
if parameters is not None:
214-
ensure_argument_type(parameters, (list), 'parameters', 'Node')
215-
# All elements in the list are paths to files with parameters (or substitutions that
216-
# evaluate to paths), or dictionaries of parameters (fields can be substitutions).
217-
normalized_params = normalize_parameters(parameters)
218-
# Forward 'exec_name' as to ExecuteProcess constructor
219-
kwargs['name'] = exec_name
220-
super().__init__(cmd=cmd, **kwargs)
221-
self.__package = package
222-
self.__node_executable = executable
223-
self.__node_name = name
224-
self.__node_namespace = namespace
225-
self.__parameters = [] if parameters is None else normalized_params
226-
self.__remappings = [] if remappings is None else list(normalize_remap_rules(remappings))
227-
self.__ros_arguments = ros_arguments
228-
self.__arguments = arguments
229-
230-
self.__expanded_node_name = self.UNSPECIFIED_NODE_NAME
231-
self.__expanded_node_namespace = self.UNSPECIFIED_NODE_NAMESPACE
232-
self.__expanded_parameter_arguments = None # type: Optional[List[Tuple[Text, bool]]]
233-
self.__final_node_name = None # type: Optional[Text]
234-
self.__expanded_remappings = None # type: Optional[List[Tuple[Text, Text]]]
235-
236-
self.__substitutions_performed = False
237-
238-
self.__logger = launch.logging.get_logger(__name__)
239-
174+
self.__node_desc = NodeDescription(node_name=name, node_namespace=namespace,
175+
parameters=parameters, remappings=remappings,
176+
arguments=arguments)
177+
self.__ros_exec = RosExecutable(package=package, executable=executable,
178+
nodes=[self.__node_desc])
240179
self.__extensions = get_extensions(self.__logger)
180+
super().__init__(process_description=self.__ros_exec, **kwargs)
181+
182+
def _perform_substitutions(self, lc: LaunchContext):
183+
self.__node_desc.prepare(lc, self.__ros_exec)
184+
185+
def is_node_name_fully_specified(self):
186+
return self.__node_desc.is_node_name_fully_specified()
187+
241188

242189
@staticmethod
243190
def parse_nested_parameters(params, parser):
@@ -306,7 +253,7 @@ def get_nested_dictionary_from_nested_key_value_pairs(params):
306253
def parse(cls, entity: Entity, parser: Parser):
307254
"""Parse node."""
308255
# See parse method of `ExecuteProcess`
309-
_, kwargs = super().parse(entity, parser, ignore=['cmd'])
256+
_, kwargs = ExecuteProcess.parse(entity, parser, ignore=['cmd'])
310257
args = entity.get_attr('args', optional=True)
311258
if args is not None:
312259
kwargs['arguments'] = super()._parse_cmdline(args, parser)
@@ -348,187 +295,27 @@ def parse(cls, entity: Entity, parser: Parser):
348295
@property
349296
def node_package(self):
350297
"""Getter for node_package."""
351-
return self.__package
298+
return self.__node_exec.package
352299

353300
@property
354301
def node_executable(self):
355302
"""Getter for node_executable."""
356-
return self.__node_executable
303+
return self.__node_exec.executable
357304

358305
@property
359306
def node_name(self):
360307
"""Getter for node_name."""
361-
if self.__final_node_name is None:
362-
raise RuntimeError("cannot access 'node_name' before executing action")
363-
return self.__final_node_name
364-
365-
def is_node_name_fully_specified(self):
366-
keywords = (self.UNSPECIFIED_NODE_NAME, self.UNSPECIFIED_NODE_NAMESPACE)
367-
return all(x not in self.node_name for x in keywords)
368-
369-
def _create_params_file_from_dict(self, params):
370-
with NamedTemporaryFile(mode='w', prefix='launch_params_', delete=False) as h:
371-
param_file_path = h.name
372-
param_dict = {
373-
self.node_name if self.is_node_name_fully_specified() else '/**':
374-
{'ros__parameters': params}
375-
}
376-
yaml.dump(param_dict, h, default_flow_style=False)
377-
return param_file_path
378-
379-
def _get_parameter_rule(self, param: 'Parameter', context: LaunchContext):
380-
name, value = param.evaluate(context)
381-
return f'{name}:={yaml.dump(value)}'
382-
383-
def _perform_substitutions(self, context: LaunchContext) -> None:
384-
# Here to avoid cyclic import
385-
from ..descriptions import Parameter
386-
try:
387-
if self.__substitutions_performed:
388-
# This function may have already been called by a subclass' `execute`, for example.
389-
return
390-
self.__substitutions_performed = True
391-
if self.__node_name is not None:
392-
self.__expanded_node_name = perform_substitutions(
393-
context, normalize_to_list_of_substitutions(self.__node_name))
394-
validate_node_name(self.__expanded_node_name)
395-
self.__expanded_node_name.lstrip('/')
396-
expanded_node_namespace: Optional[Text] = None
397-
if self.__node_namespace is not None:
398-
expanded_node_namespace = perform_substitutions(
399-
context, normalize_to_list_of_substitutions(self.__node_namespace))
400-
base_ns = context.launch_configurations.get('ros_namespace', None)
401-
expanded_node_namespace = make_namespace_absolute(
402-
prefix_namespace(base_ns, expanded_node_namespace))
403-
if expanded_node_namespace is not None:
404-
self.__expanded_node_namespace = expanded_node_namespace
405-
cmd_extension = ['-r', LocalSubstitution("ros_specific_arguments['ns']")]
406-
self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension])
407-
validate_namespace(self.__expanded_node_namespace)
408-
except Exception:
409-
self.__logger.error(
410-
"Error while expanding or validating node name or namespace for '{}':"
411-
.format('package={}, executable={}, name={}, namespace={}'.format(
412-
self.__package,
413-
self.__node_executable,
414-
self.__node_name,
415-
self.__node_namespace,
416-
))
417-
)
418-
raise
419-
self.__final_node_name = prefix_namespace(
420-
self.__expanded_node_namespace, self.__expanded_node_name)
421-
422-
# Expand global parameters first,
423-
# so they can be overridden with specific parameters of this Node
424-
# The params_container list is expected to contain name-value pairs (tuples)
425-
# and/or strings representing paths to parameter files.
426-
params_container = context.launch_configurations.get('global_params', None)
427-
428-
if any(x is not None for x in (params_container, self.__parameters)):
429-
self.__expanded_parameter_arguments = []
430-
if params_container is not None:
431-
for param in params_container:
432-
if isinstance(param, tuple):
433-
name, value = param
434-
cmd_extension = ['-p', f'{name}:={value}']
435-
self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension])
436-
else:
437-
param_file_path = os.path.abspath(param)
438-
self.__expanded_parameter_arguments.append((param_file_path, True))
439-
cmd_extension = ['--params-file', f'{param_file_path}']
440-
assert os.path.isfile(param_file_path)
441-
self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension])
442-
443-
# expand parameters too
444-
if self.__parameters is not None:
445-
evaluated_parameters = evaluate_parameters(context, self.__parameters)
446-
for params in evaluated_parameters:
447-
is_file = False
448-
if isinstance(params, dict):
449-
param_argument = self._create_params_file_from_dict(params)
450-
is_file = True
451-
assert os.path.isfile(param_argument)
452-
elif isinstance(params, pathlib.Path):
453-
param_argument = str(params)
454-
is_file = True
455-
elif isinstance(params, Parameter):
456-
param_argument = self._get_parameter_rule(params, context)
457-
else:
458-
raise RuntimeError('invalid normalized parameters {}'.format(repr(params)))
459-
if is_file and not os.path.isfile(param_argument):
460-
self.__logger.warning(
461-
'Parameter file path is not a file: {}'.format(param_argument),
462-
)
463-
continue
464-
self.__expanded_parameter_arguments.append((param_argument, is_file))
465-
cmd_extension = ['--params-file' if is_file else '-p', f'{param_argument}']
466-
self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension])
467-
# expand remappings too
468-
global_remaps = context.launch_configurations.get('ros_remaps', None)
469-
if global_remaps or self.__remappings:
470-
self.__expanded_remappings = []
471-
if global_remaps:
472-
self.__expanded_remappings.extend(global_remaps)
473-
if self.__remappings:
474-
self.__expanded_remappings.extend([
475-
(perform_substitutions(context, src), perform_substitutions(context, dst))
476-
for src, dst in self.__remappings
477-
])
478-
if self.__expanded_remappings:
479-
cmd_extension = []
480-
for src, dst in self.__expanded_remappings:
481-
cmd_extension.extend(['-r', f'{src}:={dst}'])
482-
self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension])
483-
484-
def execute(self, context: LaunchContext) -> Optional[List[Action]]:
485-
"""
486-
Execute the action.
487-
488-
Delegated to :meth:`launch.actions.ExecuteProcess.execute`.
489-
"""
490-
self._perform_substitutions(context)
491-
# Prepare the ros_specific_arguments list and add it to the context so that the
492-
# LocalSubstitution placeholders added to the the cmd can be expanded using the contents.
493-
ros_specific_arguments: Dict[str, Union[str, List[str]]] = {}
494-
if self.__node_name is not None:
495-
ros_specific_arguments['name'] = '__node:={}'.format(self.__expanded_node_name)
496-
if self.__expanded_node_namespace != '':
497-
ros_specific_arguments['ns'] = '__ns:={}'.format(self.__expanded_node_namespace)
498-
499-
# Give extensions a chance to prepare for execution
500-
for extension in self.__extensions.values():
501-
cmd_extension, ros_specific_arguments = extension.prepare_for_execute(
502-
context,
503-
ros_specific_arguments,
504-
self
505-
)
506-
self.cmd.extend(cmd_extension)
507-
508-
context.extend_locals({'ros_specific_arguments': ros_specific_arguments})
509-
ret = super().execute(context)
510-
511-
if self.is_node_name_fully_specified():
512-
add_node_name(context, self.node_name)
513-
node_name_count = get_node_name_count(context, self.node_name)
514-
if node_name_count > 1:
515-
execute_process_logger = launch.logging.get_logger(self.name)
516-
execute_process_logger.warning(
517-
'there are now at least {} nodes with the name {} created within this '
518-
'launch context'.format(node_name_count, self.node_name)
519-
)
520-
521-
return ret
308+
return self.__node_desc.node_name
522309

523310
@property
524311
def expanded_node_namespace(self):
525312
"""Getter for expanded_node_namespace."""
526-
return self.__expanded_node_namespace
313+
return self.__node_desc.expanded_node_namespace
527314

528315
@property
529316
def expanded_remapping_rules(self):
530317
"""Getter for expanded_remappings."""
531-
return self.__expanded_remappings
318+
return self.__node_desc.expanded_remappings
532319

533320

534321
def instantiate_extension(

launch_ros/launch_ros/descriptions/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,20 @@
1515
"""descriptions Module."""
1616

1717
from .composable_node import ComposableNode
18+
from .node import Node
19+
from .node_trait import NodeTrait
20+
from .ros_executable import RosExecutable
1821
from ..parameter_descriptions import Parameter
1922
from ..parameter_descriptions import ParameterFile
2023
from ..parameter_descriptions import ParameterValue
2124

2225

2326
__all__ = [
2427
'ComposableNode',
28+
'Node',
29+
'NodeTrait',
2530
'Parameter',
2631
'ParameterFile',
2732
'ParameterValue',
33+
'RosExecutable',
2834
]

0 commit comments

Comments
 (0)