This commit is contained in:
Ivan
2022-04-05 11:42:28 +03:00
commit 6dc0eb0fcf
5565 changed files with 1200500 additions and 0 deletions

View File

@@ -0,0 +1,229 @@
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Changelog for package roslaunch
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1.12.14 (2018-08-23)
--------------------
* fix "pass_all_args" for roslaunch-check, add nosetest (`#1368 <https://github.com/ros/ros_comm/issues/1368>`_)
* add substitution when loading yaml files (`#1354 <https://github.com/ros/ros_comm/issues/1354>`_)
1.12.13 (2018-02-21)
--------------------
* add process listeners to XML RPC server (`#1319 <https://github.com/ros/ros_comm/issues/1319>`_)
* make master process explicitly 'required' for parent launch (`#1228 <https://github.com/ros/ros_comm/issues/1228>`_)
1.12.12 (2017-11-16)
--------------------
1.12.11 (2017-11-07)
--------------------
1.12.10 (2017-11-06)
--------------------
1.12.9 (2017-11-06)
-------------------
1.12.8 (2017-11-06)
-------------------
* fix parameter leaking into sibling scopes (`#1158 <https://github.com/ros/ros_comm/issues/1158>`_)
* remove mention of rosmake from error message (`#1140 <https://github.com/ros/ros_comm/issues/1140>`_)
* only launch core nodes if master was launched by roslaunch (`#1098 <https://github.com/ros/ros_comm/pull/1098>`_)
* ensure pid file is removed on exit (`#1057 <https://github.com/ros/ros_comm/pull/1057>`_, `#1084 <https://github.com/ros/ros_comm/pull/1084>`_)
* ensure cwd exists (`#1031 <https://github.com/ros/ros_comm/pull/1031>`_)
* respect if/unless for roslaunch-check (`#998 <https://github.com/ros/ros_comm/pull/998>`_)
1.12.7 (2017-02-17)
-------------------
* improve error message for invalid tags (`#989 <https://github.com/ros/ros_comm/pull/989>`_)
* fix caching logic to improve performance (`#931 <https://github.com/ros/ros_comm/pull/931>`_)
1.12.6 (2016-10-26)
-------------------
* add USE_TEST_DEPENDENCIES option to roslaunch_add_file_check() (`#910 <https://github.com/ros/ros_comm/pull/910>`_)
1.12.5 (2016-09-30)
-------------------
1.12.4 (2016-09-19)
-------------------
1.12.3 (2016-09-17)
-------------------
* better naming for roslaunch check test results (`#897 <https://github.com/ros/ros_comm/pull/897>`_)
* support use_test_depends option for roslaunch-check (`#887 <https://github.com/ros/ros_comm/pull/887>`_)
* allow empty include (`#882 <https://github.com/ros/ros_comm/pull/882>`_)
* fix param command for Python 3 (`#840 <https://github.com/ros/ros_comm/pull/840>`_)
1.12.2 (2016-06-03)
-------------------
* support registering the same test multiple times with different arguments (`#814 <https://github.com/ros/ros_comm/pull/814>`_)
* fix passing multiple args to roslaunch_add_file_check (`#814 <https://github.com/ros/ros_comm/pull/814>`_)
1.12.1 (2016-04-18)
-------------------
* add support for Python expressions (`#784 <https://github.com/ros/ros_comm/pull/784>`_, `#793 <https://github.com/ros/ros_comm/pull/793>`_)
1.12.0 (2016-03-18)
-------------------
1.11.18 (2016-03-17)
--------------------
1.11.17 (2016-03-11)
--------------------
* improve roslaunch-check to not fail if recursive dependencies lack dependencies (`#730 <https://github.com/ros/ros_comm/pull/730>`_)
* add "pass_all_args" attribute to roslaunch "include" tag (`#710 <https://github.com/ros/ros_comm/pull/710>`_)
* fix a typo in unknown host error message (`#735 <https://github.com/ros/ros_comm/pull/735>`_)
* wait for param server to be available before trying to get param (`#711 <https://github.com/ros/ros_comm/pull/711>`_)
1.11.16 (2015-11-09)
--------------------
* add `-w` and `-t` options (`#687 <https://github.com/ros/ros_comm/pull/687>`_)
* fix missing minimum version for rospkg dependency (`#693 <https://github.com/ros/ros_comm/issues/693>`_)
1.11.15 (2015-10-13)
--------------------
* improve performance by reusing the rospack instance across nodes with the same default environment (`#682 <https://github.com/ros/ros_comm/pull/682>`_)
1.11.14 (2015-09-19)
--------------------
* add more information when test times out
1.11.13 (2015-04-28)
--------------------
1.11.12 (2015-04-27)
--------------------
1.11.11 (2015-04-16)
--------------------
1.11.10 (2014-12-22)
--------------------
* fix exception at roscore startup if python has IPv6 disabled (`#515 <https://github.com/ros/ros_comm/issues/515>`_)
* fix error handling (`#516 <https://github.com/ros/ros_comm/pull/516>`_)
* fix compatibility with paramiko 1.10.0 (`#498 <https://github.com/ros/ros_comm/pull/498>`_)
1.11.9 (2014-08-18)
-------------------
* fix usage of logger before it is initialized (`#490 <https://github.com/ros/ros_comm/issues/490>`_) (regression from 1.11.6)
1.11.8 (2014-08-04)
-------------------
* remove implicit rostest dependency and use rosunit instead (`#475 <https://github.com/ros/ros_comm/issues/475>`_)
* accept stdin input alongside files (`#472 <https://github.com/ros/ros_comm/issues/472>`_)
1.11.7 (2014-07-18)
-------------------
* fix the ROS_MASTER_URI environment variable logic on Windows (`#2 <https://github.com/windows/ros_comm/issues/2>`_)
1.11.6 (2014-07-10)
-------------------
* fix printing of non-ascii roslaunch parameters (`#454 <https://github.com/ros/ros_comm/issues/454>`_)
* add respawn_delay attribute to node tag in roslaunch (`#446 <https://github.com/ros/ros_comm/issues/446>`_)
* write traceback for exceptions in roslaunch to log file
1.11.5 (2014-06-24)
-------------------
1.11.4 (2014-06-16)
-------------------
* fix handling of if/unless attributes on args (`#437 <https://github.com/ros/ros_comm/issues/437>`_)
* improve parameter printing in roslaunch (`#89 <https://github.com/ros/ros_comm/issues/89>`_)
* Python 3 compatibility (`#426 <https://github.com/ros/ros_comm/issues/426>`_, `#427 <https://github.com/ros/ros_comm/issues/427>`_, `#429 <https://github.com/ros/ros_comm/issues/429>`_)
1.11.3 (2014-05-21)
-------------------
1.11.2 (2014-05-08)
-------------------
1.11.1 (2014-05-07)
-------------------
* fix roslaunch anonymous function to generate the same output for the same input (`#297 <https://github.com/ros/ros_comm/issues/297>`_)
* add doc attribute to roslaunch arg tags (`#379 <https://github.com/ros/ros_comm/issues/379>`_)
* print parameter values in roslaunch (`#89 <https://github.com/ros/ros_comm/issues/89>`_)
* add architecture_independent flag in package.xml (`#391 <https://github.com/ros/ros_comm/issues/391>`_)
1.11.0 (2014-03-04)
-------------------
* use catkin_install_python() to install Python scripts (`#361 <https://github.com/ros/ros_comm/issues/361>`_)
1.10.0 (2014-02-11)
-------------------
* add optional DEPENDENCIES argument to roslaunch_add_file_check()
* add explicit run dependency (`#347 <https://github.com/ros/ros_comm/issues/347>`_)
1.9.54 (2014-01-27)
-------------------
* add missing run/test dependencies on rosbuild to get ROS_ROOT environment variable
1.9.53 (2014-01-14)
-------------------
1.9.52 (2014-01-08)
-------------------
1.9.51 (2014-01-07)
-------------------
* fix roslaunch-check for unreleased wet dependencies (`#332 <https://github.com/ros/ros_comm/issues/332>`_)
1.9.50 (2013-10-04)
-------------------
* add option to disable terminal title setting
* fix roslaunch-check to handle more complex launch files
1.9.49 (2013-09-16)
-------------------
1.9.48 (2013-08-21)
-------------------
* update roslaunch to support ROS_NAMESPACE (`#58 <https://github.com/ros/ros_comm/issues/58>`_)
* make roslaunch relocatable (`ros/catkin#490 <https://github.com/ros/catkin/issues/490>`_)
* change roslaunch resolve order (`#256 <https://github.com/ros/ros_comm/issues/256>`_)
* fix roslaunch check script in install space (`#257 <https://github.com/ros/ros_comm/issues/257>`_)
1.9.47 (2013-07-03)
-------------------
* improve roslaunch completion to include launch file arguments (`#230 <https://github.com/ros/ros_comm/issues/230>`_)
* check for CATKIN_ENABLE_TESTING to enable configure without tests
1.9.46 (2013-06-18)
-------------------
* add CMake function roslaunch_add_file_check() (`#241 <https://github.com/ros/ros_comm/issues/241>`_)
1.9.45 (2013-06-06)
-------------------
* modified roslaunch $(find PKG) to consider path behind it for resolve strategy (`#233 <https://github.com/ros/ros_comm/pull/233>`_)
* add boolean attribute 'subst_value' to rosparam tag in launch files (`#218 <https://github.com/ros/ros_comm/issues/218>`_)
* add command line parameter to print out launch args
* fix missing import in arg_dump.py
1.9.44 (2013-03-21)
-------------------
* fix 'roslaunch --files' with non-unique anononymous ids (`#186 <https://github.com/ros/ros_comm/issues/186>`_)
* fix ROS_MASTER_URI for Windows
1.9.43 (2013-03-13)
-------------------
* implement process killer for Windows
1.9.42 (2013-03-08)
-------------------
* add option --skip-log-check (`#133 <https://github.com/ros/ros_comm/issues/133>`_)
* update API doc to list raised exceptions in config.py
* fix invocation of Python scripts under Windows (`#54 <https://github.com/ros/ros_comm/issues/54>`_)
1.9.41 (2013-01-24)
-------------------
* improve performance of $(find ...)
1.9.40 (2013-01-13)
-------------------
* fix 'roslaunch --pid=' when pointing to ROS_HOME but folder does not exist (`#43 <https://github.com/ros/ros_comm/issues/43>`_)
* fix 'roslaunch --pid=' to use shell expansion for the pid value (`#44 <https://github.com/ros/ros_comm/issues/44>`_)
1.9.39 (2012-12-29)
-------------------
* first public release for Groovy

View File

@@ -0,0 +1,27 @@
cmake_minimum_required(VERSION 2.8.3)
project(roslaunch)
find_package(catkin REQUIRED)
catkin_package(CFG_EXTRAS roslaunch-extras.cmake)
if(CMAKE_HOST_UNIX)
catkin_add_env_hooks(10.roslaunch SHELLS sh DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/env-hooks)
else()
catkin_add_env_hooks(10.roslaunch SHELLS bat DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/env-hooks)
endif()
catkin_python_setup()
install(FILES resources/roscore.xml
DESTINATION ${CATKIN_GLOBAL_ETC_DESTINATION}/ros)
# install example launch files
install(DIRECTORY resources
DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION})
# required for old rosbuild macro to be under PKGNAME/scripts
catkin_install_python(PROGRAMS scripts/roslaunch-check
DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}/scripts)
if(CATKIN_ENABLE_TESTING)
catkin_add_nosetests(test/unit)
endif()

View File

@@ -0,0 +1,64 @@
# generated from ros_comm/tools/roslaunch/cmake/roslaunch-extras.cmake.em
@[if DEVELSPACE]@
# set path to roslaunch-check script in develspace
set(roslaunch_check_script @(CMAKE_CURRENT_SOURCE_DIR)/scripts/roslaunch-check)
@[else]@
# set path to roslaunch-check script installspace
set(roslaunch_check_script ${roslaunch_DIR}/../scripts/roslaunch-check)
@[end if]@
#
# Check ROS launch files for validity as part of the unit tests.
#
# :param path: the path to a launch file or a directory containing launch files
# either relative to th current CMakeLists file or absolute
# :type url: string
# :param DEPENDENCIES: the targets which must be built before executing
# the test
# :type DEPENDENCIES: list of strings
# :param USE_TEST_DEPENDENCIES: beside run dependencies also consider test
# dependencies when checking if all dependencies of a launch file are present
# :type USE_TEST_DEPENDENCIES: option
# :param ARGV: arbitrary arguments in the form 'key=value'
# which will be set as environment variables
# :type ARGV: string
#
function(roslaunch_add_file_check path)
cmake_parse_arguments(_roslaunch "USE_TEST_DEPENDENCIES" "" "DEPENDENCIES" ${ARGN})
if(IS_ABSOLUTE ${path})
set(abspath ${path})
else()
set(abspath "${CMAKE_CURRENT_SOURCE_DIR}/${path}")
endif()
if(NOT EXISTS ${abspath})
message(FATAL_ERROR "roslaunch_add_file_check() path '${abspath}' was not found")
endif()
set(testname ${path})
# to support registering the same test with different ARGS
# append the args to the test name
if(_roslaunch_UNPARSED_ARGUMENTS)
get_filename_component(_ext ${testname} EXT)
get_filename_component(testname ${testname} NAME_WE)
foreach(arg ${_roslaunch_UNPARSED_ARGUMENTS})
string(REPLACE ":=" "_" arg_string "${arg}")
string(REPLACE "=" "_" arg_string "${arg_string}")
set(testname "${testname}__${arg_string}")
endforeach()
set(testname "${testname}${_ext}")
endif()
string(REPLACE "/" "_" testname ${testname})
set(output_path ${CATKIN_TEST_RESULTS_DIR}/${PROJECT_NAME})
set(cmd "${CMAKE_COMMAND} -E make_directory ${output_path}")
set(output_file_name "roslaunch-check_${testname}.xml")
string(REPLACE ";" " " _roslaunch_UNPARSED_ARGUMENTS "${_roslaunch_UNPARSED_ARGUMENTS}")
set(use_test_dependencies "")
if(${_roslaunch_USE_TEST_DEPENDENCIES})
set(use_test_dependencies " -t")
endif()
set(cmd ${cmd} "${roslaunch_check_script} -o '${output_path}/${output_file_name}'${use_test_dependencies} '${abspath}' ${_roslaunch_UNPARSED_ARGUMENTS}")
catkin_run_tests_target("roslaunch-check" ${testname} "${output_file_name}" COMMAND ${cmd} DEPENDENCIES ${_roslaunch_DEPENDENCIES})
endfunction()

View File

@@ -0,0 +1,5 @@
REM roslaunch/env-hooks/10.roslaunch.bat
if "%ROS_MASTER_URI%" == "" (
set ROS_MASTER_URI=http://localhost:11311
)

View File

@@ -0,0 +1,5 @@
# roslaunch/env-hooks/10.roslaunch.sh
if [ ! "$ROS_MASTER_URI" ]; then
export ROS_MASTER_URI=http://localhost:11311
fi

View File

@@ -0,0 +1,40 @@
<package>
<name>roslaunch</name>
<version>1.12.14</version>
<description>
roslaunch is a tool for easily launching multiple ROS <a
href="http://ros.org/wiki/Nodes">nodes</a> locally and remotely
via SSH, as well as setting parameters on the <a
href="http://ros.org/wiki/Parameter Server">Parameter
Server</a>. It includes options to automatically respawn processes
that have already died. roslaunch takes in one or more XML
configuration files (with the <tt>.launch</tt> extension) that
specify the parameters to set and nodes to launch, as well as the
machines that they should be run on.
</description>
<maintainer email="dthomas@osrfoundation.org">Dirk Thomas</maintainer>
<license>BSD</license>
<url>http://ros.org/wiki/roslaunch</url>
<author>Ken Conley</author>
<buildtool_depend version_gte="0.5.78">catkin</buildtool_depend>
<run_depend>python-paramiko</run_depend>
<run_depend version_gte="1.0.37">python-rospkg</run_depend>
<run_depend>python-yaml</run_depend>
<run_depend>rosclean</run_depend>
<run_depend>rosgraph_msgs</run_depend>
<run_depend>roslib</run_depend>
<run_depend version_gte="1.11.16">rosmaster</run_depend>
<run_depend>rosout</run_depend>
<run_depend>rosparam</run_depend>
<run_depend version_gte="1.13.3">rosunit</run_depend>
<test_depend>rosbuild</test_depend>
<export>
<rosdoc config="rosdoc.yaml"/>
<architecture_independent/>
</export>
</package>

View File

@@ -0,0 +1,3 @@
<launch>
<arg name="test_foo" />
</launch>

View File

@@ -0,0 +1,3 @@
<launch>
<param name="example_env_param" value="$(env VALUE)" />
</launch>

View File

@@ -0,0 +1,4 @@
<!-- NOTE: this requires that you 'rosmake roscpp_tutorials' -->
<launch>
<node name="talker" pkg="roscpp_tutorials" type="talker" launch-prefix="xterm -e gdb --args"/>
</launch>

View File

@@ -0,0 +1,17 @@
<launch>
<node name="talker" pkg="rospy" type="talker.py" />
<node name="listener" pkg="rospy" type="listener.py" />
<param name="param1" value="bar" />
<param name="param2" value="2"/>
<!-- you can set parameters in child namespaces -->
<param name="wg2/param3" value="a child namespace parameter" />
<group ns="wg2">
<param name="param4" value="4" />
<node name="talker" pkg="rospy" type="talker.py" />
<node name="listener" pkg="rospy" type="listener.py" />
</group>
</launch>

View File

@@ -0,0 +1,4 @@
<launch>
<!-- $(anon talker) creates an anonymous name for this node -->
<node name="$(anon talker)" pkg="rospy" type="talker.py" />
</launch>

View File

@@ -0,0 +1,46 @@
<launch>
<!--
Parameter Server parameters. You can omit the 'type' attribute if
value is unambiguous. Supported types are str, int, double, bool.
You can also specify the contents of a file instead using the
'textfile' or 'binfile' attributes.
-->
<param name="somestring1" value="bar2" />
<!-- force to string instead of integer -->
<param name="somestring2" value="10" type="str" />
<param name="someinteger1" value="1" type="int" />
<param name="someinteger2" value="2" />
<param name="somefloat1" value="3.14159" type="double" />
<param name="somefloat2" value="3.0" />
<!-- you can set parameters in child namespaces -->
<param name="wg/wgchildparam" value="a child namespace parameter" />
<group ns="wg2">
<param name="wg2childparam1" value="a child namespace parameter" />
<param name="wg2childparam2" value="a child namespace parameter" />
</group>
<!-- use rosparam for more complex types -->
<rosparam param="list">[1, 2, 3, 4]</rosparam>
<rosparam>
rp_key1: a
rp_key2: b
</rosparam>
<!-- use rosparam files -->
<group ns="rosparam">
<rosparam command="load" file="$(find rosparam)/example.yaml" />
</group>
<!-- upload the contents of a file as a param -->
<param name="configfile" textfile="$(find roslaunch)/resources/example.launch" />
<!-- upload the contents of a file as base64 binary as a param -->
<param name="binaryfile" binfile="$(find roslaunch)/resources/example.launch" />
<!-- upload the output of a command as a param. -->
</launch>

View File

@@ -0,0 +1,4 @@
<launch>
<arg name="test_foo" default="value_foo" />
<include file="$(find roslaunch)/resources/example-args.launch" pass_all_args="true" />
</launch>

View File

@@ -0,0 +1,17 @@
<!-- an example launch configuration that launches two demo nodes on a remote machine -->
<launch>
<group ns="local">
<node name="talker" pkg="rospy" type="talker.py" />
<node name="listener" pkg="rospy" type="listener.py" />
</group>
<!-- default only affects nodes defined later -->
<machine name="machine-1" default="true" address="foo.bar.com" env-loader="/opt/ros/fuerte/env.sh" user="whoami" ssh-port="22" />
<group ns="remote">
<node name="talker" pkg="rospy" type="talker.py" />
<node name="listener" pkg="rospy" type="listener.py" />
</group>
</launch>

View File

@@ -0,0 +1,73 @@
<launch>
<!-- localhost definition can be omitted. -->
<machine name="localhost" address="localhost" default="true" />
<machine name="localhost-altroot" address="localhost" env-loader="/opt/ros/fuerte/env.sh" />
<!--
Parameter Server parameters. You can omit the 'type' attribute if
value is unambiguous. Supported types are str, int, double, bool.
You can also specify the contents of a file instead using the
'textfile' or 'binfile' attributes.
-->
<param name="somestring1" value="bar" />
<!-- force to string instead of integer -->
<param name="somestring2" value="10" type="str" />
<param name="someinteger1" value="1" type="int" />
<param name="someinteger2" value="2" />
<param name="somefloat1" value="3.14159" type="double" />
<param name="somefloat2" value="3.0" />
<!-- you can set parameters in child namespaces -->
<param name="wg/childparam" value="a child namespace parameter" />
<!-- upload the contents of a file as a param -->
<param name="configfile" textfile="$(find roslaunch)/resources/example.launch" />
<!-- upload the contents of a file as base64 binary as a param -->
<param name="binaryfile" binfile="$(find roslaunch)/resources/example.launch" />
<!-- upload the output of a command as a param. -->
<param name="commandoutput" command="cat &quot;$(find roslaunch)/resources/example.launch&quot;" />
<!-- Group a collection of tags. ns attribute is optional -->
<group ns="wg">
<!-- remap applies to all future nodes in this group -->
<remap from="chatter" to="hello"/>
<node name="listener" pkg="rospy" type="listener.py" respawn="true" />
<node name="talker1" pkg="rospy" type="talker.py">
<env name="ENV_EXAMPLE" value="example value" />
<remap from="foo" to="bar"/>
<!-- params within node tags are equivalent to ~params.
You must set the 'name' attribute of a node to use this feature. -->
<param name="talker_param" value="a value" />
</node>
<node name="talker2" pkg="rospy" type="talker.py" />
</group>
<!-- import another roslaunch config file -->
<group ns="included">
<include file="$(find roslaunch)/resources/example-include.launch" />
</group>
<!-- more compact import syntax -->
<include ns="included2" file="$(find roslaunch)/resources/example-include.launch" />
<!-- Pass over an include and node with if-attributes that evaluate to false. -->
<arg name="false_arg" value="false" />
<include if="$(arg false_arg)" file="does/not/exist.launch" />
<node if="$(arg false_arg)" pkg="doesnt_exist" type="nope" name="nope" />
<!-- Pass over a group with an unless-attribute that evaluate to true. -->
<arg name="true_arg" value="true" />
<group unless="$(arg true_arg)">
<include file="does/not/exist.launch" />
<node pkg="doesnt_exist" type="nope" name="nope" />
</group>
</launch>

View File

@@ -0,0 +1,13 @@
<!--
ROS Core Stack definition
Before making any modifications to this file, please read:
http://ros.org/wiki/roscore
-->
<launch>
<group ns="/">
<param name="rosversion" command="rosversion roslaunch" />
<param name="rosdistro" command="rosversion -d" />
<node pkg="rosout" type="rosout" name="rosout" respawn="true"/>
</group>
</launch>

View File

@@ -0,0 +1 @@
- builder: epydoc

View File

@@ -0,0 +1,70 @@
#!/usr/bin/env python
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import sys
from optparse import OptionParser
from rosmaster.master_api import NUM_WORKERS
NAME = 'roscore'
def _get_optparse():
parser = OptionParser(usage="usage: %prog [options]",
prog=NAME,
description="roscore will start up a ROS Master, a ROS Parameter Server and a rosout logging node",
epilog="See http://www.ros.org/wiki/roscore"
)
parser.add_option("-p", "--port",
dest="port", default=None,
help="master port. Only valid if master is launched", metavar="PORT")
parser.add_option("-v", action="store_true",
dest="verbose", default=False,
help="verbose printing")
parser.add_option("-w", "--numworkers",
dest="num_workers", default=NUM_WORKERS, type=int,
help="override number of worker threads", metavar="NUM_WORKERS")
parser.add_option("-t", "--timeout",
dest="timeout",
help="override the socket connection timeout (in seconds).", metavar="TIMEOUT")
return parser
parser = _get_optparse()
(options, args) = parser.parse_args(sys.argv[1:])
if len(args) > 0:
parser.error("roscore does not take arguments")
import roslaunch
roslaunch.main(['roscore', '--core'] + sys.argv[1:])

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env python
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import roslaunch
roslaunch.main()

View File

@@ -0,0 +1,125 @@
#!/usr/bin/env python
# Software License Agreement (BSD License)
#
# Copyright (c) 2009, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import print_function
import os
import sys
import rospkg
import roslaunch.rlutil
import rosunit
def check_roslaunch_file(roslaunch_file, use_test_depends=False):
print("checking", roslaunch_file)
error_msg = roslaunch.rlutil.check_roslaunch(roslaunch_file, use_test_depends=use_test_depends)
# error message has to be XML attr safe
if error_msg:
return "[%s]:\n\t%s"%(roslaunch_file,error_msg)
def check_roslaunch_dir(roslaunch_dir, use_test_depends=False):
error_msgs = []
for f in os.listdir(roslaunch_dir):
if f.endswith('.launch'):
roslaunch_file = os.path.join(roslaunch_dir, f)
if os.path.isfile(roslaunch_file):
error_msgs.append(check_roslaunch_file(roslaunch_file, use_test_depends=use_test_depends))
# error message has to be XML attr safe
return '\n'.join([e for e in error_msgs if e])
## run check and output test result file
if __name__ == '__main__':
from optparse import OptionParser
parser = OptionParser(usage="usage: \troslaunch-check [options] <file|directory> [env=value...]")
# #3674
parser.add_option("-o", "--output-file",
dest="output_file", default=None,
help="file to store test results in", metavar="FILE")
parser.add_option("-t", "--use-test-depends",
action="store_true", dest="test_depends", default=False,
help="Assuming packages listed in test_depends are also installed")
options, args = parser.parse_args()
if not args:
parser.error('please specify a file or directory for roslaunch files to test')
roslaunch_path = args[0]
# #2590: implementing this quick and dirty as this script should only be used by higher level tools
env_vars = args[1:]
for e in env_vars:
var, val = e.split('=')
os.environ[var] = val
pkg = rospkg.get_package_name(roslaunch_path)
r = rospkg.RosPack()
pkg_dir = r.get_path(pkg)
if os.path.isfile(roslaunch_path):
error_msg = check_roslaunch_file(roslaunch_path, use_test_depends=options.test_depends)
else:
print("checking *.launch in directory", roslaunch_path)
error_msg = check_roslaunch_dir(roslaunch_path, use_test_depends=options.test_depends)
relpath = os.path.abspath(roslaunch_path)[len(os.path.abspath(pkg_dir))+1:]
outname = relpath.replace('.', '_').replace(os.sep, '_')
if outname == '_':
outname = '_pkg'
if options.output_file:
test_file = options.output_file
# If we were given a test file name to write, then construct the
# test name from it, to ensure uniqueness
test_name = os.path.basename(test_file)
else:
test_file = rosunit.xml_results_file(pkg, "roslaunch_check_"+outname, is_rostest=False)
test_name = roslaunch_path
print("...writing test results to", test_file)
dir_path = os.path.abspath(os.path.dirname(test_file))
if not os.path.exists(dir_path):
os.makedirs(dir_path)
if error_msg:
print("FAILURE:\n%s"%error_msg, file=sys.stderr)
with open(test_file, 'w') as f:
message = "roslaunch check [%s] failed"%(roslaunch_path)
f.write(rosunit.junitxml.test_failure_junit_xml(test_name, message, stdout=error_msg,
class_name="roslaunch.RoslaunchCheck", testcase_name="%s_%s" % (pkg, outname)))
print("wrote test file to [%s]"%test_file)
sys.exit(1)
else:
print("passed")
with open(test_file, 'w') as f:
f.write(rosunit.junitxml.test_success_junit_xml(test_name,
class_name="roslaunch.RoslaunchCheck", testcase_name="%s_%s" % (pkg, outname)))

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env python
# Software License Agreement (BSD License)
#
# Copyright (c) 2013, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import roslaunch.core
import roslaunch.arg_dump as roslaunch_arg_dump
from roslaunch import rlutil
import sys
try:
args = rlutil.resolve_launch_arguments(sys.argv[1:])
# Strip all other args: As we are completing they might be incomplete
# and will lead to an error. Anyways we only want all possible args.
args = args[0:1]
roslaunch_args = roslaunch_arg_dump.get_args(args)
except roslaunch.core.RLException:
# stay silent for completion
sys.exit(1)
#complete_args = [a + ":=" for a in sorted(roslaunch_args.keys())]
# Currently don't put := after that as the completion will escape := to \:\=
# Unfortunately we also want the escaping for roslaunch, when completing filenames
complete_args = [a for a in sorted(roslaunch_args.keys())]
print " ".join(complete_args)
sys.exit(0)

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env python
# Software License Agreement (BSD License)
#
# Copyright (c) 2009, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import roslaunch.depends
roslaunch.depends.roslaunch_deps_main()

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env python
# Software License Agreement (BSD License)
#
# Copyright (c) 2009, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import roslaunch.roslaunch_logs
roslaunch.roslaunch_logs.logs_main()

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env python
from distutils.core import setup
from catkin_pkg.python_setup import generate_distutils_setup
d = generate_distutils_setup(
packages=['roslaunch'],
package_dir={'': 'src'},
scripts=['scripts/roscore',
'scripts/roslaunch',
'scripts/roslaunch-complete',
'scripts/roslaunch-deps',
'scripts/roslaunch-logs'],
requires=['genmsg', 'genpy', 'roslib', 'rospkg']
)
setup(**d)

View File

@@ -0,0 +1,333 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Revision $Id$
from __future__ import print_function
import os
import logging
import sys
import traceback
# monkey-patch to suppress threading error message in Python 2.7.3
# see http://stackoverflow.com/questions/13193278/understand-python-threading-bug
if sys.version_info[:3] == (2, 7, 3):
import threading
threading._DummyThread._Thread__stop = lambda _dummy: None
import rospkg
from . import core as roslaunch_core
from . import param_dump as roslaunch_param_dump
# symbol exports
from .core import Node, Test, Master, RLException
from .config import ROSLaunchConfig
from .launch import ROSLaunchRunner
from .xmlloader import XmlLoader, XmlParseException
# script api
from .scriptapi import ROSLaunch
from .pmon import Process
try:
from rosmaster import DEFAULT_MASTER_PORT
except:
DEFAULT_MASTER_PORT = 11311
from rosmaster.master_api import NUM_WORKERS
NAME = 'roslaunch'
def configure_logging(uuid):
"""
scripts using roslaunch MUST call configure_logging
"""
try:
import socket
import rosgraph.roslogging
logfile_basename = os.path.join(uuid, '%s-%s-%s.log'%(NAME, socket.gethostname(), os.getpid()))
# additional: names of python packages we depend on that may also be logging
logfile_name = rosgraph.roslogging.configure_logging(NAME, filename=logfile_basename)
if logfile_name:
print("... logging to %s"%logfile_name)
# add logger to internal roslaunch logging infrastructure
logger = logging.getLogger('roslaunch')
roslaunch_core.add_printlog_handler(logger.info)
roslaunch_core.add_printerrlog_handler(logger.error)
except:
print("WARNING: unable to configure logging. No log files will be generated", file=sys.stderr)
def write_pid_file(options_pid_fn, options_core, port):
if options_pid_fn or options_core:
# #2987
ros_home = rospkg.get_ros_home()
if options_pid_fn:
pid_fn = os.path.expanduser(options_pid_fn)
if os.path.dirname(pid_fn) == ros_home and not os.path.exists(ros_home):
os.makedirs(ros_home)
else:
# NOTE: this assumption is not 100% valid until work on #3097 is complete
if port is None:
port = DEFAULT_MASTER_PORT
pid_fn = os.path.join(ros_home, 'roscore-%s.pid'%(port))
# #3828
if not os.path.exists(ros_home):
os.makedirs(ros_home)
with open(pid_fn, "w") as f:
f.write(str(os.getpid()))
def _get_optparse():
from optparse import OptionParser
usage = "usage: %prog [options] [package] <filename> [arg_name:=value...]\n"
usage += " %prog [options] <filename> [<filename>...] [arg_name:=value...]\n\n"
usage += "If <filename> is a single dash ('-'), launch XML is read from standard input."
parser = OptionParser(usage=usage, prog=NAME)
parser.add_option("--files",
dest="file_list", default=False, action="store_true",
help="Print list files loaded by launch file, including launch file itself")
parser.add_option("--args",
dest="node_args", default=None,
help="Print command-line arguments for node", metavar="NODE_NAME")
parser.add_option("--nodes",
dest="node_list", default=False, action="store_true",
help="Print list of node names in launch file")
parser.add_option("--find-node",
dest="find_node", default=None,
help="Find launch file that node is defined in", metavar="NODE_NAME")
parser.add_option("-c", "--child",
dest="child_name", default=None,
help="Run as child service 'NAME'. Required with -u", metavar="NAME")
parser.add_option("--local",
dest="local_only", default=False, action="store_true",
help="Do not launch remote nodes")
# #2370
parser.add_option("--screen",
dest="force_screen", default=False, action="store_true",
help="Force output of all local nodes to screen")
parser.add_option("-u", "--server_uri",
dest="server_uri", default=None,
help="URI of server. Required with -c", metavar="URI")
parser.add_option("--run_id",
dest="run_id", default=None,
help="run_id of session. Required with -c", metavar="RUN_ID")
# #1254: wait until master comes online before starting
parser.add_option("--wait", action="store_true",
dest="wait_for_master", default=False,
help="wait for master to start before launching")
parser.add_option("-p", "--port",
dest="port", default=None,
help="master port. Only valid if master is launched", metavar="PORT")
parser.add_option("--core", action="store_true",
dest="core", default=False,
help="Launch core services only")
parser.add_option("--pid",
dest="pid_fn", default="",
help="write the roslaunch pid to filename")
parser.add_option("-v", action="store_true",
dest="verbose", default=False,
help="verbose printing")
# 2685 - Dump parameters of launch files
parser.add_option("--dump-params", default=False, action="store_true",
dest="dump_params",
help="Dump parameters of all roslaunch files to stdout")
parser.add_option("--skip-log-check", default=False, action="store_true",
dest="skip_log_check",
help="skip check size of log folder")
parser.add_option("--ros-args", default=False, action="store_true",
dest="ros_args",
help="Display command-line arguments for this launch file")
parser.add_option("--disable-title", default=False, action="store_true",
dest="disable_title",
help="Disable setting of terminal title")
parser.add_option("-w", "--numworkers",
dest="num_workers", default=NUM_WORKERS, type=int,
help="override number of worker threads. Only valid for core services.", metavar="NUM_WORKERS")
parser.add_option("-t", "--timeout",
dest="timeout",
help="override the socket connection timeout (in seconds). Only valid for core services.", metavar="TIMEOUT")
return parser
def _validate_args(parser, options, args):
# validate args first so we don't spin up any resources
if options.child_name:
if not options.server_uri:
parser.error("--child option requires --server_uri to be set as well")
if not options.run_id:
parser.error("--child option requires --run_id to be set as well")
if options.port:
parser.error("port option cannot be used with roslaunch child mode")
if args:
parser.error("Input files are not allowed when run in child mode")
elif options.core:
if args:
parser.error("Input files are not allowed when launching core")
if options.run_id:
parser.error("--run_id should only be set for child roslaunches (-c)")
# we don't actually do anything special for core as the roscore.xml file
# is an implicit include for any roslaunch
elif len(args) == 0:
parser.error("you must specify at least one input file")
elif [f for f in args if not (f == '-' or os.path.exists(f))]:
parser.error("The following input files do not exist: %s"%f)
if args.count('-') > 1:
parser.error("Only a single instance of the dash ('-') may be specified.")
if len([x for x in [options.node_list, options.find_node, options.node_args, options.ros_args] if x]) > 1:
parser.error("only one of [--nodes, --find-node, --args --ros-args] may be specified")
def main(argv=sys.argv):
options = None
logger = None
try:
from . import rlutil
parser = _get_optparse()
(options, args) = parser.parse_args(argv[1:])
args = rlutil.resolve_launch_arguments(args)
_validate_args(parser, options, args)
# node args doesn't require any roslaunch infrastructure, so process it first
if any([options.node_args, options.node_list, options.find_node, options.dump_params, options.file_list, options.ros_args]):
if options.node_args and not args:
parser.error("please specify a launch file")
from . import node_args
if options.node_args:
node_args.print_node_args(options.node_args, args)
elif options.find_node:
node_args.print_node_filename(options.find_node, args)
# Dump parameters, #2685
elif options.dump_params:
roslaunch_param_dump.dump_params(args)
elif options.file_list:
rlutil.print_file_list(args)
elif options.ros_args:
import arg_dump as roslaunch_arg_dump
roslaunch_arg_dump.dump_args(args)
else:
node_args.print_node_list(args)
return
# we have to wait for the master here because we don't have the run_id yet
if options.wait_for_master:
if options.core:
parser.error("--wait cannot be used with roscore")
rlutil._wait_for_master()
# write the pid to a file
write_pid_file(options.pid_fn, options.core, options.port)
# spin up the logging infrastructure. have to wait until we can read options.run_id
uuid = rlutil.get_or_generate_uuid(options.run_id, options.wait_for_master)
configure_logging(uuid)
# #3088: don't check disk usage on remote machines
if not options.child_name and not options.skip_log_check:
# #2761
rlutil.check_log_disk_usage()
logger = logging.getLogger('roslaunch')
logger.info("roslaunch starting with args %s"%str(argv))
logger.info("roslaunch env is %s"%os.environ)
if options.child_name:
logger.info('starting in child mode')
# This is a roslaunch child, spin up client server.
# client spins up an XML-RPC server that waits for
# commands and configuration from the server.
from . import child as roslaunch_child
c = roslaunch_child.ROSLaunchChild(uuid, options.child_name, options.server_uri)
c.run()
else:
logger.info('starting in server mode')
# #1491 change terminal name
if not options.disable_title:
rlutil.change_terminal_name(args, options.core)
# Read roslaunch string from stdin when - is passed as launch filename.
roslaunch_strs = []
if '-' in args:
roslaunch_core.printlog("Passed '-' as file argument, attempting to read roslaunch XML from stdin.")
roslaunch_strs.append(sys.stdin.read())
roslaunch_core.printlog("... %d bytes read successfully.\n" % len(roslaunch_strs[-1]))
args.remove('-')
# This is a roslaunch parent, spin up parent server and launch processes.
# args are the roslaunch files to load
from . import parent as roslaunch_parent
# force a port binding spec if we are running a core
if options.core:
options.port = options.port or DEFAULT_MASTER_PORT
p = roslaunch_parent.ROSLaunchParent(uuid, args, roslaunch_strs=roslaunch_strs,
is_core=options.core, port=options.port, local_only=options.local_only,
verbose=options.verbose, force_screen=options.force_screen,
num_workers=options.num_workers, timeout=options.timeout)
p.start()
p.spin()
except RLException as e:
roslaunch_core.printerrlog(str(e))
roslaunch_core.printerrlog('The traceback for the exception was written to the log file')
if logger:
logger.error(traceback.format_exc())
sys.exit(1)
except ValueError as e:
# TODO: need to trap better than this high-level trap
roslaunch_core.printerrlog(str(e))
roslaunch_core.printerrlog('The traceback for the exception was written to the log file')
if logger:
logger.error(traceback.format_exc())
sys.exit(1)
except Exception as e:
traceback.print_exc()
sys.exit(1)
finally:
# remove the pid file
if options is not None and options.pid_fn:
try: os.unlink(options.pid_fn)
except os.error: pass
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,82 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2013, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Revision $Id$
from __future__ import print_function
"""
Utility module of roslaunch that computes the command-line arguments
for a launch file.
"""
import sys
import roslaunch.xmlloader
from roslaunch.core import RLException
from roslaunch.config import load_config_default
def get_args(roslaunch_files):
loader = roslaunch.xmlloader.XmlLoader(resolve_anon=False)
config = load_config_default(roslaunch_files, None, loader=loader, verbose=False, assign_machines=False)
return loader.root_context.resolve_dict.get('arg_doc', {})
def dump_args(roslaunch_files):
"""
Print list of args to screen. Will cause system exit if exception
occurs. This is a subroutine for the roslaunch main handler.
@param roslaunch_files: list of launch files to load
@type roslaunch_files: str
"""
try:
args = get_args(roslaunch_files)
if len(args) == 0:
print("No arguments.")
else:
required_args = [(name, (doc or 'undocumented', default)) for (name, (doc, default)) in args.items() if not default]
optional_args = [(name, (doc or 'undocumented', default)) for (name, (doc, default)) in args.items() if default]
if len(required_args) > 0:
print("Required Arguments:")
for (name, (doc, _)) in sorted(required_args):
print(" %s: %s" % (name, doc))
if len(optional_args) > 0:
print("Optional Arguments:")
for (name, (doc, default)) in sorted(optional_args):
print(" %s (default \"%s\"): %s" % (name, default, doc))
except RLException as e:
print(str(e), file=sys.stderr)
sys.exit(1)

View File

@@ -0,0 +1,125 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Revision $Id$
"""
ROSLaunch child server.
ROSLaunch has a client/server architecture for running remote
processes. When a user runs roslaunch, this creates a "parent"
roslaunch process. This parent process will then start "child"
processes on remote machines. The parent can then invoke methods on
this child process to launch remote processes, and the child can
invoke methods on the parent to provide feedback.
"""
import logging
import traceback
import roslaunch.core
import roslaunch.pmon
import roslaunch.server
class ROSLaunchChild(object):
"""
ROSLaunchChild infrastructure.
This must be called from the Python Main thread due to signal registration.
"""
def __init__(self, run_id, name, server_uri):
"""
Startup roslaunch remote client XML-RPC services. Blocks until shutdown
@param run_id: UUID of roslaunch session
@type run_id: str
@param name: name of remote client
@type name: str
@param server_uri: XML-RPC URI of roslaunch server
@type server_uri: str
@return: XML-RPC URI
@rtype: str
"""
roslaunch.core.set_child_mode(True)
self.logger = logging.getLogger("roslaunch.child")
self.run_id = run_id
self.name = name
self.server_uri = server_uri
self.child_server = None
self.pm = None
roslaunch.pmon._init_signal_handlers()
def _start_pm(self):
"""
Start process monitor for child roslaunch
"""
# start process monitor
# - this test is mainly here so that testing logic can
# override process monitor with a mock
if self.pm is None:
self.pm = roslaunch.pmon.start_process_monitor()
if self.pm is None:
# this should only happen if a shutdown signal is received during startup
raise roslaunch.core.RLException("cannot startup remote child: unable to start process monitor.")
self.logger.debug("started process monitor")
def run(self):
"""
Runs child. Blocks until child processes exit.
"""
try:
try:
self.logger.info("starting roslaunch child process [%s], server URI is [%s]", self.name, self.server_uri)
self._start_pm()
self.child_server = roslaunch.server.ROSLaunchChildNode(self.run_id, self.name, self.server_uri, self.pm)
self.logger.info("... creating XMLRPC server for child")
self.child_server.start()
self.logger.info("... started XMLRPC server for child")
# block until process monitor is shutdown
self.pm.mainthread_spin()
self.logger.info("... process monitor is done spinning")
except:
self.logger.error(traceback.format_exc())
raise
finally:
if self.pm:
self.pm.shutdown()
self.pm.join()
if self.child_server:
self.child_server.shutdown('roslaunch child complete')
def shutdown(self):
if self.pm:
self.pm.shutdown()
self.pm.join()

View File

@@ -0,0 +1,473 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Revision $Id: launch.py 2165 2008-09-17 22:38:49Z sfkwc $
"""
Defines the L{ROSLaunchConfig} object, which holds and the state of
the roslaunch file.
"""
import os
import logging
import types
import rospkg
import rospkg.distro
import rosgraph.names
import rosgraph.network
from .core import Master, local_machine, is_machine_local, RLException
import roslaunch.loader
import roslaunch.xmlloader
try:
from rosmaster import DEFAULT_MASTER_PORT
except:
DEFAULT_MASTER_PORT = 11311
def namespaces_of(name):
"""
utility to determine namespaces of a name
@raises ValueError
@raises TypeError
"""
if name is None:
raise ValueError('name')
try:
if not isinstance(name, basestring):
raise TypeError('name')
except NameError:
if not isinstance(name, str):
raise TypeError('name')
if not name:
return ['/']
splits = [x for x in name.split('/') if x]
return ['/'] + ['/'+'/'.join(splits[:i]) for i in range(1, len(splits))]
def get_roscore_filename():
# precedence: look for version in /etc/ros. If it's not there, fall back to roslaunch package
filename = os.path.join(rospkg.get_etc_ros_dir(), 'roscore.xml')
if os.path.isfile(filename):
return filename
r = rospkg.RosPack()
return os.path.join(r.get_path('roslaunch'), 'resources', 'roscore.xml')
def load_roscore(loader, config, verbose=True):
"""
Load roscore configuration into the ROSLaunchConfig using the specified XmlLoader
@param config ROSLaunchConfig
@param loader XmlLoader
"""
f_roscore = get_roscore_filename()
logging.getLogger('roslaunch').info('loading roscore config file %s'%f_roscore)
loader.load(f_roscore, config, core=True, verbose=verbose)
def calculate_env_loader(env=None):
"""
@raise RLException
"""
if env is None:
env = os.environ
# guess the env loader based on distro name
distro_name = rospkg.distro.current_distro_codename()
# sanity check
if distro_name in ['electric', 'diamondback', 'cturtle']:
raise RLException("This version of roslaunch is not compatible with pre-Fuerte ROS distributions")
return '/opt/ros/%s/env.sh'%(distro_name)
def _summary_name(node):
"""
Generate summary label for node based on its package, type, and name
"""
if node.name:
return "%s (%s/%s)"%(node.name, node.package, node.type)
else:
return "%s/%s"%(node.package, node.type)
class ROSLaunchConfig(object):
"""
ROSLaunchConfig is the container for the loaded roslaunch file state. It also
is responsible for validating then executing the desired state.
"""
def __init__(self):
"""
Initialize an empty config object. Master defaults to the environment's master.
"""
self.master = Master()
self.nodes_core = []
self.nodes = [] #nodes are unnamed
self.roslaunch_files = [] # metadata about files used to create config
# list of resolved node names. This is so that we can check for naming collisions
self.resolved_node_names = []
self.tests = []
self.machines = {} #key is name
self.params = {} #key is name
self.clear_params = []
self.executables = []
# for tools like roswtf
self.config_errors = []
m = local_machine() #for local exec
self.machines[m.name] = m
self._assign_machines_complete = False
self._remote_nodes_present = None
self.logger = logging.getLogger('roslaunch')
def add_roslaunch_file(self, f):
"""
Add metadata about file used to create config
"""
self.roslaunch_files.append(f)
def add_config_error(self, msg):
"""
Report human-readable error message related to configuration error
@param msg: error message
@type msg: str
"""
self.config_errors.append(msg)
def set_master(self, m):
"""
Set the master configuration
@param m: Master
@type m: L{Master}
"""
self.master = m
def has_remote_nodes(self):
"""
@return: True if roslaunch will launch nodes on a remote machine
@rtype: bool
@raises: RLException
"""
if not self._assign_machines_complete:
raise RLException("ERROR: has_remote_nodes() cannot be called until prelaunch check is complete")
return self._remote_nodes_present
def assign_machines(self):
"""
Assign nodes to machines and determine whether or not there are any remote machines
"""
# don't repeat machine assignment
if self._assign_machines_complete:
return
machine_unify_dict = {}
self._assign_machines_complete = True
# #653: current have to set all core nodes to local launch
local_machine = self.machines['']
for n in self.nodes_core:
n.machine = local_machine
#for n in self.nodes_core + self.nodes + self.tests:
for n in self.nodes + self.tests:
m = self._select_machine(n)
# if machines have the same config keys it means that they are identical except
# for their name. we unify the machine assignments so that we don't use
# extra resources.
config_key = m.config_key()
if config_key in machine_unify_dict:
new_m = machine_unify_dict[config_key]
if m != new_m:
self.logger.info("... changing machine assignment from [%s] to [%s] as they are equivalent", m.name, new_m.name)
m = new_m
else:
machine_unify_dict[config_key] = m
n.machine = m
self.logger.info("... selected machine [%s] for node of type [%s/%s]", m.name, n.package, n.type)
# determine whether or not there are any machines we will need
# to setup remote roslaunch clients for
self._remote_nodes_present = False
if [m for m in machine_unify_dict.values() if not is_machine_local(m)]:
self._remote_nodes_present = True
def summary(self, local=False):
"""
Get a human-readable string summary of the launch
@param local bool: if True, only print local nodes
@return: summary
@rtype: str
"""
summary = '\nSUMMARY\n========'
if self.clear_params:
summary += '\n\nCLEAR PARAMETERS\n' + '\n'.join(sorted([' * %s'%p for p in self.clear_params]))
if self.params:
def strip_string(value):
# not dealing with non-ascii characters here
try:
value = str(value)
except UnicodeEncodeError:
return '<...>'
max_length = 20
if len(value) > max_length:
value = value[:max_length - 3] + '...'
# if non printable characters are present return replacement
for i, char in enumerate(value):
o = ord(char)
if o < 32 or o > 126:
# skip when the special characters are only trailing whitespaces
value = value.rstrip()
if i >= len(value):
break
return '<...>'
return value
summary += '\n\nPARAMETERS\n' + '\n'.join(sorted([' * %s: %s' % (k, strip_string(v.value)) for k, v in self.params.items()]))
if not local:
summary += '\n\nMACHINES\n' + '\n'.join(sorted([' * %s'%k for k in self.machines if k]))
summary += '\n\nNODES\n'
namespaces = {}
if local:
nodes = [n for n in self.nodes if is_machine_local(n.machine)]
else:
nodes = self.nodes
for n in nodes:
ns = n.namespace
if ns not in namespaces:
namespaces[ns] = [n]
else:
namespaces[ns].append(n)
for k,v in namespaces.items():
summary += ' %s\n'%k + '\n'.join(sorted([' %s'%_summary_name(n) for n in v]))
summary += '\n'
return summary
def add_executable(self, exe):
"""
Declare an exectuable to be run during the launch
@param exe: Executable
@type exe: L{Executable}
@raises ValueError
"""
if not exe:
raise ValueError("exe is None")
self.executables.append(exe)
def add_clear_param(self, param):
"""
Declare a parameter to be cleared before new parameters are set
@param param: parameter to clear
@type param: str
"""
self.clear_params.append(param)
def add_param(self, p, filename=None, verbose=True):
"""
Declare a parameter to be set on the param server before launching nodes
@param p: parameter instance
@type p: L{Param}
"""
key = p.key
# check for direct overrides
if key in self.params and self.params[key] != p:
if filename:
self.logger.debug("[%s] overriding parameter [%s]"%(filename, p.key))
else:
self.logger.debug("overriding parameter [%s]"%p.key)
# check for parent conflicts
for parent_key in [pk for pk in namespaces_of(key) if pk in self.params]:
self.add_config_error("parameter [%s] conflicts with parent parameter [%s]"%(key, parent_key))
self.params[key] = p
if verbose:
print("Added parameter [%s]" % key)
t = type(p.value)
if t in [bool, int, float]:
self.logger.debug("add_param[%s]: type [%s] value [%s]"%(p.key, t, p.value))
else:
self.logger.debug("add_param[%s]: type [%s]"%(p.key, t))
def add_machine(self, m, verbose=True):
"""
Declare a machine and associated parameters so that it can be used for
running nodes.
@param m: machine instance
@type m: L{Machine}
@return: True if new machine added, False if machine already specified.
@rtype: bool
@raises RLException: if cannot add machine as specified
"""
name = m.name
# Fuerte: all machines must have an env loader. We can guess
# it from the distro name for easier migration.
if not m.env_loader:
m.env_loader = calculate_env_loader()
if m.address == 'localhost': #simplify address comparison
address = rosgraph.network.get_local_address()
self.logger.info("addMachine[%s]: remapping localhost address to %s"%(name, address))
if name in self.machines:
if m != self.machines[name]:
raise RLException("Machine [%s] already added and does not match duplicate entry"%name)
return False
else:
self.machines[name] = m
if verbose:
print("Added machine [%s]" % name)
return True
def add_test(self, test, verbose=True):
"""
Add test declaration. Used by rostest
@param test: test node instance to add to launch
@type test: L{Test}
"""
self.tests.append(test)
def add_node(self, node, core=False, verbose=True):
"""
Add node declaration
@param node: node instance to add to launch
@type node: L{Node}
@param core: if True, node is a ROS core node
@type core: bool
@raises RLException: if ROS core node is missing required name
"""
if node.name:
# check for duplicates
resolved_name = rosgraph.names.ns_join(node.namespace, node.name)
matches = [n for n in self.resolved_node_names if n == resolved_name]
if matches:
raise RLException("roslaunch file contains multiple nodes named [%s].\nPlease check all <node> 'name' attributes to make sure they are unique.\nAlso check that $(anon id) use different ids."%resolved_name)
else:
self.resolved_node_names.append(resolved_name)
if not core:
self.nodes.append(node)
if verbose:
print("Added node of type [%s/%s] in namespace [%s]" % (node.package, node.type, node.namespace))
self.logger.info("Added node of type [%s/%s] in namespace [%s]", node.package, node.type, node.namespace)
else:
if not node.name:
raise RLException("ROS core nodes must have a name. [%s/%s]"%(node.package, node.type))
self.nodes_core.append(node)
if verbose:
print("Added core node of type [%s/%s] in namespace [%s]" % (node.package, node.type, node.namespace))
self.logger.info("Added core node of type [%s/%s] in namespace [%s]", node.package, node.type, node.namespace)
def _select_machine(self, node):
"""
Select a machine for a node to run on. For nodes that are
already assigned to a machine, this will map the string name to
a L{Machine} instance. If the node isn't already tagged with a
particular machine, one will be selected for it.
@param node: node to assign machine for
@type node: L{Node}
@return: machine to run on
@rtype: L{Machine}
@raises RLException: If machine state is improperly configured
"""
machine = node.machine_name
#Lookup machine
if machine:
if not machine in self.machines:
raise RLException("ERROR: unknown machine [%s]"%machine)
return self.machines[machine]
else:
# assign to local machine
return self.machines['']
def load_config_default(roslaunch_files, port, roslaunch_strs=None, loader=None, verbose=False, assign_machines=True):
"""
Base routine for creating a ROSLaunchConfig from a set of
roslaunch_files and or launch XML strings and initializing it. This
config will have a core definition and also set the master to run
on port.
@param roslaunch_files: list of launch files to load
@type roslaunch_files: [str]
@param port: roscore/master port override. Set to 0 or None to use default.
@type port: int
@param roslaunch_strs: (optional) roslaunch XML strings to load
@type roslaunch_strs: [str]
@param verbose: (optional) print info to screen about model as it is loaded.
@type verbose: bool
@param assign_machines: (optional) assign nodes to machines (default: True)
@type assign_machines: bool
@return: initialized rosconfig instance
@rytpe: L{ROSLaunchConfig} initialized rosconfig instance
@raises: RLException
"""
logger = logging.getLogger('roslaunch.config')
# This is the main roslaunch server process. Load up the
# files specified on the command line and launch the
# requested resourcs.
config = ROSLaunchConfig()
if port:
config.master.uri = rosgraph.network.create_local_xmlrpc_uri(port)
loader = loader or roslaunch.xmlloader.XmlLoader()
# load the roscore file first. we currently have
# last-declaration wins rules. roscore is just a
# roslaunch file with special load semantics
load_roscore(loader, config, verbose=verbose)
# load the roslaunch_files into the config
for f in roslaunch_files:
try:
logger.info('loading config file %s'%f)
loader.load(f, config, verbose=verbose)
except roslaunch.xmlloader.XmlParseException as e:
raise RLException(e)
except roslaunch.loader.LoadException as e:
raise RLException(e)
# we need this for the hardware test systems, which builds up
# roslaunch launch files in memory
if roslaunch_strs:
for launch_str in roslaunch_strs:
try:
logger.info('loading config file from string')
loader.load_string(launch_str, config)
except roslaunch.xmlloader.XmlParseException as e:
raise RLException('Launch string: %s\nException: %s'%(launch_str, e))
except roslaunch.loader.LoadException as e:
raise RLException('Launch string: %s\nException: %s'%(launch_str, e))
# choose machines for the nodes
if assign_machines:
config.assign_machines()
return config

View File

@@ -0,0 +1,675 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import print_function
"""
Core roslaunch model and lower-level utility routines.
"""
import os
import logging
import socket
import sys
try:
from xmlrpc.client import MultiCall, ServerProxy
except ImportError:
from xmlrpclib import MultiCall, ServerProxy
import rospkg
import rosgraph
import rosgraph.names
import rosgraph.network
from xml.sax.saxutils import escape
try:
unicode
except NameError:
# Python 3: for _xml_escape
basestring = unicode = str
class RLException(Exception):
"""Base roslaunch exception type"""
pass
## Phases allow executables to be assigned to a particular run period
PHASE_SETUP = 'setup'
PHASE_RUN = 'run'
PHASE_TEARDOWN = 'teardown'
_child_mode = False
def is_child_mode():
"""
:returns: ``True`` if roslaunch is running in remote child mode, ``bool``
"""
return _child_mode
def set_child_mode(child_mode):
"""
:param child_mode: True if roslaunch is running in remote
child mode, ``bool``
"""
global _child_mode
_child_mode = child_mode
def is_machine_local(machine):
"""
Check to see if machine is local. NOTE: a machine is not local if
its user credentials do not match the current user.
:param machine: Machine, ``Machine``
:returns: True if machine is local and doesn't require remote login, ``bool``
"""
try:
# If Python has ipv6 disabled but machine.address can be resolved somehow to an ipv6 address, then host[4][0] will be int
machine_ips = [host[4][0] for host in socket.getaddrinfo(machine.address, 0, 0, 0, socket.SOL_TCP) if isinstance(host[4][0], str)]
except socket.gaierror:
raise RLException("cannot resolve host address for machine [%s]"%machine.address)
local_addresses = ['localhost'] + rosgraph.network.get_local_addresses()
# check 127/8 and local addresses
is_local = ([ip for ip in machine_ips if (ip.startswith('127.') or ip == '::1')] != []) or (set(machine_ips) & set(local_addresses) != set())
#491: override local to be ssh if machine.user != local user
if is_local and machine.user:
import getpass
is_local = machine.user == getpass.getuser()
return is_local
_printlog_handlers = []
_printerrlog_handlers = []
def printlog(msg):
"""
Core utility for printing message to stdout as well as printlog handlers
:param msg: message to print, ``str``
"""
for h in _printlog_handlers:
try: # don't let this bomb out the actual code
h(msg)
except:
pass
try: # don't let this bomb out the actual code
print(msg)
except:
pass
def printlog_bold(msg):
"""
Similar to L{printlog()}, but the message printed to screen is bolded for greater clarity
:param msg: message to print, ``str``
"""
for h in _printlog_handlers:
try: # don't let this bomb out the actual code
h(msg)
except:
pass
try: # don't let this bomb out the actual code
if sys.platform in ['win32']:
print('%s' % msg) # windows console is terrifically boring
else:
print('\033[1m%s\033[0m' % msg)
except:
pass
def printerrlog(msg):
"""
Core utility for printing message to stderr as well as printerrlog handlers
:param msg: message to print, ``str``
"""
for h in _printerrlog_handlers:
try: # don't let this bomb out the actual code
h(msg)
except:
pass
# #1003: this has raised IOError (errno 104) in robot use. Better to
# trap than let a debugging routine fault code.
try: # don't let this bomb out the actual code
print('\033[31m%s\033[0m' % msg, file=sys.stderr)
except:
pass
def add_printlog_handler(h):
"""
Register additional handler for printlog()
"""
_printlog_handlers.append(h)
def add_printerrlog_handler(h):
"""
Register additional handler for printerrlog()
"""
_printerrlog_handlers.append(h)
def clear_printlog_handlers():
"""
Delete all printlog handlers. required for testing
"""
del _printlog_handlers[:]
def clear_printerrlog_handlers():
"""
Delete all printerrlog handlers. required for testing
"""
del _printerrlog_handlers[:]
def setup_env(node, machine, master_uri, env=None):
"""
Create dictionary of environment variables to set for launched
process.
setup_env() will only set ROS_*, PYTHONPATH, and user-specified
environment variables.
:param machine: machine being launched on, ``Machine``
:param node: node that is being launched or None, ``Node``
:param master_uri: ROS master URI, ``str``
:param env: base environment configuration, defaults to ``os.environ``
:returns: process env dictionary, ``dict``
"""
if env is None:
env = os.environ
d = env.copy()
d[rosgraph.ROS_MASTER_URI] = master_uri
# add node-specific env args last as they have highest precedence
if node:
if rosgraph.ROS_NAMESPACE in d:
del d[rosgraph.ROS_NAMESPACE]
ns = node.namespace
if ns[-1] == '/':
ns = ns[:-1]
if ns:
d[rosgraph.ROS_NAMESPACE] = ns
for name, value in node.env_args:
d[name] = value
return d
def rle_wrapper(fn):
"""
Wrap lower-level exceptions in RLException class
:returns:: function wrapper that throws an RLException if the
wrapped function throws an Exception, ``fn``
"""
def wrapped_fn(*args):
try:
return fn(*args)
except Exception as e:
# we specifically catch RLExceptions and print their messages differently
raise RLException("ERROR: %s"%e)
return wrapped_fn
get_ros_root = rospkg.get_ros_root
get_master_uri_env = rle_wrapper(rosgraph.get_master_uri)
get_ros_package_path = rospkg.get_ros_package_path
def remap_localhost_uri(uri, force_localhost=False):
"""
Resolve localhost addresses to an IP address so that
:param uri: XML-RPC URI, ``str``
:param force_localhost: if True, URI is mapped onto the local machine no matter what, ``bool``
"""
hostname, port = rosgraph.network.parse_http_host_and_port(uri)
if force_localhost or hostname == 'localhost':
return rosgraph.network.create_local_xmlrpc_uri(port)
else:
return uri
##################################################################
# DATA STRUCTURES
class Master(object):
"""
Data structure for representing and querying state of master
"""
__slots__ = ['type', 'auto', 'uri']
ROSMASTER = 'rosmaster'
# deprecated
ZENMASTER = 'zenmaster'
def __init__(self, type_=None, uri=None, auto=None):
"""
Create new Master instance.
:param uri: master URI. Defaults to ROS_MASTER_URI environment variable, ``str``
:param type_: Currently only support 'rosmaster', ``str``
"""
self.auto = None # no longer used
self.type = type_ or Master.ROSMASTER
self.uri = uri or get_master_uri_env()
def get_host(self):
# parse from the URI
host, _ = rosgraph.network.parse_http_host_and_port(self.uri)
return host
def get_port(self):
"""
Get the port this master is configured for.
"""
# parse from the URI
_, urlport = rosgraph.network.parse_http_host_and_port(self.uri)
return urlport
def __eq__(self, m2):
if not isinstance(m2, Master):
return False
else:
return m2.type == self.type and m2.uri == self.uri
def get(self):
"""
:returns:: XMLRPC proxy for communicating with master, ``xmlrpc.client.ServerProxy``
"""
return ServerProxy(self.uri)
def get_multi(self):
"""
:returns:: multicall XMLRPC proxy for communicating with master, ``xmlrpc.client.MultiCall``
"""
return MultiCall(self.get())
def is_running(self):
"""
Check if master is running.
:returns:: True if the master is running, ``bool``
"""
try:
try:
to_orig = socket.getdefaulttimeout()
# enable timeout
socket.setdefaulttimeout(5.0)
logging.getLogger('roslaunch').info('master.is_running[%s]'%self.uri)
code, status, val = self.get().getPid('/roslaunch')
if code != 1:
raise RLException("ERROR: master failed status check: %s"%msg)
logging.getLogger('roslaunch.core').debug('master.is_running[%s]: True'%self.uri)
return True
finally:
socket.setdefaulttimeout(to_orig)
except:
logging.getLogger('roslaunch.core').debug('master.is_running[%s]: False'%self.uri)
return False
## number of seconds that a child machine is allowed to register with
## the parent before being considered failed
_DEFAULT_REGISTER_TIMEOUT = 10.0
class Machine(object):
"""
Data structure for storing information about a machine in the ROS
system. Corresponds to the 'machine' tag in the launch
specification.
"""
__slots__ = ['name', 'address', 'ssh_port', 'user', 'password', 'assignable',
'env_loader', 'timeout']
def __init__(self, name, address,
env_loader=None, ssh_port=22, user=None, password=None,
assignable=True, env_args=[], timeout=None):
"""
:param name: machine name, ``str``
:param address: network address of machine, ``str``
:param env_loader: Path to environment loader, ``str``
:param ssh_port: SSH port number, ``int``
:param user: SSH username, ``str``
:param password: SSH password. Not recommended for use. Use SSH keys instead., ``str``
"""
self.name = name
self.env_loader = env_loader
self.user = user or None
self.password = password or None
self.address = address
self.ssh_port = ssh_port
self.assignable = assignable
self.timeout = timeout or _DEFAULT_REGISTER_TIMEOUT
def __str__(self):
return "Machine(name[%s] env_loader[%s] address[%s] ssh_port[%s] user[%s] assignable[%s] timeout[%s])"%(self.name, self.env_loader, self.address, self.ssh_port, self.user, self.assignable, self.timeout)
def __eq__(self, m2):
if not isinstance(m2, Machine):
return False
return self.name == m2.name and \
self.assignable == m2.assignable and \
self.config_equals(m2)
def config_key(self):
"""
Get a key that represents the configuration of the
machine. machines with identical configurations have identical
keys
:returns:: configuration key, ``str``
"""
return "Machine(address[%s] env_loader[%s] ssh_port[%s] user[%s] password[%s] timeout[%s])"%(self.address, self.env_loader, self.ssh_port, self.user or '', self.password or '', self.timeout)
def config_equals(self, m2):
"""
:returns:: True if machines have identical configurations, ``bool``
"""
if not isinstance(m2, Machine):
return False
return self.config_key() == m2.config_key()
def __ne__(self, m2):
return not self.__eq__(m2)
class Param(object):
"""
Data structure for storing information about a desired parameter in
the ROS system Corresponds to the 'param' tag in the launch
specification.
"""
def __init__(self, key, value):
self.key = rosgraph.names.canonicalize_name(key)
self.value = value
def __eq__(self, p):
if not isinstance(p, Param):
return False
return p.key == self.key and p.value == self.value
def __ne__(self, p):
return not self.__eq__(p)
def __str__(self):
return "%s=%s"%(self.key, self.value)
def __repr__(self):
return "%s=%s"%(self.key, self.value)
_local_m = None
def local_machine():
"""
:returns:: Machine instance representing the local machine, ``Machine``
"""
global _local_m
if _local_m is None:
_local_m = Machine('', 'localhost')
return _local_m
class Node(object):
"""
Data structure for storing information about a desired node in
the ROS system Corresponds to the 'node' tag in the launch
specification.
"""
__slots__ = ['package', 'type', 'name', 'namespace', \
'machine_name', 'machine', 'args', 'respawn', \
'respawn_delay', \
'remap_args', 'env_args',\
'process_name', 'output', 'cwd',
'launch_prefix', 'required',
'filename']
def __init__(self, package, node_type, name=None, namespace='/', \
machine_name=None, args='', \
respawn=False, respawn_delay=0.0, \
remap_args=None,env_args=None, output=None, cwd=None, \
launch_prefix=None, required=False, filename='<unknown>'):
"""
:param package: node package name, ``str``
:param node_type: node type, ``str``
:param name: node name, ``str``
:param namespace: namespace for node, ``str``
:param machine_name: name of machine to run node on, ``str``
:param args: argument string to pass to node executable, ``str``
:param respawn: if True, respawn node if it dies, ``bool``
:param respawn: if respawn is True, respawn node after delay, ``float``
:param remap_args: list of [(from, to)] remapping arguments, ``[(str, str)]``
:param env_args: list of [(key, value)] of
additional environment vars to set for node, ``[(str, str)]``
:param output: where to log output to, either Node, 'screen' or 'log', ``str``
:param cwd: current working directory of node, either 'node', 'ROS_HOME'. Default: ROS_HOME, ``str``
:param launch_prefix: launch command/arguments to prepend to node executable arguments, ``str``
:param required: node is required to stay running (launch fails if node dies), ``bool``
:param filename: name of file Node was parsed from, ``str``
:raises: :exc:`ValueError` If parameters do not validate
"""
self.package = package
self.type = node_type
self.name = name or None
self.namespace = rosgraph.names.make_global_ns(namespace or '/')
self.machine_name = machine_name or None
self.respawn = respawn
self.respawn_delay = respawn_delay
self.args = args or ''
self.remap_args = remap_args or []
self.env_args = env_args or []
self.output = output
self.cwd = cwd
if self.cwd == 'ros_home': # be lenient on case
self.cwd = 'ROS_HOME'
self.launch_prefix = launch_prefix or None
self.required = required
self.filename = filename
if self.respawn and self.required:
raise ValueError("respawn and required cannot both be set to true")
# validation
if self.name and rosgraph.names.SEP in self.name: # #1821, namespaces in nodes need to be banned
raise ValueError("node name cannot contain a namespace")
if not len(self.package.strip()):
raise ValueError("package must be non-empty")
if not len(self.type.strip()):
raise ValueError("type must be non-empty")
if not self.output in ['log', 'screen', None]:
raise ValueError("output must be one of 'log', 'screen'")
if not self.cwd in ['ROS_HOME', 'node', None]:
raise ValueError("cwd must be one of 'ROS_HOME', 'node'")
# Extra slots for assigning later
# slot to store the process name in so that we can query the
# associated process state
self.process_name = None
# machine is the assigned machine instance. should probably
# consider storing this elsewhere as it can be inconsistent
# with machine_name and is also a runtime, rather than
# configuration property
self.machine = None
def xmltype(self):
return 'node'
def xmlattrs(self):
name_str = cwd_str = respawn_str = None
if self.name:
name_str = self.name
if self.cwd:
cwd_str = self.cwd
return [
('pkg', self.package),
('type', self.type),
('machine', self.machine_name),
('ns', self.namespace),
('args', self.args),
('output', self.output),
('cwd', cwd_str),
('respawn', self.respawn), #not valid on <test>
('respawn_delay', self.respawn_delay), # not valid on <test>
('name', name_str),
('launch-prefix', self.launch_prefix),
('required', self.required),
]
#TODO: unify with to_remote_xml using a filter_fn
def to_xml(self):
"""
convert representation into XML representation. Currently cannot represent private parameters.
:returns:: XML representation for remote machine, ``str``
"""
t = self.xmltype()
attrs = [(a, v) for a, v in self.xmlattrs() if v != None]
xmlstr = '<%s %s>\n'%(t, ' '.join(['%s="%s"'%(val[0], _xml_escape(val[1])) for val in attrs]))
xmlstr += ''.join([' <remap from="%s" to="%s" />\n'%tuple(r) for r in self.remap_args])
xmlstr += ''.join([' <env name="%s" value="%s" />\n'%tuple(e) for e in self.env_args])
xmlstr += "</%s>"%t
return xmlstr
def to_remote_xml(self):
"""
convert representation into remote representation. Remote representation does
not include parameter settings or 'machine' attribute
:returns:: XML representation for remote machine, ``str``
"""
t = self.xmltype()
attrs = [(a, v) for a, v in self.xmlattrs() if v != None and a != 'machine']
xmlstr = '<%s %s>\n'%(t, ' '.join(['%s="%s"'%(val[0], _xml_escape(val[1])) for val in attrs]))
xmlstr += ''.join([' <remap from="%s" to="%s" />\n'%tuple(r) for r in self.remap_args])
xmlstr += ''.join([' <env name="%s" value="%s" />\n'%tuple(e) for e in self.env_args])
xmlstr += "</%s>"%t
return xmlstr
def _xml_escape(s):
"""
Escape string for XML
:param s: string to escape, ``str``
:returns:: string with XML entities (<, >, \", &) escaped, ``str``
"""
# use official escaping to preserve unicode.
# see import at the top for py3k-compat
if isinstance(s, basestring):
return escape(s, entities={'"': '&quot;'})
else:
# don't escape non-string attributes
return s
TEST_TIME_LIMIT_DEFAULT = 1 * 60 #seconds
class Test(Node):
"""
A Test is a Node with special semantics that it performs a
unit/integration test. The data model is the same except the
option to set the respawn flag is removed.
"""
__slots__ = ['test_name', 'time_limit', 'retry']
def __init__(self, test_name, package, node_type, name=None, \
namespace='/', machine_name=None, args='', \
remap_args=None, env_args=None, time_limit=None, cwd=None,
launch_prefix=None, retry=None, filename="<unknown>"):
"""
Construct a new test node.
:param test_name: name of test for recording in test results, ``str``
:param time_limit: number of seconds that a test
should run before marked as a failure, ``int/float/long``
"""
super(Test, self).__init__(package, node_type, name=name, \
namespace=namespace, \
machine_name=machine_name, args=args, \
remap_args=remap_args,
env_args=env_args,
#output always is log
output='log', cwd=cwd,
launch_prefix=launch_prefix, filename=filename)
self.test_name = test_name
self.retry = retry or 0
time_limit = time_limit or TEST_TIME_LIMIT_DEFAULT
number_types = [float, int]
try:
number_types.append(long)
except NameError:
pass
if not type(time_limit) in number_types:
raise ValueError("'time-limit' must be a number")
time_limit = float(time_limit) #force to floating point
if time_limit <= 0:
raise ValueError("'time-limit' must be a positive number")
self.time_limit = time_limit
def xmltype(self):
return 'test'
def xmlattrs(self):
"""
NOTE: xmlattrs does not necessarily produce identical XML as
to what it was initialized with, though the properties are the same
"""
attrs = Node.xmlattrs(self)
attrs = [(a, v) for (a, v) in attrs if a not in ['respawn', \
'respawn_delay']]
attrs.append(('test-name', self.test_name))
if self.retry:
attrs.append(('retry', str(self.retry)))
if self.time_limit != TEST_TIME_LIMIT_DEFAULT:
attrs.append(('time-limit', self.time_limit))
return attrs
class Executable(object):
"""
Executable is a generic container for exectuable commands.
"""
def __init__(self, cmd, args, phase=PHASE_RUN):
"""
:param cmd: name of command to run, ``str``
:param args: arguments to command, ``(str,)``
:param phase: PHASE_SETUP|PHASE_RUN|PHASE_TEARDOWN. Indicates whether the
command should be run before, during, or after launch, ``str``
"""
self.command = cmd
self.args = args
self.phase = phase
def __repr__(self):
return "%s %s"%(self.command, ' '.join(self.args))
def __str__(self):
return "%s %s"%(self.command, ' '.join(self.args))
class RosbinExecutable(Executable):
"""
RosbinExecutables are exectuables stored in ROS_ROOT/bin.
"""
def __init__(self, cmd, args, phase=PHASE_RUN):
super(RosbinExecutable, self).__init__(cmd, args, phase)
def __repr__(self):
return "ros/bin/%s %s"%(self.command, ' '.join(self.args))
def __str__(self):
return "ros/bin/%s %s"%(self.command, ' '.join(self.args))
def generate_run_id():
"""
Utility routine for generating run IDs (UUIDs)
:returns: guid, ``str``
"""
import uuid
return str(uuid.uuid1())

View File

@@ -0,0 +1,371 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""
Utility module of roslaunch that extracts dependency information from
roslaunch files, including calculating missing package dependencies.
"""
from __future__ import print_function
import os
import sys
from xml.dom.minidom import parse
from xml.dom import Node as DomNode
import rospkg
from .loader import convert_value
from .substitution_args import resolve_args
NAME="roslaunch-deps"
class RoslaunchDepsException(Exception):
"""
Base exception of roslaunch.depends errors.
"""
pass
class RoslaunchDeps(object):
"""
Represents dependencies of a roslaunch file.
"""
def __init__(self, nodes=None, includes=None, pkgs=None):
if nodes == None:
nodes = []
if includes == None:
includes = []
if pkgs == None:
pkgs = []
self.nodes = nodes
self.includes = includes
self.pkgs = pkgs
def __eq__(self, other):
if not isinstance(other, RoslaunchDeps):
return False
return set(self.nodes) == set(other.nodes) and \
set(self.includes) == set(other.includes) and \
set(self.pkgs) == set(other.pkgs)
def __repr__(self):
return "nodes: %s\nincludes: %s\npkgs: %s"%(str(self.nodes), str(self.includes), str(self.pkgs))
def __str__(self):
return "nodes: %s\nincludes: %s\npkgs: %s"%(str(self.nodes), str(self.includes), str(self.pkgs))
def _get_arg_value(tag, context):
name = tag.attributes['name'].value
if tag.attributes.has_key('value'):
return resolve_args(tag.attributes['value'].value, context)
elif name in context['arg']:
return context['arg'][name]
elif tag.attributes.has_key('default'):
return resolve_args(tag.attributes['default'].value, context)
else:
raise RoslaunchDepsException("No value for arg [%s]"%(name))
def _check_ifunless(tag, context):
if tag.attributes.has_key('if'):
val = resolve_args(tag.attributes['if'].value, context)
if not convert_value(val, 'bool'):
return False
elif tag.attributes.has_key('unless'):
val = resolve_args(tag.attributes['unless'].value, context)
if convert_value(val, 'bool'):
return False
return True
def _parse_subcontext(tags, context):
subcontext = {'arg': {}}
if tags == None:
return subcontext
for tag in [t for t in tags if t.nodeType == DomNode.ELEMENT_NODE]:
if tag.tagName == 'arg' and _check_ifunless(tag, context):
subcontext['arg'][tag.attributes['name'].value] = _get_arg_value(tag, context)
return subcontext
def _parse_launch(tags, launch_file, file_deps, verbose, context):
dir_path = os.path.dirname(os.path.abspath(launch_file))
launch_file_pkg = rospkg.get_package_name(dir_path)
# process group, include, node, and test tags from launch file
for tag in [t for t in tags if t.nodeType == DomNode.ELEMENT_NODE]:
if not _check_ifunless(tag, context):
continue
if tag.tagName == 'group':
#descend group tags as they can contain node tags
_parse_launch(tag.childNodes, launch_file, file_deps, verbose, context)
elif tag.tagName == 'arg':
context['arg'][tag.attributes['name'].value] = _get_arg_value(tag, context)
elif tag.tagName == 'include':
try:
sub_launch_file = resolve_args(tag.attributes['file'].value, context)
except KeyError as e:
raise RoslaunchDepsException("Cannot load roslaunch <%s> tag: missing required attribute %s.\nXML is %s"%(tag.tagName, str(e), tag.toxml()))
# Check if an empty file is included, and skip if so.
# This will allow a default-empty <include> inside a conditional to pass
if sub_launch_file == '':
if verbose:
print("Empty <include> in %s. Skipping <include> of %s" %
(launch_file, tag.attributes['file'].value))
continue
if verbose:
print("processing included launch %s"%sub_launch_file)
# determine package dependency for included file
sub_pkg = rospkg.get_package_name(os.path.dirname(os.path.abspath(sub_launch_file)))
if sub_pkg is None:
print("ERROR: cannot determine package for [%s]"%sub_launch_file, file=sys.stderr)
if sub_launch_file not in file_deps[launch_file].includes:
file_deps[launch_file].includes.append(sub_launch_file)
if launch_file_pkg != sub_pkg:
file_deps[launch_file].pkgs.append(sub_pkg)
# recurse
file_deps[sub_launch_file] = RoslaunchDeps()
try:
dom = parse(sub_launch_file).getElementsByTagName('launch')
if not len(dom):
print("ERROR: %s is not a valid roslaunch file"%sub_launch_file, file=sys.stderr)
else:
launch_tag = dom[0]
sub_context = _parse_subcontext(tag.childNodes, context)
try:
if tag.attributes['pass_all_args']:
sub_context["arg"] = context["arg"]
sub_context["arg"].update(_parse_subcontext(tag.childNodes, context)["arg"])
except KeyError as e:
pass
_parse_launch(launch_tag.childNodes, sub_launch_file, file_deps, verbose, sub_context)
except IOError as e:
raise RoslaunchDepsException("Cannot load roslaunch include '%s' in '%s'"%(sub_launch_file, launch_file))
elif tag.tagName in ['node', 'test']:
try:
pkg, type = [resolve_args(tag.attributes[a].value, context) for a in ['pkg', 'type']]
except KeyError as e:
raise RoslaunchDepsException("Cannot load roslaunch <%s> tag: missing required attribute %s.\nXML is %s"%(tag.tagName, str(e), tag.toxml()))
if (pkg, type) not in file_deps[launch_file].nodes:
file_deps[launch_file].nodes.append((pkg, type))
# we actually want to include the package itself if that's referenced
#if launch_file_pkg != pkg:
if pkg not in file_deps[launch_file].pkgs:
file_deps[launch_file].pkgs.append(pkg)
def parse_launch(launch_file, file_deps, verbose):
if verbose:
print("processing launch %s"%launch_file)
try:
dom = parse(launch_file).getElementsByTagName('launch')
except:
raise RoslaunchDepsException("invalid XML in %s"%(launch_file))
if not len(dom):
raise RoslaunchDepsException("invalid XML in %s"%(launch_file))
file_deps[launch_file] = RoslaunchDeps()
launch_tag = dom[0]
context = { 'arg': {}}
_parse_launch(launch_tag.childNodes, launch_file, file_deps, verbose, context)
def rl_file_deps(file_deps, launch_file, verbose=False):
"""
Generate file_deps file dependency info for the specified
roslaunch file and its dependencies.
@param file_deps: dictionary mapping roslaunch filenames to
roslaunch dependency information. This dictionary will be
updated with dependency information.
@type file_deps: { str : RoslaunchDeps }
@param verbose: if True, print verbose output
@type verbose: bool
@param launch_file: name of roslaunch file
@type launch_file: str
"""
parse_launch(launch_file, file_deps, verbose)
def fullusage():
name = NAME
print("""Usage:
\t%(name)s [options] <file-or-package>
"""%locals())
def print_deps(base_pkg, file_deps, verbose):
pkgs = []
# for verbose output we print extra source information
if verbose:
for f, deps in file_deps.items():
for p, t in deps.nodes:
print("%s [%s/%s]"%(p, p, t))
pkg = rospkg.get_package_name(os.path.dirname(os.path.abspath(f)))
if pkg is None: #cannot determine package
print("ERROR: cannot determine package for [%s]"%pkg, file=sys.stderr)
else:
print("%s [%s]"%(pkg, f))
print('-'*80)
# print out list of package dependencies
pkgs = []
for deps in file_deps.values():
pkgs.extend(deps.pkgs)
# print space-separated to be friendly to rosmake
print(' '.join([p for p in set(pkgs)]))
def calculate_missing(base_pkg, missing, file_deps, use_test_depends=False):
"""
Calculate missing package dependencies in the manifest. This is
mainly used as a subroutine of roslaunch_deps().
@param base_pkg: name of package where initial walk begins (unused).
@type base_pkg: str
@param missing: dictionary mapping package names to set of missing package dependencies.
@type missing: { str: set(str) }
@param file_deps: dictionary mapping launch file names to RoslaunchDeps of each file
@type file_deps: { str: RoslaunchDeps}
@param use_test_depends [bool]: use test_depends as installed package
@type use_test_depends: [bool]
@return: missing (see parameter)
@rtype: { str: set(str) }
"""
rospack = rospkg.RosPack()
for launch_file in file_deps.keys():
pkg = rospkg.get_package_name(os.path.dirname(os.path.abspath(launch_file)))
if pkg is None: #cannot determine package
print("ERROR: cannot determine package for [%s]"%pkg, file=sys.stderr)
continue
m = rospack.get_manifest(pkg)
d_pkgs = set([d.name for d in m.depends])
if m.is_catkin:
# for catkin packages consider the run dependencies instead
# else not released packages will not appear in the dependency list
# since rospkg does uses rosdep to decide which dependencies to return
from catkin_pkg.package import parse_package
p = parse_package(os.path.dirname(m.filename))
d_pkgs = set([d.name for d in p.run_depends])
if use_test_depends:
for d in p.test_depends:
d_pkgs.add(d.name)
# make sure we don't count ourselves as a dep
d_pkgs.add(pkg)
diff = list(set(file_deps[launch_file].pkgs) - d_pkgs)
if not pkg in missing:
missing[pkg] = set()
missing[pkg].update(diff)
return missing
def roslaunch_deps(files, verbose=False, use_test_depends=False):
"""
@param packages: list of packages to check
@type packages: [str]
@param files [str]: list of roslaunch files to check. Must be in
same package.
@type files: [str]
@param use_test_depends [bool]: use test_depends as installed package
@type use_test_depends: [bool]
@return: base_pkg, file_deps, missing.
base_pkg is the package of all files
file_deps is a { filename : RoslaunchDeps } dictionary of
roslaunch dependency information to update, indexed by roslaunch
file name.
missing is a { package : [packages] } dictionary of missing
manifest dependencies, indexed by package.
@rtype: str, dict, dict
"""
file_deps = {}
missing = {}
base_pkg = None
for launch_file in files:
if not os.path.exists(launch_file):
raise RoslaunchDepsException("roslaunch file [%s] does not exist"%launch_file)
pkg = rospkg.get_package_name(os.path.dirname(os.path.abspath(launch_file)))
if base_pkg and pkg != base_pkg:
raise RoslaunchDepsException("roslaunch files must be in the same package: %s vs. %s"%(base_pkg, pkg))
base_pkg = pkg
rl_file_deps(file_deps, launch_file, verbose)
calculate_missing(base_pkg, missing, file_deps, use_test_depends=use_test_depends)
return base_pkg, file_deps, missing
def roslaunch_deps_main(argv=sys.argv):
from optparse import OptionParser
parser = OptionParser(usage="usage: %prog [options] <file(s)...>", prog=NAME)
parser.add_option("--verbose", "-v", action="store_true",
dest="verbose", default=False,
help="Verbose output")
parser.add_option("--warn", "-w", action="store_true",
dest="warn", default=False,
help="Warn about missing manifest dependencies")
(options, args) = parser.parse_args(argv[1:])
if not args:
parser.error('please specify a launch file')
files = [arg for arg in args if os.path.exists(arg)]
packages = [arg for arg in args if not os.path.exists(arg)]
if packages:
parser.error("cannot locate %s"%','.join(packages))
try:
base_pkg, file_deps, missing = roslaunch_deps(files, verbose=options.verbose)
except RoslaunchDepsException as e:
print(sys.stderr, str(e))
sys.exit(1)
if options.warn:
print("Dependencies:")
print_deps(base_pkg, file_deps, options.verbose)
if options.warn:
print('\nMissing declarations:')
for pkg, miss in missing.items():
if miss:
print("%s/manifest.xml:"%pkg)
for m in miss:
print(' <depend package="%s" />'%m)

View File

@@ -0,0 +1,714 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Revision $Id$
from __future__ import print_function
"""
Top-level implementation of launching processes. Coordinates
lower-level libraries.
"""
import os
import logging
import subprocess
import sys
import time
import traceback
import rosgraph
import rosgraph.names
import rosgraph.network
from roslaunch.core import *
#from roslaunch.core import setup_env
from roslaunch.nodeprocess import create_master_process, create_node_process
from roslaunch.pmon import start_process_monitor, ProcessListener
from roslaunch.rlutil import update_terminal_name
from rosmaster.master_api import NUM_WORKERS
_TIMEOUT_MASTER_START = 10.0 #seconds
_TIMEOUT_MASTER_STOP = 10.0 #seconds
_ID = '/roslaunch'
class RLTestTimeoutException(RLException): pass
def validate_master_launch(m, is_core, is_rostest=False):
"""
Validate the configuration of a master we are about to launch. Ths
validation already assumes that no existing master is running at
this configuration and merely checks configuration for a new
launch.
"""
# Before starting a master, we do some sanity check on the
# master configuration. There are two ways the user starts:
# roscore or roslaunch. If the user types roscore, we always
# will start a core if possible, but we warn about potential
# misconfigurations. If the user types roslaunch, we will
# auto-start a new master if it is achievable, i.e. if the
# master is local.
if not rosgraph.network.is_local_address(m.get_host()):
# The network configuration says that the intended
# hostname is not local, so...
if is_core:
# ...user is expecting to start a new master. Thus, we
# only warn if network configuration appears
# non-local.
try:
reverse_ips = [host[4][0] for host in socket.getaddrinfo(m.get_host(), 0, 0, 0, socket.SOL_TCP)]
local_addrs = rosgraph.network.get_local_addresses()
printerrlog("""WARNING: IP addresses %s for local hostname '%s' do not appear to match
any local IP address (%s). Your ROS nodes may fail to communicate.
Please use ROS_IP to set the correct IP address to use."""%(','.join(reverse_ips), m.get_host(), ','.join(local_addrs)))
except:
printerrlog("""WARNING: local hostname '%s' does not map to an IP address.
Your ROS nodes may fail to communicate.
Please use ROS_IP to set the correct IP address to use."""%(m.get_host()))
else:
# ... the remote master cannot be contacted, so we fail. #3097
raise RLException("ERROR: unable to contact ROS master at [%s]"%(m.uri))
if is_core and not is_rostest:
# User wants to start a master, and our configuration does
# point to the local host, and we're not running as rostest.
env_uri = rosgraph.get_master_uri()
env_host, env_port = rosgraph.network.parse_http_host_and_port(env_uri)
if not rosgraph.network.is_local_address(env_host):
# The ROS_MASTER_URI points to a different machine, warn user
printerrlog("WARNING: ROS_MASTER_URI [%s] host is not set to this machine"%(env_uri))
elif env_port != m.get_port():
# The ROS_MASTER_URI points to a master on a different port, warn user
printerrlog("WARNING: ROS_MASTER_URI port [%s] does not match this roscore [%s]"%(env_port, m.get_port()))
def _all_namespace_parents(p):
splits = [s for s in p.split(rosgraph.names.SEP) if s]
curr =rosgraph.names.SEP
parents = [curr]
for s in splits[:-1]:
next_ = curr + s + rosgraph.names.SEP
parents.append(next_)
curr = next_
return parents
def _unify_clear_params(params):
"""
Reduce clear_params such that only the highest-level namespaces
are represented for overlapping namespaces. e.g. if both /foo/ and
/foo/bar are present, return just /foo/.
@param params: paremter namees
@type params: [str]
@return: unified parameters
@rtype: [str]
"""
# note: this is a quick-and-dirty implementation
# canonicalize parameters
canon_params = []
for p in params:
if not p[-1] == rosgraph.names.SEP:
p += rosgraph.names.SEP
if not p in canon_params:
canon_params.append(p)
# reduce operation
clear_params = canon_params[:]
for p in canon_params:
for parent in _all_namespace_parents(p):
if parent in canon_params and p in clear_params and p != '/':
clear_params.remove(p)
return clear_params
def _hostname_to_rosname(hostname):
"""
Utility function to strip illegal characters from hostname. This
is implemented to be correct, not efficient."""
# prepend 'host_', which guarantees leading alpha character
fixed = 'host_'
# simplify comparison by making name lowercase
hostname = hostname.lower()
for c in hostname:
# only allow alphanumeric and numeral
if (c >= 'a' and c <= 'z') or \
(c >= '0' and c <= '9'):
fixed+=c
else:
fixed+='_'
return fixed
class _ROSLaunchListeners(ProcessListener):
"""
Helper class to manage distributing process events. It functions as
a higher level aggregator of events. In particular, events about
remote and local processes require different mechanisms for being
caught and reported.
"""
def __init__(self):
self.process_listeners = []
def add_process_listener(self, l):
"""
Add listener to list of listeners. Not threadsafe.
@param l: listener
@type l: L{ProcessListener}
"""
self.process_listeners.append(l)
def process_died(self, process_name, exit_code):
"""
ProcessListener callback
"""
for l in self.process_listeners:
try:
l.process_died(process_name, exit_code)
except Exception as e:
logging.getLogger('roslaunch').error(traceback.format_exc())
class ROSLaunchListener(object):
"""
Listener interface for events related to ROSLaunch.
ROSLaunchListener is currently identical to the lower-level
L{roslaunch.pmon.ProcessListener} interface, but is separate for
architectural reasons. The lower-level
L{roslaunch.pmon.ProcessMonitor} does not provide events about
remotely running processes.
"""
def process_died(self, process_name, exit_code):
"""
Notifies listener that process has died. This callback only
occurs for processes that die during normal process monitor
execution -- processes that are forcibly killed during
L{roslaunch.pmon.ProcessMonitor} shutdown are not reported.
@param process_name: name of process
@type process_name: str
@param exit_code int: exit code of process. If None, it means
that L{roslaunch.pmon.ProcessMonitor} was unable to determine an exit code.
@type exit_code: int
"""
pass
class ROSLaunchRunner(object):
"""
Runs a roslaunch. The normal sequence of API calls is L{launch()}
followed by L{spin()}. An external thread can call L{stop()}; otherwise
the runner will block until an exit signal. Another usage is to
call L{launch()} followed by repeated calls to L{spin_once()}. This usage
allows the main thread to continue to do work while processes are
monitored.
"""
def __init__(self, run_id, config, server_uri=None, pmon=None, is_core=False, remote_runner=None, is_child=False, is_rostest=False, num_workers=NUM_WORKERS, timeout=None):
"""
@param run_id: /run_id for this launch. If the core is not
running, this value will be used to initialize /run_id. If
the core is already running, this value will be checked
against the value stored on the core. L{ROSLaunchRunner} will
fail during L{launch()} if they do not match.
@type run_id: str
@param config: roslauch instance to run
@type config: L{ROSLaunchConfig}
@param server_uri: XML-RPC URI of roslaunch server.
@type server_uri: str
@param pmon: optionally override the process
monitor the runner uses for starting and tracking processes
@type pmon: L{ProcessMonitor}
@param is_core: if True, this runner is a roscore
instance. This affects the error behavior if a master is
already running -- aborts if is_core is True and a core is
detected.
@type is_core: bool
@param remote_runner: remote roslaunch process runner
@param is_rostest: if True, this runner is a rostest
instance. This affects certain validation checks.
@type is_rostest: bool
@param num_workers: If this is the core, the number of worker-threads to use.
@type num_workers: int
@param timeout: If this is the core, the socket-timeout to use.
@type timeout: Float or None
"""
if run_id is None:
raise RLException("run_id is None")
self.run_id = run_id
# In the future we should can separate the notion of a core
# config vs. the config being launched. In that way, we can
# start to migrate to a model where a config is a parameter to
# a launch() method, rather than a central thing to the
# runner.
self.config = config
self.server_uri = server_uri
self.is_child = is_child
self.is_core = is_core
self.is_rostest = is_rostest
self.num_workers = num_workers
self.timeout = timeout
self.logger = logging.getLogger('roslaunch')
self.pm = pmon or start_process_monitor()
# wire in ProcessMonitor events to our listeners
# aggregator. We similarly wire in the remote events when we
# create the remote runner.
self.listeners = _ROSLaunchListeners()
if self.pm is None:
raise RLException("unable to initialize roslaunch process monitor")
if self.pm.is_shutdown:
raise RLException("bad roslaunch process monitor initialization: process monitor is already dead")
self.pm.add_process_listener(self.listeners)
self.remote_runner = remote_runner
def add_process_listener(self, l):
"""
Add listener to list of listeners. Not threadsafe. Must be
called before processes started.
@param l: listener
@type l: L{ProcessListener}
"""
self.listeners.add_process_listener(l)
def _load_parameters(self):
"""
Load parameters onto the parameter server
"""
self.logger.info("load_parameters starting ...")
config = self.config
param_server = config.master.get()
p = None
try:
# multi-call style xmlrpc
param_server_multi = config.master.get_multi()
# clear specified parameter namespaces
# #2468 unify clear params to prevent error
for p in _unify_clear_params(config.clear_params):
if param_server.hasParam(_ID, p)[2]:
#printlog("deleting parameter [%s]"%p)
param_server_multi.deleteParam(_ID, p)
r = param_server_multi()
for code, msg, _ in r:
if code != 1:
raise RLException("Failed to clear parameter: %s"%(msg))
# multi-call objects are not reusable
param_server_multi = config.master.get_multi()
for p in config.params.values():
# suppressing this as it causes too much spam
#printlog("setting parameter [%s]"%p.key)
param_server_multi.setParam(_ID, p.key, p.value)
r = param_server_multi()
for code, msg, _ in r:
if code != 1:
raise RLException("Failed to set parameter: %s"%(msg))
except RLException:
raise
except Exception as e:
printerrlog("load_parameters: unable to set parameters (last param was [%s]): %s"%(p,e))
raise #re-raise as this is fatal
self.logger.info("... load_parameters complete")
def _launch_nodes(self):
"""
Launch all the declared nodes/master
@return: two lists of node names where the first
is the nodes that successfully launched and the second is the
nodes that failed to launch.
@rtype: [[str], [str]]
"""
config = self.config
succeeded = []
failed = []
self.logger.info("launch_nodes: launching local nodes ...")
local_nodes = config.nodes
# don't launch remote nodes
local_nodes = [n for n in config.nodes if is_machine_local(n.machine)]
for node in local_nodes:
proc, success = self.launch_node(node)
if success:
succeeded.append(str(proc))
else:
failed.append(str(proc))
if self.remote_runner:
self.logger.info("launch_nodes: launching remote nodes ...")
r_succ, r_fail = self.remote_runner.launch_remote_nodes()
succeeded.extend(r_succ)
failed.extend(r_fail)
self.logger.info("... launch_nodes complete")
return succeeded, failed
def _launch_master(self):
"""
Launches master if requested.
@return: True if a master was launched, False if a master was
already running.
@rtype: bool
@raise RLException: if master launch fails
"""
m = self.config.master
is_running = m.is_running()
if self.is_core and is_running:
raise RLException("roscore cannot run as another roscore/master is already running. \nPlease kill other roscore/master processes before relaunching.\nThe ROS_MASTER_URI is %s"%(m.uri))
if not is_running:
validate_master_launch(m, self.is_core, self.is_rostest)
printlog("auto-starting new master")
p = create_master_process(self.run_id, m.type, get_ros_root(), m.get_port(), self.num_workers, self.timeout)
self.pm.register_core_proc(p)
success = p.start()
if not success:
raise RLException("ERROR: unable to auto-start master process")
timeout_t = time.time() + _TIMEOUT_MASTER_START
while not m.is_running() and time.time() < timeout_t:
time.sleep(0.1)
if not m.is_running():
raise RLException("ERROR: could not contact master [%s]"%m.uri)
printlog_bold("ROS_MASTER_URI=%s"%m.uri)
# TODO: although this dependency doesn't cause anything bad,
# it's still odd for launch to know about console stuff. This
# really should be an event.
update_terminal_name(m.uri)
# Param Server config params
param_server = m.get()
# #773: unique run ID
self._check_and_set_run_id(param_server, self.run_id)
if self.server_uri:
# store parent XML-RPC URI on param server
# - param name is the /roslaunch/hostname:port so that multiple roslaunches can store at once
hostname, port = rosgraph.network.parse_http_host_and_port(self.server_uri)
hostname = _hostname_to_rosname(hostname)
self.logger.info("setting /roslaunch/uris/%s__%s' to %s"%(hostname, port, self.server_uri))
param_server.setParam(_ID, '/roslaunch/uris/%s__%s'%(hostname, port),self.server_uri)
return not is_running
def _check_and_set_run_id(self, param_server, run_id):
"""
Initialize self.run_id to existing value or setup parameter
server with /run_id set to default_run_id
@param default_run_id: run_id to use if value is not set
@type default_run_id: str
@param param_server: parameter server proxy
@type param_server: xmlrpclib.ServerProxy
"""
code, _, val = param_server.hasParam(_ID, '/run_id')
if code == 1 and not val:
printlog_bold("setting /run_id to %s"%run_id)
param_server.setParam('/roslaunch', '/run_id', run_id)
else:
# verify that the run_id we have been set to matches what's on the parameter server
code, _, val = param_server.getParam('/roslaunch', '/run_id')
if code != 1:
#could only happen in a bad race condition with
#someone else restarting core
raise RLException("ERROR: unable to retrieve /run_id from parameter server")
if run_id != val:
raise RLException("run_id on parameter server does not match declared run_id: %s vs %s"%(val, run_id))
#self.run_id = val
#printlog_bold("/run_id is %s"%self.run_id)
def _launch_executable(self, e):
"""
Launch a single L{Executable} object. Blocks until executable finishes.
@param e: Executable
@type e: L{Executable}
@raise RLException: if exectuable fails. Failure includes non-zero exit code.
"""
try:
#kwc: I'm still debating whether shell=True is proper
cmd = e.command
cmd = "%s %s"%(cmd, ' '.join(e.args))
print("running %s" % cmd)
local_machine = self.config.machines['']
env = setup_env(None, local_machine, self.config.master.uri)
retcode = subprocess.call(cmd, shell=True, env=env)
if retcode < 0:
raise RLException("command [%s] failed with exit code %s"%(cmd, retcode))
except OSError as e:
raise RLException("command [%s] failed: %s"%(cmd, e))
#TODO: _launch_run_executables, _launch_teardown_executables
#TODO: define and implement behavior for remote launch
def _launch_setup_executables(self):
"""
@raise RLException: if exectuable fails. Failure includes non-zero exit code.
"""
exes = [e for e in self.config.executables if e.phase == PHASE_SETUP]
for e in exes:
self._launch_executable(e)
def _launch_core_nodes(self):
"""
launch any core services that are not already running. master must
be already running
@raise RLException: if core launches fail
"""
config = self.config
master = config.master.get()
tolaunch = []
for node in config.nodes_core:
node_name = rosgraph.names.ns_join(node.namespace, node.name)
code, msg, _ = master.lookupNode(_ID, node_name)
if code == -1:
tolaunch.append(node)
elif code == 1:
print("core service [%s] found" % node_name)
else:
print("WARN: master is not behaving well (unexpected return value when looking up node)", file=sys.stderr)
self.logger.error("ERROR: master return [%s][%s] on lookupNode call"%(code,msg))
for node in tolaunch:
node_name = rosgraph.names.ns_join(node.namespace, node.name)
name, success = self.launch_node(node, core=True)
if success:
print("started core service [%s]" % node_name)
else:
raise RLException("failed to start core service [%s]"%node_name)
def launch_node(self, node, core=False):
"""
Launch a single node locally. Remote launching is handled separately by the remote module.
If node name is not assigned, one will be created for it.
@param node Node: node to launch
@param core bool: if True, core node
@return obj, bool: Process handle, successful launch. If success, return actual Process instance. Otherwise return name.
"""
self.logger.info("... preparing to launch node of type [%s/%s]", node.package, node.type)
# TODO: should this always override, per spec?. I added this
# so that this api can be called w/o having to go through an
# extra assign machines step.
if node.machine is None:
node.machine = self.config.machines['']
if node.name is None:
node.name = rosgraph.names.anonymous_name(node.type)
master = self.config.master
import roslaunch.node_args
try:
process = create_node_process(self.run_id, node, master.uri)
except roslaunch.node_args.NodeParamsException as e:
self.logger.error(e)
printerrlog("ERROR: cannot launch node of type [%s/%s]: %s"%(node.package, node.type, str(e)))
if node.name:
return node.name, False
else:
return "%s/%s"%(node.package,node.type), False
self.logger.info("... created process [%s]", process.name)
if core:
self.pm.register_core_proc(process)
else:
self.pm.register(process)
node.process_name = process.name #store this in the node object for easy reference
self.logger.info("... registered process [%s]", process.name)
# note: this may raise FatalProcessLaunch, which aborts the entire launch
success = process.start()
if not success:
if node.machine.name:
printerrlog("launch of %s/%s on %s failed"%(node.package, node.type, node.machine.name))
else:
printerrlog("local launch of %s/%s failed"%(node.package, node.type))
else:
self.logger.info("... successfully launched [%s]", process.name)
return process, success
def is_node_running(self, node):
"""
Check for running node process.
@param node Node: node object to check
@return bool: True if process associated with node is running (launched && !dead)
"""
#process_name is not set until node is launched.
return node.process_name and self.pm.has_process(node.process_name)
def spin_once(self):
"""
Same as spin() but only does one cycle. must be run from the main thread.
"""
if not self.pm:
return False
return self.pm.mainthread_spin_once()
def spin(self):
"""
spin() must be run from the main thread. spin() is very
important for roslaunch as it picks up jobs that the process
monitor need to be run in the main thread.
"""
self.logger.info("spin")
# #556: if we're just setting parameters and aren't launching
# any processes, exit.
if not self.pm or not self.pm.get_active_names():
printlog_bold("No processes to monitor")
self.stop()
return # no processes
self.pm.mainthread_spin()
#self.pm.join()
self.logger.info("process monitor is done spinning, initiating full shutdown")
self.stop()
printlog_bold("done")
def stop(self):
"""
Stop the launch and all associated processes. not thread-safe.
"""
self.logger.info("runner.stop()")
if self.pm is not None:
printlog("shutting down processing monitor...")
self.logger.info("shutting down processing monitor %s"%self.pm)
self.pm.shutdown()
self.pm.join()
self.pm = None
printlog("... shutting down processing monitor complete")
else:
self.logger.info("... roslaunch runner has already been stopped")
def _setup(self):
"""
Setup the state of the ROS network, including the parameter
server state and core services
"""
# this may have already been done, but do just in case
self.config.assign_machines()
if self.remote_runner:
# hook in our listener aggregator
self.remote_runner.add_process_listener(self.listeners)
# start up the core: master + core nodes defined in core.xml
launched = self._launch_master()
if launched:
self._launch_core_nodes()
# run exectuables marked as setup period. this will block
# until these executables exit. setup executable have to run
# *before* parameters are uploaded so that commands like
# rosparam delete can execute.
self._launch_setup_executables()
# no parameters for a child process
if not self.is_child:
self._load_parameters()
def launch(self):
"""
Run the launch. Depending on usage, caller should call
spin_once or spin as appropriate after launch().
@return ([str], [str]): tuple containing list of nodes that
successfully launches and list of nodes that failed to launch
@rtype: ([str], [str])
@raise RLException: if launch fails (e.g. run_id parameter does
not match ID on parameter server)
"""
try:
self._setup()
succeeded, failed = self._launch_nodes()
return succeeded, failed
except KeyboardInterrupt:
self.stop()
raise
def run_test(self, test):
"""
Run the test node. Blocks until completion or timeout.
@param test: test node to run
@type test: Test
@raise RLTestTimeoutException: if test fails to launch or test times out
"""
self.logger.info("... preparing to run test [%s] of type [%s/%s]", test.test_name, test.package, test.type)
proc_h, success = self.launch_node(test)
if not success:
raise RLException("test [%s] failed to launch"%test.test_name)
#poll until test terminates or alloted time exceed
timeout_t = time.time() + test.time_limit
pm = self.pm
while pm.mainthread_spin_once() and self.is_node_running(test):
#test fails on timeout
if time.time() > timeout_t:
raise RLTestTimeoutException(
"max time [%ss] allotted for test [%s] of type [%s/%s]" %
(test.time_limit, test.test_name, test.package, test.type))
time.sleep(0.1)
# NOTE: the mainly exists to prevent implicit circular dependency as
# the runner needs to invoke methods on the remote API, which depends
# on launch.
class ROSRemoteRunnerIF(object):
"""
API for remote running
"""
def __init__(self):
pass
def setup(self):
pass
def add_process_listener(self, l):
"""
Listen to events about remote processes dying. Not
threadsafe. Must be called before processes started.
@param l: listener
@type l: L{ProcessListener}
"""
pass
def launch_remote_nodes(self):
"""
Contact each child to launch remote nodes
@return: succeeded, failed
@rtype: [str], [str]
"""
pass

View File

@@ -0,0 +1,506 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Revision $Id$
"""
General routines and representations for loading roslaunch model.
"""
import os
from copy import deepcopy
from roslaunch.core import Param, RosbinExecutable, RLException, PHASE_SETUP
from rosgraph.names import make_global_ns, ns_join, PRIV_NAME, load_mappings, is_legal_name, canonicalize_name
#lazy-import global for yaml and rosparam
yaml = None
rosparam = None
class LoadException(RLException):
"""Error loading data as specified (e.g. cannot find included files, etc...)"""
pass
#TODO: lists, maps(?)
def convert_value(value, type_):
"""
Convert a value from a string representation into the specified
type
@param value: string representation of value
@type value: str
@param type_: int, double, string, bool, or auto
@type type_: str
@raise ValueError: if parameters are invalid
"""
type_ = type_.lower()
# currently don't support XML-RPC date, dateTime, maps, or list
# types
if type_ == 'auto':
#attempt numeric conversion
try:
if '.' in value:
return float(value)
else:
return int(value)
except ValueError as e:
pass
#bool
lval = value.lower()
if lval == 'true' or lval == 'false':
return convert_value(value, 'bool')
#string
return value
elif type_ == 'str' or type_ == 'string':
return value
elif type_ == 'int':
return int(value)
elif type_ == 'double':
return float(value)
elif type_ == 'bool' or type_ == 'boolean':
value = value.lower()
if value == 'true' or value == '1':
return True
elif value == 'false' or value == '0':
return False
raise ValueError("%s is not a '%s' type"%(value, type_))
else:
raise ValueError("Unknown type '%s'"%type_)
def process_include_args(context):
"""
Processes arg declarations in context and makes sure that they are
properly declared for passing into an included file. Also will
correctly setup the context for passing to the included file.
"""
# make sure all arguments have values. arg_names and resolve_dict
# are cleared when the context was initially created.
arg_dict = context.include_resolve_dict.get('arg', {})
for arg in context.arg_names:
if not arg in arg_dict:
raise LoadException("include args must have declared values")
# save args that were passed so we can check for unused args in post-processing
context.args_passed = list(arg_dict.keys())
# clear arg declarations so included file can re-declare
context.arg_names = []
# swap in new resolve dict for passing
context.resolve_dict = context.include_resolve_dict
context.include_resolve_dict = None
def post_process_include_args(context):
bad = [a for a in context.args_passed if a not in context.arg_names]
if bad:
raise LoadException("unused args [%s] for include of [%s]"%(', '.join(bad), context.filename))
def load_sysargs_into_context(context, argv):
"""
Load in ROS remapping arguments as arg assignments for context.
@param context: context to load into. context's resolve_dict for 'arg' will be reinitialized with values.
@type context: L{LoaderContext{
@param argv: command-line arguments
@type argv: [str]
"""
# we use same command-line spec as ROS nodes
mappings = load_mappings(argv)
context.resolve_dict['arg'] = mappings
class LoaderContext(object):
"""
Container for storing current loader context (e.g. namespace,
local parameter state, remapping state).
"""
def __init__(self, ns, filename, parent=None, params=None, env_args=None, \
resolve_dict=None, include_resolve_dict=None, arg_names=None):
"""
@param ns: namespace
@type ns: str
@param filename: name of file this is being loaded from
@type filename: str
@param resolve_dict: (optional) resolution dictionary for substitution args
@type resolve_dict: dict
@param include_resolve_dict: special resolution dictionary for
<include> tags. Must be None if this is not an <include>
context.
@type include_resolve_dict: dict
@param arg_names: name of args that have been declared in this context
@type arg_names: [str]
"""
self.parent = parent
self.ns = make_global_ns(ns or '/')
self._remap_args = []
self.params = params or []
self.env_args = env_args or []
self.filename = filename
# for substitution args
self.resolve_dict = resolve_dict or {}
# arg names. Args that have been declared in this context
self.arg_names = arg_names or []
# special scoped resolve dict for processing in <include> tag
self.include_resolve_dict = include_resolve_dict or None
# when this context was created via include, was pass_all_args set?
self.pass_all_args = False
def add_param(self, p):
"""
Add a ~param to the context. ~params are evaluated by any node
declarations that occur later in the same context.
@param p: parameter
@type p: L{Param}
"""
# override any params already set
matches = [m for m in self.params if m.key == p.key]
for m in matches:
self.params.remove(m)
self.params.append(p)
def add_remap(self, remap):
"""
Add a new remap setting to the context. if a remap already
exists with the same from key, it will be removed
@param remap: remap setting
@type remap: (str, str)
"""
remap = [canonicalize_name(x) for x in remap]
if not remap[0] or not remap[1]:
raise RLException("remap from/to attributes cannot be empty")
if not is_legal_name(remap[0]):
raise RLException("remap from [%s] is not a valid ROS name"%remap[0])
if not is_legal_name(remap[1]):
raise RLException("remap to [%s] is not a valid ROS name"%remap[1])
matches = [r for r in self._remap_args if r[0] == remap[0]]
for m in matches:
self._remap_args.remove(m)
self._remap_args.append(remap)
def add_arg(self, name, default=None, value=None, doc=None):
"""
Add 'arg' to existing context. Args are only valid for their immediate context.
"""
if name in self.arg_names:
# Ignore the duplication if pass_all_args was set
if not self.pass_all_args:
raise LoadException("arg '%s' has already been declared"%name)
else:
self.arg_names.append(name)
resolve_dict = self.resolve_dict if self.include_resolve_dict is None else self.include_resolve_dict
if not 'arg' in resolve_dict:
resolve_dict['arg'] = {}
arg_dict = resolve_dict['arg']
# args can only be declared once. they must also have one and
# only value at the time that they are declared.
if value is not None:
# value is set, error if declared in our arg dict as args
# with set values are constant/grounded.
# But don't error if pass_all_args was used to include this
# context; rather just override the passed-in value.
if name in arg_dict and not self.pass_all_args:
raise LoadException("cannot override arg '%s', which has already been set"%name)
arg_dict[name] = value
elif default is not None:
# assign value if not in context
if name not in arg_dict:
arg_dict[name] = default
else:
# no value or default: appending to arg_names is all we
# need to do as it declares the existence of the arg.
pass
# add arg documentation string dict if it doesn't exist yet and if it can be used
if not 'arg_doc' in resolve_dict:
resolve_dict['arg_doc'] = {}
arg_doc_dict = resolve_dict['arg_doc']
if not value:
# store the documentation for this argument
arg_doc_dict[name] = (doc, default)
def remap_args(self):
"""
@return: copy of the current remap arguments
@rtype: [(str, str)]
"""
if self.parent:
args = []
# filter out any parent remap args that have the same from key
for pr in self.parent.remap_args():
if not [r for r in self._remap_args if r[0] == pr[0]]:
args.append(pr)
args.extend(self._remap_args)
return args
return self._remap_args[:]
def include_child(self, ns, filename):
"""
Create child namespace based on include inheritance rules
@param ns: sub-namespace of child context, or None if the
child context shares the same namespace
@type ns: str
@param filename: name of include file
@type filename: str
@return: A child xml context that inherits from this context
@rtype: L{LoaderContext}jj
"""
ctx = self.child(ns)
# arg declarations are reset across include boundaries
ctx.arg_names = []
ctx.filename = filename
# keep the resolve_dict for now, we will do all new assignments into include_resolve_dict
ctx.include_resolve_dict = {}
#del ctx.resolve_dict['arg']
return ctx
def child(self, ns):
"""
@param ns: sub-namespace of child context, or None if the
child context shares the same namespace
@type ns: str
@return: A child xml context that inherits from this context
@rtype: L{LoaderContext}
"""
if ns:
if ns[0] == '/': # global (discouraged)
child_ns = ns
elif ns == PRIV_NAME: # ~name
# private names can only be scoped privately or globally
child_ns = PRIV_NAME
else:
child_ns = ns_join(self.ns, ns)
else:
child_ns = self.ns
return LoaderContext(child_ns, self.filename, parent=self,
params=self.params, env_args=self.env_args[:],
resolve_dict=deepcopy(self.resolve_dict),
arg_names=self.arg_names[:], include_resolve_dict=self.include_resolve_dict)
#TODO: in-progress refactorization. I'm slowly separating out
#non-XML-specific logic from xmlloader and moving into Loader. Soon
#this will mean that it will be easier to write coverage tests for
#lower-level logic.
class Loader(object):
"""
Lower-level library for loading ROS launch model. It provides an
abstraction between the representation (e.g. XML) and the
validation of the property values.
"""
def add_param(self, ros_config, param_name, param_value, verbose=True):
"""
Add L{Param} instances to launch config. Dictionary values are
unrolled into individual parameters.
@param ros_config: launch configuration
@type ros_config: L{ROSLaunchConfig}
@param param_name: name of parameter namespace to load values
into. If param_name is '/', param_value must be a dictionary
@type param_name: str
@param param_value: value to assign to param_name. If
param_value is a dictionary, it's values will be unrolled
into individual parameters.
@type param_value: str
@raise ValueError: if parameters cannot be processed into valid Params
"""
# shouldn't ever happen
if not param_name:
raise ValueError("no parameter name specified")
if param_name == '/' and type(param_value) != dict:
raise ValueError("Cannot load non-dictionary types into global namespace '/'")
if type(param_value) == dict:
# unroll params
for k, v in param_value.items():
self.add_param(ros_config, ns_join(param_name, k), v, verbose=verbose)
else:
ros_config.add_param(Param(param_name, param_value), verbose=verbose)
def load_rosparam(self, context, ros_config, cmd, param, file_, text, verbose=True, subst_function=None):
"""
Load rosparam setting
@param context: Loader context
@type context: L{LoaderContext}
@param ros_config: launch configuration
@type ros_config: L{ROSLaunchConfig}
@param cmd: 'load', 'dump', or 'delete'
@type cmd: str
@param file_: filename for rosparam to use or None
@type file_: str
@param text: text for rosparam to load. Ignored if file_ is set.
@type text: str
@raise ValueError: if parameters cannot be processed into valid rosparam setting
"""
if not cmd in ('load', 'dump', 'delete'):
raise ValueError("command must be 'load', 'dump', or 'delete'")
if file_ is not None:
if cmd == 'load' and not os.path.isfile(file_):
raise ValueError("file does not exist [%s]"%file_)
if cmd == 'delete':
raise ValueError("'file' attribute is invalid with 'delete' command.")
full_param = ns_join(context.ns, param) if param else context.ns
if cmd == 'dump':
ros_config.add_executable(RosbinExecutable('rosparam', (cmd, file_, full_param), PHASE_SETUP))
elif cmd == 'delete':
ros_config.add_executable(RosbinExecutable('rosparam', (cmd, full_param), PHASE_SETUP))
elif cmd == 'load':
# load YAML text
if file_:
with open(file_, 'r') as f:
text = f.read()
if subst_function is not None:
text = subst_function(text)
# parse YAML text
# - lazy import
global yaml
if yaml is None:
import yaml
# - lazy import: we have to import rosparam in oder to to configure the YAML constructors
global rosparam
if rosparam is None:
import rosparam
try:
data = yaml.load(text)
# #3162: if there is no YAML, load() will return an
# empty string. We want an empty dictionary instead
# for our representation of empty.
if data is None:
data = {}
except yaml.MarkedYAMLError as e:
if not file_:
raise ValueError("Error within YAML block:\n\t%s\n\nYAML is:\n%s"%(str(e), text))
else:
raise ValueError("file %s contains invalid YAML:\n%s"%(file_, str(e)))
except Exception as e:
if not file_:
raise ValueError("invalid YAML: %s\n\nYAML is:\n%s"%(str(e), text))
else:
raise ValueError("file %s contains invalid YAML:\n%s"%(file_, str(e)))
# 'param' attribute is required for non-dictionary types
if not param and type(data) != dict:
raise ValueError("'param' attribute must be set for non-dictionary values")
self.add_param(ros_config, full_param, data, verbose=verbose)
else:
raise ValueError("unknown command %s"%cmd)
def load_env(self, context, ros_config, name, value):
"""
Load environment variable setting
@param context: Loader context
@type context: L{LoaderContext}
@param ros_config: launch configuration
@type ros_config: L{ROSLaunchConfig}
@param name: environment variable name
@type name: str
@param value: environment variable value
@type value: str
"""
if not name:
raise ValueError("'name' attribute must be non-empty")
context.env_args.append((name, value))
def param_value(self, verbose, name, ptype, value, textfile, binfile, command):
"""
Parse text representation of param spec into Python value
@param name: param name, for error message use only
@type name: str
@param verbose: print verbose output
@type verbose: bool
@param textfile: name of text file to load from, or None
@type textfile: str
@param binfile: name of binary file to load from, or None
@type binfile: str
@param command: command to execute for parameter value, or None
@type command: str
@raise ValueError: if parameters are invalid
"""
if value is not None:
return convert_value(value.strip(), ptype)
elif textfile is not None:
with open(textfile, 'r') as f:
return f.read()
elif binfile is not None:
try:
from xmlrpc.client import Binary
except ImportError:
from xmlrpclib import Binary
with open(binfile, 'rb') as f:
return Binary(f.read())
elif command is not None:
try:
if type(command) == unicode:
command = command.encode('utf-8') #attempt to force to string for shlex/subprocess
except NameError:
pass
if verbose:
print("... executing command param [%s]" % command)
import subprocess, shlex #shlex rocks
try:
p = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE)
c_value = p.communicate()[0]
if not isinstance(c_value, str):
c_value = c_value.decode('utf-8')
if p.returncode != 0:
raise ValueError("Cannot load command parameter [%s]: command [%s] returned with code [%s]"%(name, command, p.returncode))
except OSError as e:
if e.errno == 2:
raise ValueError("Cannot load command parameter [%s]: no such command [%s]"%(name, command))
raise
if c_value is None:
raise ValueError("parameter: unable to get output of command [%s]"%command)
return c_value
else: #_param_tag prevalidates, so this should not be reachable
raise ValueError("unable to determine parameter value")

View File

@@ -0,0 +1,98 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Revision $Id$
"""
Convience methods for manipulating XML-RPC APIs
"""
try:
from xmlrpc.client import ServerProxy
except ImportError:
from xmlrpclib import ServerProxy
import rosgraph
import rosgraph.network
_ID = '/roslaunch_netapi'
def get_roslaunch_uris():
"""
@return: list of roslaunch XML-RPC URIs for roscore that's in
the current environment, or None if roscore cannot be contacted.
@rtype: [str]
"""
try:
m = rosgraph.Master(_ID)
vals = m.getParam('/roslaunch/uris')
return vals.values()
except rosgraph.MasterException:
return None
class NetProcess(object):
def __init__(self, name, respawn_count, is_alive, roslaunch_uri):
self.is_alive = is_alive
self.respawn_count = respawn_count
self.name = name
self.roslaunch_uri = roslaunch_uri
self.machine, _ = rosgraph.network.parse_http_host_and_port(roslaunch_uri)
def list_processes(roslaunch_uris=None):
"""
@param roslaunch_uris: (optional) list of XML-RPCS. If none
are provided, will look up URIs dynamically
@type roslaunch_uris: [str]
@return: list of roslaunch processes
@rtype: [L{NetProcess}]
"""
if not roslaunch_uris:
roslaunch_uris = get_roslaunch_uris()
if not roslaunch_uris:
return []
procs = []
for uri in roslaunch_uris:
try:
r = ServerProxy(uri)
code, msg, val = r.list_processes()
if code == 1:
active, dead = val
procs.extend([NetProcess(a[0], a[1], True, uri) for a in active])
procs.extend([NetProcess(d[0], d[1], False, uri) for d in dead])
except:
#import traceback
#traceback.print_exc()
# don't have a mechanism for reporting these errors upwards yet
pass
return procs

View File

@@ -0,0 +1,266 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2009, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Revision $Id$
from __future__ import print_function
"""
Utility module of roslaunch that computes the command-line arguments
for a node.
"""
import logging
import os
import shlex
import sys
import time
import rospkg
import rosgraph
import rosgraph.names
from rosgraph.names import script_resolve_name
import roslib.packages
from . import substitution_args
from roslaunch.core import setup_env, local_machine, RLException
from roslaunch.config import load_config_default
import roslaunch.xmlloader
class NodeParamsException(Exception):
"""
Exception to indicate that node parameters were invalid
"""
pass
def get_node_list(config):
"""
@param config: roslaunch config
@type config: ROSLaunchConfig
@return: list of node names in config
@rtype: [str]
"""
l = [_resolved_name(node) for node in config.nodes] + [_resolved_name(test) for test in config.tests]
# filter out unnamed nodes
return [x for x in l if x]
def print_node_list(roslaunch_files):
"""
Print list of nodes to screen. Will cause system exit if exception
occurs. This is a subroutine for the roslaunch main handler.
@param roslaunch_files: list of launch files to load
@type roslaunch_files: str
"""
try:
loader = roslaunch.xmlloader.XmlLoader(resolve_anon=False)
config = load_config_default(roslaunch_files, None, loader=loader, verbose=False, assign_machines=False)
node_list = get_node_list(config)
print('\n'.join(node_list))
except RLException as e:
print(str(e), file=sys.stderr)
sys.exit(1)
def print_node_args(node_name, roslaunch_files):
"""
Print arguments of node to screen. Will cause system exit if
exception occurs. This is a subroutine for the roslaunch main
handler.
@param node_name: node name
@type node_name: str
@param roslaunch_files: list of launch files to load
@type roslaunch_files: str
"""
try:
node_name = script_resolve_name('roslaunch', node_name)
args = get_node_args(node_name, roslaunch_files)
print(' '.join(args))
except RLException as e:
print(str(e), file=sys.stderr)
sys.exit(1)
def _resolved_name(node):
if node.name:
# $(anon id) passthrough
if node.name.startswith('$'):
return node.name
else:
return rosgraph.names.ns_join(node.namespace, node.name)
else:
return None
def print_node_filename(node_name, roslaunch_files):
try:
# #2309
node_name = script_resolve_name('roslaunch', node_name)
loader = roslaunch.xmlloader.XmlLoader(resolve_anon=False)
config = load_config_default(roslaunch_files, None, loader=loader, verbose=False, assign_machines=False)
nodes = [n for n in config.nodes if _resolved_name(n) == node_name] + \
[t for t in config.tests if _resolved_name(t) == node_name]
if len(nodes) > 1:
raise RLException("ERROR: multiple nodes named [%s] in [%s].\nPlease fix the launch files as duplicate names are not allowed."%(node_name, ', '.join(roslaunch_files)))
if not nodes:
print('ERROR: cannot find node named [%s]. Run \n\troslaunch --nodes <files>\nto see list of node names.' % (node_name), file=sys.stderr)
else:
print(nodes[0].filename)
except RLException as e:
print(str(e), file=sys.stderr)
sys.exit(1)
def get_node_args(node_name, roslaunch_files):
"""
Get the node arguments for a node in roslaunch_files.
@param node_name: name of node in roslaunch_files.
@type node_name: str
@param roslaunch_files: roslaunch file names
@type roslaunch_files: [str]
@return: list of command-line arguments used to launch node_name
@rtype: [str]
@raise RLException: if node args cannot be retrieved
"""
# we have to create our own XmlLoader so that we can use the same
# resolution context for substitution args
loader = roslaunch.xmlloader.XmlLoader(resolve_anon=False)
config = load_config_default(roslaunch_files, None, loader=loader, verbose=False, assign_machines=False)
(node_name) = substitution_args.resolve_args((node_name), resolve_anon=False)
node_name = script_resolve_name('roslaunch', node_name) if not node_name.startswith('$') else node_name
node = [n for n in config.nodes if _resolved_name(n) == node_name] + \
[n for n in config.tests if _resolved_name(n) == node_name]
if not node:
node_list = get_node_list(config)
node_list_str = '\n'.join([" * %s"%x for x in node_list])
raise RLException("ERROR: Cannot find node named [%s] in [%s].\nNode names are:\n%s"%(node_name, ', '.join(roslaunch_files), node_list_str))
elif len(node) > 1:
raise RLException("ERROR: multiple nodes named [%s] in [%s].\nPlease fix the launch files as duplicate names are not allowed."%(node_name, ', '.join(roslaunch_files)))
node = node[0]
master_uri = rosgraph.get_master_uri()
machine = local_machine()
env = setup_env(node, machine, master_uri)
# remove setting identical to current environment for easier debugging
to_remove = []
for k in env.keys():
if env[k] == os.environ.get(k, None):
to_remove.append(k)
for k in to_remove:
del env[k]
# resolve node name for generating args
args = create_local_process_args(node, machine)
# join environment vars are bash prefix args
return ["%s=%s"%(k, v) for k, v in env.items()] + args
def _launch_prefix_args(node):
if node.launch_prefix:
prefix = node.launch_prefix
try:
if type(prefix) == unicode:
prefix = prefix.encode('UTF-8')
except NameError:
pass
return shlex.split(prefix)
else:
return []
_rospack = None
def create_local_process_args(node, machine, env=None):
"""
Subroutine for creating node arguments.
:param env: override os.environ. Warning, this does not override
substitution args in node configuration (for now), ``dict``
:returns: arguments for node process, ``[str]``
:raises: :exc:`NodeParamsException` If args cannot be constructed for Node
as specified (e.g. the node type does not exist)
"""
global _rospack
if not node.name:
raise ValueError("node name must be defined")
# create rospack instance if no cached value is available or for custom environments
if not _rospack or env is not None:
rospack = rospkg.RosPack(rospkg.get_ros_paths(env=env))
# cache rospack instance for default environment
if env is None:
_rospack = rospack
else:
rospack = _rospack
# - Construct rosrun command
remap_args = ["%s:=%s"%(src,dst) for src, dst in node.remap_args]
resolve_dict = {}
#resolve args evaluates substitution commands
#shlex parses a command string into a list of args
# - for the local process args, we *do* resolve the anon tag so that the user can execute
# - the node name and args must be resolved together in case the args refer to the anon node name
(node_name) = substitution_args.resolve_args((node.name), context=resolve_dict, resolve_anon=True)
node.name = node_name
remap_args.append('__name:=%s'%node_name)
resolved = substitution_args.resolve_args(node.args, context=resolve_dict, resolve_anon=True)
try:
if type(resolved) == unicode:
resolved = resolved.encode('UTF-8') #attempt to force to string for shlex/subprocess
except NameError:
pass
args = shlex.split(resolved) + remap_args
try:
#TODO:fuerte: pass through rospack and catkin cache
matches = roslib.packages.find_node(node.package, node.type, rospack=rospack)
except rospkg.ResourceNotFound as e:
# multiple nodes, invalid package
raise NodeParamsException(str(e))
if not matches:
raise NodeParamsException("can't locate node [%s] in package [%s]"%(node.type, node.package))
else:
# old behavior was to take first, do we want to change this in Fuerte-style?
cmd = matches[0]
if not cmd:
raise NodeParamsException("Cannot locate node of type [%s] in package [%s]"%(node.type, node.package))
cmd = [cmd]
if sys.platform in ['win32']:
if os.path.splitext(cmd[0])[1] == '.py':
cmd = ['python'] + cmd
return _launch_prefix_args(node) + cmd + args

View File

@@ -0,0 +1,543 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Revision $Id$
"""
Local process implementation for running and monitoring nodes.
"""
import os
import signal
import subprocess
import time
import traceback
import rospkg
from roslaunch.core import *
#from roslaunch.core import setup_env
from roslaunch.node_args import create_local_process_args
from roslaunch.pmon import Process, FatalProcessLaunch
from rosmaster.master_api import NUM_WORKERS
import logging
_logger = logging.getLogger("roslaunch")
_TIMEOUT_SIGINT = 15.0 #seconds
_TIMEOUT_SIGTERM = 2.0 #seconds
_counter = 0
def _next_counter():
global _counter
_counter += 1
return _counter
def create_master_process(run_id, type_, ros_root, port, num_workers=NUM_WORKERS, timeout=None):
"""
Launch a master
@param type_: name of master executable (currently just Master.ZENMASTER)
@type type_: str
@param ros_root: ROS_ROOT environment setting
@type ros_root: str
@param port: port to launch master on
@type port: int
@param num_workers: number of worker threads.
@type num_workers: int
@param timeout: socket timeout for connections.
@type timeout: float
@raise RLException: if type_ or port is invalid
"""
if port < 1 or port > 65535:
raise RLException("invalid port assignment: %s"%port)
_logger.info("create_master_process: %s, %s, %s, %s, %s", type_, ros_root, port, num_workers, timeout)
# catkin/fuerte: no longer use ROS_ROOT-relative executables, search path instead
master = type_
# zenmaster is deprecated and aliased to rosmaster
if type_ in [Master.ROSMASTER, Master.ZENMASTER]:
package = 'rosmaster'
args = [master, '--core', '-p', str(port), '-w', str(num_workers)]
if timeout is not None:
args += ['-t', str(timeout)]
else:
raise RLException("unknown master typ_: %s"%type_)
_logger.info("process[master]: launching with args [%s]"%args)
log_output = False
return LocalProcess(run_id, package, 'master', args, os.environ, log_output, None, required=True)
def create_node_process(run_id, node, master_uri):
"""
Factory for generating processes for launching local ROS
nodes. Also registers the process with the L{ProcessMonitor} so that
events can be generated when the process dies.
@param run_id: run_id of launch
@type run_id: str
@param node: node to launch. Node name must be assigned.
@type node: L{Node}
@param master_uri: API URI for master node
@type master_uri: str
@return: local process instance
@rtype: L{LocalProcess}
@raise NodeParamsException: If the node's parameters are improperly specific
"""
_logger.info("create_node_process: package[%s] type[%s] machine[%s] master_uri[%s]", node.package, node.type, node.machine, master_uri)
# check input args
machine = node.machine
if machine is None:
raise RLException("Internal error: no machine selected for node of type [%s/%s]"%(node.package, node.type))
if not node.name:
raise ValueError("node name must be assigned")
# - setup env for process (vars must be strings for os.environ)
env = setup_env(node, machine, master_uri)
if not node.name:
raise ValueError("node name must be assigned")
# we have to include the counter to prevent potential name
# collisions between the two branches
name = "%s-%s"%(rosgraph.names.ns_join(node.namespace, node.name), _next_counter())
if name[0] == '/':
name = name[1:]
_logger.info('process[%s]: env[%s]', name, env)
args = create_local_process_args(node, machine)
_logger.info('process[%s]: args[%s]', name, args)
# default for node.output not set is 'log'
log_output = node.output != 'screen'
_logger.debug('process[%s]: returning LocalProcess wrapper')
return LocalProcess(run_id, node.package, name, args, env, log_output, \
respawn=node.respawn, respawn_delay=node.respawn_delay, \
required=node.required, cwd=node.cwd)
class LocalProcess(Process):
"""
Process launched on local machine
"""
def __init__(self, run_id, package, name, args, env, log_output,
respawn=False, respawn_delay=0.0, required=False, cwd=None,
is_node=True):
"""
@param run_id: unique run ID for this roslaunch. Used to
generate log directory location. run_id may be None if this
feature is not being used.
@type run_id: str
@param package: name of package process is part of
@type package: str
@param name: name of process
@type name: str
@param args: list of arguments to process
@type args: [str]
@param env: environment dictionary for process
@type env: {str : str}
@param log_output: if True, log output streams of process
@type log_output: bool
@param respawn: respawn process if it dies (default is False)
@type respawn: bool
@param respawn_delay: respawn process after a delay
@type respawn_delay: float
@param cwd: working directory of process, or None
@type cwd: str
@param is_node: (optional) if True, process is ROS node and accepts ROS node command-line arguments. Default: True
@type is_node: False
"""
super(LocalProcess, self).__init__(package, name, args, env,
respawn, respawn_delay, required)
self.run_id = run_id
self.popen = None
self.log_output = log_output
self.started = False
self.stopped = False
self.cwd = cwd
self.log_dir = None
self.pid = -1
self.is_node = is_node
# NOTE: in the future, info() is going to have to be sufficient for relaunching a process
def get_info(self):
"""
Get all data about this process in dictionary form
"""
info = super(LocalProcess, self).get_info()
info['pid'] = self.pid
if self.run_id:
info['run_id'] = self.run_id
info['log_output'] = self.log_output
if self.cwd is not None:
info['cwd'] = self.cwd
return info
def _configure_logging(self):
"""
Configure logging of node's log file and stdout/stderr
@return: stdout log file name, stderr log file
name. Values are None if stdout/stderr are not logged.
@rtype: str, str
"""
log_dir = rospkg.get_log_dir(env=os.environ)
if self.run_id:
log_dir = os.path.join(log_dir, self.run_id)
if not os.path.exists(log_dir):
try:
os.makedirs(log_dir)
except OSError as e:
if e.errno == 13:
raise RLException("unable to create directory for log file [%s].\nPlease check permissions."%log_dir)
else:
raise RLException("unable to create directory for log file [%s]: %s"%(log_dir, e.strerror))
# #973: save log dir for error messages
self.log_dir = log_dir
# send stdout/stderr to file. in the case of respawning, we have to
# open in append mode
# note: logfileerr: disabling in favor of stderr appearing in the console.
# will likely reinstate once roserr/rosout is more properly used.
logfileout = logfileerr = None
logfname = self._log_name()
if self.log_output:
outf, errf = [os.path.join(log_dir, '%s-%s.log'%(logfname, n)) for n in ['stdout', 'stderr']]
if self.respawn:
mode = 'a'
else:
mode = 'w'
logfileout = open(outf, mode)
if is_child_mode():
logfileerr = open(errf, mode)
# #986: pass in logfile name to node
node_log_file = log_dir
if self.is_node:
# #1595: on respawn, these keep appending
self.args = _cleanup_remappings(self.args, '__log:=')
self.args.append("__log:=%s"%os.path.join(log_dir, "%s.log"%(logfname)))
return logfileout, logfileerr
def start(self):
"""
Start the process.
@raise FatalProcessLaunch: if process cannot be started and it
is not likely to ever succeed
"""
super(LocalProcess, self).start()
try:
self.lock.acquire()
if self.started:
_logger.info("process[%s]: restarting os process", self.name)
else:
_logger.info("process[%s]: starting os process", self.name)
self.started = self.stopped = False
full_env = self.env
# _configure_logging() can mutate self.args
try:
logfileout, logfileerr = self._configure_logging()
except Exception as e:
_logger.error(traceback.format_exc())
printerrlog("[%s] ERROR: unable to configure logging [%s]"%(self.name, str(e)))
# it's not safe to inherit from this process as
# rostest changes stdout to a StringIO, which is not a
# proper file.
logfileout, logfileerr = subprocess.PIPE, subprocess.PIPE
if self.cwd == 'node':
cwd = os.path.dirname(self.args[0])
elif self.cwd == 'cwd':
cwd = os.getcwd()
elif self.cwd == 'ros-root':
cwd = get_ros_root()
else:
cwd = rospkg.get_ros_home()
if not os.path.exists(cwd):
try:
os.makedirs(cwd)
except OSError:
# exist_ok=True
pass
_logger.info("process[%s]: start w/ args [%s]", self.name, self.args)
_logger.info("process[%s]: cwd will be [%s]", self.name, cwd)
try:
self.popen = subprocess.Popen(self.args, cwd=cwd, stdout=logfileout, stderr=logfileerr, env=full_env, close_fds=True, preexec_fn=os.setsid)
except OSError as e:
self.started = True # must set so is_alive state is correct
_logger.error("OSError(%d, %s)", e.errno, e.strerror)
if e.errno == 8: #Exec format error
raise FatalProcessLaunch("Unable to launch [%s]. \nIf it is a script, you may be missing a '#!' declaration at the top."%self.name)
elif e.errno == 2: #no such file or directory
raise FatalProcessLaunch("""Roslaunch got a '%s' error while attempting to run:
%s
Please make sure that all the executables in this command exist and have
executable permission. This is often caused by a bad launch-prefix."""%(e.strerror, ' '.join(self.args)))
else:
raise FatalProcessLaunch("unable to launch [%s]: %s"%(' '.join(self.args), e.strerror))
self.started = True
# Check that the process is either still running (poll returns
# None) or that it completed successfully since when we
# launched it above (poll returns the return code, 0).
poll_result = self.popen.poll()
if poll_result is None or poll_result == 0:
self.pid = self.popen.pid
printlog_bold("process[%s]: started with pid [%s]"%(self.name, self.pid))
return True
else:
printerrlog("failed to start local process: %s"%(' '.join(self.args)))
return False
finally:
self.lock.release()
def _log_name(self):
return self.name.replace('/', '-')
def is_alive(self):
"""
@return: True if process is still running
@rtype: bool
"""
if not self.started: #not started yet
return True
if self.stopped or self.popen is None:
if self.time_of_death is None:
self.time_of_death = time.time()
return False
self.exit_code = self.popen.poll()
if self.exit_code is not None:
if self.time_of_death is None:
self.time_of_death = time.time()
return False
return True
def get_exit_description(self):
"""
@return: human-readable description of exit state
@rtype: str
"""
if self.exit_code is None:
output = 'process has died without exit code [pid %s, cmd %s].'%(self.pid, ' '.join(self.args))
elif self.exit_code != 0:
output = 'process has died [pid %s, exit code %s, cmd %s].'%(self.pid, self.exit_code, ' '.join(self.args))
else:
output = 'process has finished cleanly'
if self.log_dir:
# #973: include location of output location in message
output += '\nlog file: %s*.log'%(os.path.join(self.log_dir, self._log_name()))
return output
def _stop_unix(self, errors):
"""
UNIX implementation of process killing
@param errors: error messages. stop() will record messages into this list.
@type errors: [str]
"""
self.exit_code = self.popen.poll()
if self.exit_code is not None:
_logger.debug("process[%s].stop(): process has already returned %s", self.name, self.exit_code)
#print "process[%s].stop(): process has already returned %s"%(self.name, self.exit_code)
self.popen = None
self.stopped = True
return
pid = self.popen.pid
pgid = os.getpgid(pid)
_logger.info("process[%s]: killing os process with pid[%s] pgid[%s]", self.name, pid, pgid)
try:
# Start with SIGINT and escalate from there.
_logger.info("[%s] sending SIGINT to pgid [%s]", self.name, pgid)
os.killpg(pgid, signal.SIGINT)
_logger.info("[%s] sent SIGINT to pgid [%s]", self.name, pgid)
timeout_t = time.time() + _TIMEOUT_SIGINT
retcode = self.popen.poll()
while time.time() < timeout_t and retcode is None:
time.sleep(0.1)
retcode = self.popen.poll()
# Escalate non-responsive process
if retcode is None:
printerrlog("[%s] escalating to SIGTERM"%self.name)
timeout_t = time.time() + _TIMEOUT_SIGTERM
os.killpg(pgid, signal.SIGTERM)
_logger.info("[%s] sent SIGTERM to pgid [%s]"%(self.name, pgid))
retcode = self.popen.poll()
while time.time() < timeout_t and retcode is None:
time.sleep(0.2)
_logger.debug('poll for retcode')
retcode = self.popen.poll()
if retcode is None:
printerrlog("[%s] escalating to SIGKILL"%self.name)
errors.append("process[%s, pid %s]: required SIGKILL. May still be running."%(self.name, pid))
try:
os.killpg(pgid, signal.SIGKILL)
_logger.info("[%s] sent SIGKILL to pgid [%s]"%(self.name, pgid))
# #2096: don't block on SIGKILL, because this results in more orphaned processes overall
#self.popen.wait()
#os.wait()
_logger.info("process[%s]: sent SIGKILL", self.name)
except OSError as e:
if e.args[0] == 3:
printerrlog("no [%s] process with pid [%s]"%(self.name, pid))
else:
printerrlog("errors shutting down [%s], see log for details"%self.name)
_logger.error(traceback.format_exc())
else:
_logger.info("process[%s]: SIGTERM killed with return value %s", self.name, retcode)
else:
_logger.info("process[%s]: SIGINT killed with return value %s", self.name, retcode)
finally:
self.popen = None
def _stop_win32(self, errors):
"""
Win32 implementation of process killing. In part, refer to
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/347462
Note that it doesn't work as completely as _stop_unix as it can't utilise
group id's. This means that any program which forks children underneath it
won't get caught by this kill mechanism.
@param errors: error messages. stop() will record messages into this list.
@type errors: [str]
"""
self.exit_code = self.popen.poll()
if self.exit_code is not None:
_logger.debug("process[%s].stop(): process has already returned %s", self.name, self.exit_code)
self.popen = None
self.stopped = True
return
pid = self.popen.pid
_logger.info("process[%s]: killing os process/subprocesses with pid[%s]", self.name, pid)
# windows has no group id's :(
try:
# Start with SIGINT and escalate from there.
_logger.info("[%s] sending SIGINT to pgid [%s]", self.name, pid)
os.kill(pid, signal.SIGINT)
_logger.info("[%s] sent SIGINT to pgid [%s]", self.name, pid)
timeout_t = time.time() + _TIMEOUT_SIGINT
retcode = self.popen.poll()
while time.time() < timeout_t and retcode is None:
time.sleep(0.1)
retcode = self.popen.poll()
# Escalate non-responsive process
if retcode is None:
printerrlog("[%s] escalating to SIGTERM"%self.name)
timeout_t = time.time() + _TIMEOUT_SIGTERM
os.killpg(pid, signal.SIGTERM)
_logger.info("[%s] sent SIGTERM to pid [%s]"%(self.name, pid))
retcode = self.popen.poll()
while time.time() < timeout_t and retcode is None:
time.sleep(0.2)
_logger.debug('poll for retcode')
retcode = self.popen.poll()
if retcode is None:
printerrlog("[%s] escalating to SIGKILL"%self.name)
errors.append("process[%s, pid %s]: required SIGKILL. May still be running."%(self.name, pid))
try:
os.killpg(pid, signal.SIGKILL)
_logger.info("[%s] sent SIGKILL to pid [%s]"%(self.name, pid))
# #2096: don't block on SIGKILL, because this results in more orphaned processes overall
#self.popen.wait()
#os.wait()
_logger.info("process[%s]: sent SIGKILL", self.name)
except OSError as e:
if e.args[0] == 3:
printerrlog("no [%s] process with pid [%s]"%(self.name, pid))
else:
printerrlog("errors shutting down [%s], see log for details"%self.name)
_logger.error(traceback.format_exc())
else:
_logger.info("process[%s]: SIGTERM killed with return value %s", self.name, retcode)
else:
_logger.info("process[%s]: SIGINT killed with return value %s", self.name, retcode)
finally:
self.popen = None
def stop(self, errors=None):
"""
Stop the process. Record any significant error messages in the errors parameter
@param errors: error messages. stop() will record messages into this list.
@type errors: [str]
"""
if errors is None:
errors = []
super(LocalProcess, self).stop(errors)
self.lock.acquire()
try:
try:
_logger.debug("process[%s].stop() starting", self.name)
if self.popen is None:
_logger.debug("process[%s].stop(): popen is None, nothing to kill")
return
if sys.platform in ['win32']: # cygwin seems to be ok
self._stop_win32(errors)
else:
self._stop_unix(errors)
except:
#traceback.print_exc()
_logger.error("[%s] EXCEPTION %s", self.name, traceback.format_exc())
finally:
self.stopped = True
self.lock.release()
# #1595
def _cleanup_remappings(args, prefix):
"""
Remove all instances of args that start with prefix. This is used
to remove args that were previously added (and are now being
regenerated due to respawning)
"""
existing_args = [a for a in args if a.startswith(prefix)]
for a in existing_args:
args.remove(a)
return args

View File

@@ -0,0 +1,69 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2010, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
"""
Dumps parameters from ROSLaunch files to stdout as YAML.
"""
import sys
import roslaunch.config
import roslaunch.xmlloader
import yaml
def dump_params(files):
"""
Dumps ROS parameters of a list of files to STDOUT in YAML
@param files: List of ROSLaunch files to load
@type files: [ str ]
@return: True if loaded parameters successfully
@rtype: bool
"""
config = roslaunch.config.ROSLaunchConfig()
loader = roslaunch.xmlloader.XmlLoader()
for f in files:
try:
loader.load(f, config, verbose = False)
except Exception as e:
sys.stderr.write("Unable to load file %s: %s" % (f, e))
return False
# Now print params in YAML format.
params_dict = {}
for k, v in config.params.items():
params_dict[str(k)] = v.value
sys.stdout.write(yaml.safe_dump(params_dict)+'\n')
return True

View File

@@ -0,0 +1,316 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Revision $Id$
"""
roslaunch.parent providees the L{ROSLaunchParent} implementation,
which represents the main 'parent' roslaunch process.
ROSLaunch has a client/server architecture for running remote
processes. When a user runs roslaunch, this creates a "parent"
roslaunch process, which is responsible for managing local
processes. This parent process will also start "child" processes on
remote machines. The parent can then invoke methods on this child
process to launch remote processes, and the child can invoke methods
on the parent to provide feedback.
"""
import logging
import roslaunch.config
from roslaunch.core import printlog_bold, printerrlog, RLException
import roslaunch.launch
import roslaunch.pmon
import roslaunch.server
import roslaunch.xmlloader
from rosmaster.master_api import NUM_WORKERS
#TODO: probably move process listener infrastructure into here
# TODO: remove after wg_hardware_roslaunch has been updated
# qualification accesses this API, which has been relocated
load_config_default = roslaunch.config.load_config_default
class ROSLaunchParent(object):
"""
ROSLaunchParent represents the main 'parent' roslaunch process. It
is responsible for loading the launch files, assigning machines,
and then starting up any remote processes. The __main__ method
delegates most of runtime to ROSLaunchParent.
This must be called from the Python Main thread due to signal registration.
"""
def __init__(self, run_id, roslaunch_files, is_core=False, port=None, local_only=False, process_listeners=None,
verbose=False, force_screen=False, is_rostest=False, roslaunch_strs=None, num_workers=NUM_WORKERS, timeout=None):
"""
@param run_id: UUID of roslaunch session
@type run_id: str
@param roslaunch_files: list of launch configuration
files to load
@type roslaunch_files: [str]
@param is_core bool: if True, this launch is a roscore
instance. This affects the error behavior if a master is
already running (i.e. it fails).
@type is_core: bool
@param process_listeners: (optional) list of process listeners
to register with process monitor once launch is running
@type process_listeners: [L{roslaunch.pmon.ProcessListener}]
@param port: (optional) override master port number from what is specified in the master URI.
@type port: int
@param verbose: (optional) print verbose output
@type verbose: boolean
@param force_screen: (optional) force output of all nodes to screen
@type force_screen: boolean
@param is_rostest bool: if True, this launch is a rostest
instance. This affects validation checks.
@type is_rostest: bool
@param num_workers: If this is the core, the number of worker-threads to use.
@type num_workers: int
@param timeout: If this is the core, the socket-timeout to use.
@type timeout: Float or None
@throws RLException
"""
self.logger = logging.getLogger('roslaunch.parent')
self.run_id = run_id
self.process_listeners = process_listeners
self.roslaunch_files = roslaunch_files
self.roslaunch_strs = roslaunch_strs
self.is_core = is_core
self.is_rostest = is_rostest
self.port = port
self.local_only = local_only
self.verbose = verbose
self.num_workers = num_workers
self.timeout = timeout
# I don't think we should have to pass in so many options from
# the outside into the roslaunch parent. One possibility is to
# allow alternate config loaders to be passed in.
self.force_screen = force_screen
# flag to prevent multiple shutdown attempts
self._shutting_down = False
self.config = self.runner = self.server = self.pm = self.remote_runner = None
def _load_config(self):
self.config = roslaunch.config.load_config_default(self.roslaunch_files, self.port,
roslaunch_strs=self.roslaunch_strs, verbose=self.verbose)
# #2370 (I really want to move this logic outside of parent)
if self.force_screen:
for n in self.config.nodes:
n.output = 'screen'
def _start_pm(self):
"""
Start the process monitor
"""
self.pm = roslaunch.pmon.start_process_monitor()
def _init_runner(self):
"""
Initialize the roslaunch runner
"""
if self.config is None:
raise RLException("config is not initialized")
if self.pm is None:
raise RLException("pm is not initialized")
if self.server is None:
raise RLException("server is not initialized")
self.runner = roslaunch.launch.ROSLaunchRunner(self.run_id, self.config, server_uri=self.server.uri, pmon=self.pm, is_core=self.is_core, remote_runner=self.remote_runner, is_rostest=self.is_rostest, num_workers=self.num_workers, timeout=self.timeout)
# print runner info to user, put errors last to make the more visible
if self.is_core:
print("ros_comm version %s" % (self.config.params['/rosversion'].value))
print(self.config.summary(local=self.remote_runner is None))
if self.config:
for err in self.config.config_errors:
printerrlog("WARNING: %s"%err)
def _start_server(self):
"""
Initialize the roslaunch parent XML-RPC server
"""
if self.config is None:
raise RLException("config is not initialized")
if self.pm is None:
raise RLException("pm is not initialized")
self.logger.info("starting parent XML-RPC server")
self.server = roslaunch.server.ROSLaunchParentNode(self.config, self.pm)
self.server.start()
if not self.server.uri:
raise RLException("server URI did not initialize")
self.logger.info("... parent XML-RPC server started")
def _init_remote(self):
"""
Initialize the remote process runner, if required. Subroutine
of _start_remote, separated out for easier testing
"""
if self.config is None:
raise RLException("config is not initialized")
if self.pm is None:
raise RLException("pm is not initialized")
if self.server is None:
raise RLException("server is not initialized")
if not self.local_only and self.config.has_remote_nodes():
# keep the remote package lazy-imported
import roslaunch.remote
self.remote_runner = roslaunch.remote.ROSRemoteRunner(self.run_id, self.config, self.pm, self.server)
elif self.local_only:
printlog_bold("LOCAL\nlocal only launch specified, will not launch remote nodes\nLOCAL\n")
def _start_remote(self):
"""
Initializes and runs the remote process runner, if required
"""
if self.remote_runner is None:
self._init_remote()
if self.remote_runner is not None:
# start_servers() runs the roslaunch children
self.remote_runner.start_children()
def _start_infrastructure(self):
"""
load config, start XMLRPC servers and process monitor
"""
if self.config is None:
self._load_config()
# Start the process monitor
if self.pm is None:
self._start_pm()
# Startup the roslaunch runner and XMLRPC server.
# Requires pm
if self.server is None:
self._start_server()
# Startup the remote infrastructure.
# Requires config, pm, and server
self._start_remote()
def _stop_infrastructure(self):
"""
Tear down server and process monitor. Not multithread safe.
"""
#TODO more explicit teardown of remote infrastructure
# test and set flag so we don't shutdown multiple times
if self._shutting_down:
return
self._shutting_down = True
if self.server:
try:
self.server.shutdown("roslaunch parent complete")
except:
# don't let exceptions halt the rest of the shutdown
pass
if self.pm:
self.pm.shutdown()
self.pm.join()
def start(self, auto_terminate=True):
"""
Run the parent roslaunch.
@param auto_terminate: stop process monitor once there are no
more processes to monitor (default True). This defaults to
True, which is the command-line behavior of roslaunch. Scripts
may wish to set this to False if they wish to keep the
roslauch infrastructure up regardless of processes being
monitored.
"""
self.logger.info("starting roslaunch parent run")
# load config, start XMLRPC servers and process monitor
try:
self._start_infrastructure()
except:
# infrastructure did not initialize, do teardown on whatever did come up
self._stop_infrastructure()
raise
# Initialize the actual runner.
# Requires config, pm, server and remote_runner
self._init_runner()
# Start the launch
self.runner.launch()
# inform process monitor that we are done with process registration
if auto_terminate:
self.pm.registrations_complete()
self.logger.info("... roslaunch parent running, waiting for process exit")
if self.process_listeners:
for l in self.process_listeners:
self.runner.pm.add_process_listener(l)
# Add listeners to server as well, otherwise they won't be
# called when a node on a remote machine dies.
self.server.add_process_listener(l)
def spin_once(self):
"""
Run the parent roslaunch event loop once
"""
if self.runner:
self.runner.spin_once()
def spin(self):
"""
Run the parent roslaunch until exit
"""
if not self.runner:
raise RLException("parent not started yet")
try:
# Blocks until all processes dead/shutdown
self.runner.spin()
finally:
self._stop_infrastructure()
def shutdown(self):
"""
Stop the parent roslaunch.
"""
self._stop_infrastructure()

View File

@@ -0,0 +1,719 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Revision $Id$
from __future__ import print_function
from __future__ import with_statement
"""
Process monitoring implementation for roslaunch.
"""
import os
import sys
import time
import traceback
import logging
try:
from queue import Empty, Queue
except ImportError:
from Queue import Empty, Queue
import signal
import atexit
from threading import Thread, RLock, Lock
import roslib
from roslaunch.core import printlog, printlog_bold, printerrlog, RLException
logger = logging.getLogger("roslaunch.pmon")
class FatalProcessLaunch(RLException):
"""
Exception to indicate that a process launch has failed in a fatal
manner (i.e. relaunch is unlikely to succeed)
"""
pass
# start/shutdown ################################################
_pmons = []
_pmon_counter = 0
def start_process_monitor():
global _pmon_counter
if _shutting_down:
#logger.error("start_process_monitor: cannot start new ProcessMonitor (shutdown initiated)")
return None
_pmon_counter += 1
name = "ProcessMonitor-%s"%_pmon_counter
logger.info("start_process_monitor: creating ProcessMonitor")
process_monitor = ProcessMonitor(name)
try:
# prevent race condition with pmon_shutdown() being triggered
# as we are starting a ProcessMonitor (i.e. user hits ctrl-C
# during startup)
_shutdown_lock.acquire()
_pmons.append(process_monitor)
process_monitor.start()
logger.info("start_process_monitor: ProcessMonitor started")
finally:
_shutdown_lock.release()
return process_monitor
def shutdown_process_monitor(process_monitor):
"""
@param process_monitor: process monitor to kill
@type process_monitor: L{ProcessMonitor}
@return: True if process_monitor was successfully
shutdown. False if it could not be shutdown cleanly or if there is
a problem with process_monitor
parameter. shutdown_process_monitor() does not throw any exceptions
as this is shutdown-critical code.
@rtype: bool
"""
try:
if process_monitor is None or process_monitor.is_shutdown:
return False
# I've decided to comment out use of logger until after
# critical section is done, just in case logger is already
# being torn down
#logger.debug("shutdown_process_monitor: shutting down ProcessMonitor")
process_monitor.shutdown()
#logger.debug("shutdown_process_monitor: joining ProcessMonitor")
process_monitor.join(20.0)
if process_monitor.isAlive():
logger.error("shutdown_process_monitor: ProcessMonitor shutdown failed!")
return False
else:
logger.debug("shutdown_process_monitor: ProcessMonitor shutdown succeeded")
return True
except Exception as e:
print("exception in shutdown_process_monitor: %s" % e, file=sys.stderr)
traceback.print_exc()
return False
_shutdown_lock = Lock()
def pmon_shutdown():
global _pmons
try:
_shutdown_lock.acquire()
try:
if not _pmons:
return
for p in _pmons:
shutdown_process_monitor(p)
del _pmons[:]
except:
print("exception in pmon_shutdown")
traceback.print_exc()
finally:
_shutdown_lock.release()
_signal_chain = {}
_shutting_down = False
def rl_signal(sig, stackframe):
global _shutting_down
if _shutting_down:
return #prevent infinite callbacks
_shutting_down = True
pmon_shutdown()
prev_handler = _signal_chain.get(sig, None)
if prev_handler and prev_handler not in [signal.SIG_IGN, signal.SIG_DFL, rl_signal]:
try:
prev_handler(sig, stackframe)
except KeyboardInterrupt:
pass #filter out generic keyboard interrupt handler
if sys.platform in ['win32']: # cygwin seems to be ok
_signal_list = [signal.SIGTERM, signal.SIGINT]
else:
_signal_list = [signal.SIGTERM, signal.SIGINT, signal.SIGHUP]
_sig_initialized = False
def _init_signal_handlers():
global _sig_initialized
if _sig_initialized:
return
if not roslib.is_interactive():
for s in _signal_list:
_signal_chain[s] = signal.signal(s, rl_signal)
atexit.register(pmon_shutdown)
_sig_initialized = True
# ##############################################################
class Process(object):
"""
Basic process representation for L{ProcessMonitor}. Must be subclassed
to provide actual start()/stop() implementations.
Constructor *must* be called from the Python Main thread in order
for signal handlers to register properly.
"""
def __init__(self, package, name, args, env,
respawn=False, respawn_delay=0.0, required=False):
self.package = package
self.name = name
self.args = args
self.env = env
self.respawn = respawn
self.respawn_delay = respawn_delay
self.required = required
self.lock = Lock()
self.exit_code = None
# for keeping track of respawning
self.spawn_count = 0
self.time_of_death = None
_init_signal_handlers()
def __str__(self):
return "Process<%s>"%(self.name)
# NOTE: get_info() is going to have to be sufficient for
# generating respawn requests, so we must be complete about it
def get_info(self):
"""
Get all data about this process in dictionary form
@return: dictionary of all relevant process properties
@rtype: dict { str: val }
"""
info = {
'spawn_count': self.spawn_count,
'args': self.args,
'env': self.env,
'package': self.package,
'name': self.name,
'alive': self.is_alive(),
'respawn': self.respawn,
'respawn_delay': self.respawn_delay,
'required': self.required,
}
if self.exit_code is not None:
info['exit_code'] = self.exit_code
return info
def start(self):
self.time_of_death = None
self.spawn_count += 1
def is_alive(self):
if self.time_of_death is None:
self.time_of_death = time.time()
return False
def should_respawn(self):
"""
@return: False if process should not respawn
floating point seconds until respawn otherwise
"""
if not self.respawn:
return False
if self.time_of_death is None:
if self.is_alive():
return False
return (self.time_of_death + self.respawn_delay) - time.time()
def stop(self, errors=None):
"""
Stop the process. Record any significant error messages in the errors parameter
@param errors: error messages. stop() will record messages into this list.
@type errors: [str]
"""
pass
def get_exit_description(self):
if self.exit_code is not None:
if self.exit_code:
return 'process has died [exit code %s]'%self.exit_code
else:
# try not to scare users about process exit
return 'process has finished cleanly'
else:
return 'process has died'
class DeadProcess(Process):
"""
Container class to maintain information about a process that has died. This
container allows us to delete the actual Process but still maintain the metadata
"""
def __init__(self, p):
super(DeadProcess, self).__init__(p.package, p.name, p.args, p.env,
p.respawn, p.respawn_delay)
self.exit_code = p.exit_code
self.lock = None
self.spawn_count = p.spawn_count
self.info = p.get_info()
def get_info(self):
return self.info
def start(self):
raise Exception("cannot call start on a dead process!")
def is_alive(self):
return False
class ProcessListener(object):
"""
Listener class for L{ProcessMonitor}
"""
def process_died(self, process_name, exit_code):
"""
Notifies listener that process has died. This callback only
occurs for processes that die during normal process monitor
execution -- processes that are forcibly killed during
ProcessMonitor shutdown are not reported.
@param process_name: name of process
@type process_name: str
@param exit_code: exit code of process. If None, it means
that ProcessMonitor was unable to determine an exit code.
@type exit_code: int
"""
pass
class ProcessMonitor(Thread):
def __init__(self, name="ProcessMonitor"):
Thread.__init__(self, name=name)
self.procs = []
self.plock = RLock()
self.is_shutdown = False
self.done = False
self.setDaemon(True)
self.reacquire_signals = set()
self.listeners = []
self.dead_list = []
# #885: ensure core procs
self.core_procs = []
# #642: flag to prevent process monitor exiting prematurely
self._registrations_complete = False
logger.info("created process monitor %s"%self)
def add_process_listener(self, l):
"""
Listener for process events. MUST be called before
ProcessMonitor is running.See ProcessListener class.
@param l: listener instance
@type l: L{ProcessListener}
"""
self.listeners.append(l)
def register(self, p):
"""
Register process with L{ProcessMonitor}
@param p: Process
@type p: L{Process}
@raise RLException: if process with same name is already registered
"""
logger.info("ProcessMonitor.register[%s]"%p.name)
e = None
with self.plock:
if self.has_process(p.name):
e = RLException("cannot add process with duplicate name '%s'"%p.name)
elif self.is_shutdown:
e = RLException("cannot add process [%s] after process monitor has been shut down"%p.name)
else:
self.procs.append(p)
if e:
logger.error("ProcessMonitor.register[%s] failed %s"%(p.name, e))
raise e
else:
logger.info("ProcessMonitor.register[%s] complete"%p.name)
def register_core_proc(self, p):
"""
Register core process with ProcessMonitor. Coreprocesses
have special shutdown semantics. They are killed after all
other processes, in reverse order in which they are added.
@param p Process
@type p: L{Process}
@raise RLException: if process with same name is already registered
"""
self.register(p)
self.core_procs.append(p)
def registrations_complete(self):
"""
Inform the process monitor that registrations are complete.
After the registrations_complete flag is set, process monitor
will exit if there are no processes left to monitor.
"""
self._registrations_complete = True
logger.info("registrations completed %s"%self)
def unregister(self, p):
logger.info("ProcessMonitor.unregister[%s] starting"%p.name)
with self.plock:
self.procs.remove(p)
logger.info("ProcessMonitor.unregister[%s] complete"%p.name)
def has_process(self, name):
"""
@return: True if process is still be monitored. If False, process
has died or was never registered with process
@rtype: bool
"""
return len([p for p in self.procs if p.name == name]) > 0
def get_process(self, name):
"""
@return: process registered under \a name, or None
@rtype: L{Process}
"""
with self.plock:
v = [p for p in self.procs if p.name == name]
if v:
return v[0]
def has_main_thread_jobs(self):
"""
@return: True if ProcessMonitor has tasks that need to be run in the main thread
@rtype: bool
"""
return len(self.reacquire_signals)
def do_main_thread_jobs(self):
"""
Execute tasks that need to be run in the main thread. Must be
called from main thread.
"""
#not entirely threadsafe
sigs = [s for s in self.reacquire_signals]
for s in sigs:
_signal_chain[s] = signal.signal(s, rl_signal)
self.reacquire_signals.remove(s)
def kill_process(self, name):
"""
Kill process that matches name. NOTE: a killed process will
continue to show up as active until the process monitor thread
has caught that it has died.
@param name: Process name
@type name: str
@return: True if a process named name was removed from
process monitor. A process is considered killed if its stop()
method was called.
@rtype: bool
"""
def isstring(s):
"""Small helper version to check an object is a string in
a way that works for both Python 2 and 3
"""
try:
return isinstance(s, basestring)
except NameError:
return isinstance(s, str)
if not isstring(name):
raise RLException("kill_process takes in a process name but was given: %s"%name)
logger.debug("ProcessMonitor.kill_process[%s]"%name)
printlog("[%s] kill requested"%name)
with self.plock:
p = self.get_process(name)
if p:
try:
# no need to accumulate errors, so pass in []
p.stop([])
except:
logger.error(traceback.format_exc())
return True
else:
return False
def shutdown(self):
"""
Shutdown the process monitor thread
"""
logger.info("ProcessMonitor.shutdown %s"%self)
self.is_shutdown = True
def get_active_names(self):
"""
@return [str]: list of active process names
"""
with self.plock:
retval = [p.name for p in self.procs]
return retval
def get_process_names_with_spawn_count(self):
"""
@return: Two lists, where first
list of active process names along with the number of times
that process has been spawned. Second list contains dead process names
and their spawn count.
@rtype: [[(str, int),], [(str,int),]]
"""
with self.plock:
actives = [(p.name, p.spawn_count) for p in self.procs]
deads = [(p.name, p.spawn_count) for p in self.dead_list]
retval = [actives, deads]
return retval
def mainthread_spin_once(self):
"""
run() occurs in a separate thread and cannot do certain signal-related
work. The main thread of the application must call mainthread_spin()
or mainthread_spin_once() in order to perform these jobs.
"""
if not self.done:
if self.has_main_thread_jobs():
self.do_main_thread_jobs()
return True
else:
return False
def mainthread_spin(self):
"""
run() occurs in a separate thread and cannot do certain signal-related
work. The main thread of the application must call mainthread_spin()
or mainthread_spin_once() in order to perform these jobs. mainthread_spin()
blocks until the process monitor is complete.
"""
while not self.done:
if sys.platform in ['win32']: # cygwin seems to be ok
# windows sleep throws an exception when a signal has arrived, even when
# it has a handler. We can either use win32api.Sleep OR....catch
# the exception
try:
time.sleep(0.1)
except IOError:
pass
else:
time.sleep(0.1)
if self.has_main_thread_jobs():
self.do_main_thread_jobs()
def run(self):
"""
thread routine of the process monitor. NOTE: you must still
call mainthread_spin or mainthread_spin_once() from the main
thread in order to pick up main thread work from the process
monitor.
"""
try:
#don't let exceptions bomb thread, interferes with exit
try:
self._run()
except:
logger.error(traceback.format_exc())
traceback.print_exc()
finally:
self._post_run()
def _run(self):
"""
Internal run loop of ProcessMonitor
"""
plock = self.plock
dead = []
respawn = []
while not self.is_shutdown:
with plock: #copy self.procs
procs = self.procs[:]
if self.is_shutdown:
break
# check current signal handlers to see if children have stolen them away
# TODO: this code may not be necessary anymore (have to test)
for s in _signal_list:
if signal.getsignal(s) != rl_signal:
self.reacquire_signals.add(s)
for p in procs:
try:
if not p.is_alive():
logger.debug("Process[%s] has died, respawn=%s, required=%s, exit_code=%s",
p.name,
"True(%f)" % p.respawn_delay if p.respawn else p.respawn,
p.required, p.exit_code)
exit_code_str = p.get_exit_description()
if p.required:
printerrlog('='*80+"REQUIRED process [%s] has died!\n%s\nInitiating shutdown!\n"%(p.name, exit_code_str)+'='*80)
self.is_shutdown = True
elif not p in respawn:
if p.exit_code:
printerrlog("[%s] %s"%(p.name, exit_code_str))
else:
printlog_bold("[%s] %s"%(p.name, exit_code_str))
dead.append(p)
## no need for lock as we require listeners be
## added before process monitor is launched
for l in self.listeners:
l.process_died(p.name, p.exit_code)
except Exception as e:
traceback.print_exc()
#don't respawn as this is an internal error
dead.append(p)
if self.is_shutdown:
break #stop polling
for d in dead:
try:
if d.should_respawn():
respawn.append(d)
else:
self.unregister(d)
# stop process, don't accumulate errors
d.stop([])
# save process data to dead list
with plock:
self.dead_list.append(DeadProcess(d))
except:
logger.error(traceback.format_exc())
# dead check is to make sure that ProcessMonitor at least
# waits until its had at least one process before exiting
if self._registrations_complete and dead and not self.procs and not respawn:
printlog("all processes on machine have died, roslaunch will exit")
self.is_shutdown = True
del dead[:]
_respawn=[]
for r in respawn:
try:
if self.is_shutdown:
break
if r.should_respawn() <= 0.0:
printlog("[%s] restarting process" % r.name)
# stop process, don't accumulate errors
r.stop([])
r.start()
else:
# not ready yet, keep it around
_respawn.append(r)
except:
traceback.print_exc()
logger.error("Restart failed %s",traceback.format_exc())
respawn = _respawn
time.sleep(0.1) #yield thread
#moved this to finally block of _post_run
#self._post_run() #kill all processes
def _post_run(self):
logger.info("ProcessMonitor._post_run %s"%self)
# this is already true entering, but go ahead and make sure
self.is_shutdown = True
# killall processes on run exit
q = Queue()
q.join()
with self.plock:
# make copy of core_procs for threadsafe usage
core_procs = self.core_procs[:]
logger.info("ProcessMonitor._post_run %s: remaining procs are %s"%(self, self.procs))
# enqueue all non-core procs in reverse order for parallel kill
# #526/885: ignore core procs
[q.put(p) for p in reversed(self.procs) if not p in core_procs]
# use 10 workers
killers = []
for i in range(10):
t = _ProcessKiller(q, i)
killers.append(t)
t.start()
# wait for workers to finish
q.join()
shutdown_errors = []
# accumulate all the shutdown errors
for t in killers:
shutdown_errors.extend(t.errors)
del killers[:]
# #526/885: kill core procs last
# we don't want to parallelize this as the master has to be last
for p in reversed(core_procs):
_kill_process(p, shutdown_errors)
# delete everything except dead_list
logger.info("ProcessMonitor exit: cleaning up data structures and signals")
with self.plock:
del core_procs[:]
del self.procs[:]
del self.core_procs[:]
reacquire_signals = self.reacquire_signals
if reacquire_signals:
reacquire_signals.clear()
logger.info("ProcessMonitor exit: pmon has shutdown")
self.done = True
if shutdown_errors:
printerrlog("Shutdown errors:\n"+'\n'.join([" * %s"%e for e in shutdown_errors]))
def _kill_process(p, errors):
"""
Routine for kill Process p with appropriate logging to screen and logfile
@param p: process to kill
@type p: Process
@param errors: list of error messages from killed process
@type errors: [str]
"""
try:
logger.info("ProcessMonitor exit: killing %s", p.name)
printlog("[%s] killing on exit"%p.name)
# we accumulate errors from each process so that we can print these at the end
p.stop(errors)
except:
traceback.print_exc()
logger.error(traceback.format_exc())
class _ProcessKiller(Thread):
def __init__(self, q, i):
Thread.__init__(self, name="ProcessKiller-%s"%i)
self.q = q
self.errors = []
def run(self):
q = self.q
while not q.empty():
try:
p = q.get(False)
_kill_process(p, self.errors)
q.task_done()
except Empty:
pass

View File

@@ -0,0 +1,225 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Revision $Id$
"""
Integrates roslaunch remote process launching capabilities.
"""
import logging
import socket
import time
import rosgraph.network as network
import roslaunch.config
import roslaunch.remoteprocess
from roslaunch.remoteprocess import SSHChildROSLaunchProcess
import roslaunch.launch
import roslaunch.server #ROSLaunchParentNode hidden dep
from roslaunch.core import RLException, is_machine_local, printerrlog, printlog
_CHILD_REGISTER_TIMEOUT = 10.0 #seconds
class ROSRemoteRunner(roslaunch.launch.ROSRemoteRunnerIF):
"""
Manages the running of remote roslaunch children
"""
def __init__(self, run_id, rosconfig, pm, server):
"""
:param run_id: roslaunch run_id of this runner, ``str``
:param config: launch configuration, ``ROSConfig``
:param pm process monitor, ``ProcessMonitor``
:param server: roslaunch parent server, ``ROSLaunchParentNode``
"""
self.run_id = run_id
self.rosconfig = rosconfig
self.server = server
self.pm = pm
self.logger = logging.getLogger('roslaunch.remote')
self.listeners = []
self.machine_list = []
self.remote_processes = []
def add_process_listener(self, l):
"""
Listen to events about remote processes dying. Not
threadsafe. Must be called before processes started.
:param l: ProcessListener
"""
self.listeners.append(l)
def _start_child(self, server_node_uri, machine, counter):
# generate a name for the machine. don't use config key as
# it's too long to easily display
name = "%s-%s"%(machine.address, counter)
self.logger.info("remote[%s] starting roslaunch", name)
printlog("remote[%s] starting roslaunch"%name)
p = SSHChildROSLaunchProcess(self.run_id, name, server_node_uri, machine, self.rosconfig.master.uri)
success = p.start()
self.pm.register(p)
if not success: #treat as fatal
raise RLException("unable to start remote roslaunch child: %s"%name)
self.server.add_child(name, p)
return p
def start_children(self):
"""
Start the child roslaunch processes
"""
server_node_uri = self.server.uri
if not server_node_uri:
raise RLException("server URI is not initialized")
# TODOXXX: break out table building code into a separate
# routine so we can unit test it _start_child() should not be
# determining the process name
# Build table of unique machines that we are going to launch on
machines = {}
for n in self.rosconfig.nodes:
if not is_machine_local(n.machine):
machines[n.machine.config_key()] = n.machine
# Launch child roslaunch processes on remote machines
counter = 0
# - keep a list of procs so we can check for those that failed to launch
procs = []
for m in machines:
p = self._start_child(server_node_uri, machines[m], counter)
procs.append(p)
counter += 1
# Wait for all children to call register() callback. The machines can have
# non-uniform registration timeouts. We consider the failure to occur once
# one of the machines has failed to meet it's timeout.
start_t = time.time()
while True:
pending = []
for p in procs:
if not p.is_alive():
raise RLException("remote roslaunch failed to launch: %s"%p.machine.name)
elif not p.uri:
pending.append(p.machine)
if not pending:
break
# timeout is the minimum of the remaining timeouts of the machines
timeout_t = start_t + min([m.timeout for m in pending])
if time.time() > timeout_t:
break
time.sleep(0.1)
if pending:
raise RLException(
"""The following roslaunch remote processes failed to register:
%s
If this is a network latency issue, you may wish to consider setting
<machine timeout="NUMBER OF SECONDS" ... />
in your launch"""%'\n'.join([" * %s (timeout %ss)"%(m.name, m.timeout) for m in pending]))
# convert machine dictionary to a list
self.machine_list = machines.values()
# save a list of the remote processes
self.remote_processes = procs
def _assume_failed(self, nodes, failed):
"""
Utility routine for logging/recording nodes that failed
:param nodes: list of nodes that are assumed to have failed, ``Node``
:param failed: list of names of nodes that have failed to extend, ``[str]``
"""
str_nodes = ["%s/%s"%(n.package, n.type) for n in nodes]
failed.extend(str_nodes)
printerrlog("Launch of the following nodes most likely failed: %s"%', '.join(str_nodes))
def launch_remote_nodes(self):
"""
Contact each child to launch remote nodes
"""
succeeded = []
failed = []
# initialize remote_nodes. we use the machine config key as
# the key for the dictionary so that we can bin the nodes.
self.remote_nodes = {}
for m in self.machine_list:
self.remote_nodes[m.config_key()] = []
# build list of nodes that will be launched by machine
nodes = [x for x in self.rosconfig.nodes if not is_machine_local(x.machine)]
for n in nodes:
self.remote_nodes[n.machine.config_key()].append(n)
for child in self.remote_processes:
nodes = self.remote_nodes[child.machine.config_key()]
body = '\n'.join([n.to_remote_xml() for n in nodes])
# #3799: force utf-8 encoding
xml = '<?xml version="1.0" encoding="utf-8"?>\n<launch>\n%s</launch>'%body
api = child.getapi()
# TODO: timeouts
try:
self.logger.debug("sending [%s] XML [\n%s\n]"%(child.uri, xml))
code, msg, val = api.launch(xml)
if code == 1:
c_succ, c_fail = val
succeeded.extend(c_succ)
failed.extend(c_fail)
else:
printerrlog('error launching on [%s, uri %s]: %s'%(child.name, child.uri, msg))
self._assume_failed(nodes, failed)
except socket.error as e:
errno, msg = e
printerrlog('error launching on [%s, uri %s]: %s'%(child.name, child.uri, str(msg)))
self._assume_failed(nodes, failed)
except socket.gaierror as e:
errno, msg = e
# usually errno == -2. See #815.
child_host, _ = network.parse_http_host_and_port(child.uri)
printerrlog("Unable to contact remote roslaunch at [%s]. This is most likely due to a network misconfiguration with host lookups. Please make sure that you can contact '%s' from this machine"%(child.uri, child_host))
self._assume_failed(nodes, failed)
except Exception as e:
printerrlog('error launching on [%s, uri %s]: %s'%(child.name, child.uri, str(e)))
self._assume_failed(nodes, failed)
return succeeded, failed

View File

@@ -0,0 +1,334 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Revision $Id$
"""
Process handler for launching ssh-based roslaunch child processes.
"""
import os
import socket
import traceback
try:
from xmlrpc.client import ServerProxy
except ImportError:
from xmlrpclib import ServerProxy
import rosgraph
from roslaunch.core import printlog, printerrlog
import roslaunch.pmon
import roslaunch.server
import logging
_logger = logging.getLogger("roslaunch.remoteprocess")
# #1975 timeout for creating ssh connections
TIMEOUT_SSH_CONNECT = 30.
def ssh_check_known_hosts(ssh, address, port, username=None, logger=None):
"""
Validation routine for loading the host keys and making sure that
they are configured properly for the desired SSH. The behavior of
this routine can be modified by the ROSLAUNCH_SSH_UNKNOWN
environment variable, which enables the paramiko.AutoAddPolicy.
:param ssh: paramiko SSH client, :class:`paramiko.SSHClient`
:param address: SSH IP address, ``str``
:param port: SSH port, ``int``
:param username: optional username to include in error message if check fails, ``str``
:param logger: (optional) logger to record tracebacks to, :class:`logging.Logger`
:returns: error message if improperly configured, or ``None``. ``str``
"""
import paramiko
try:
try:
if os.path.isfile('/etc/ssh/ssh_known_hosts'): #default ubuntu location
ssh.load_system_host_keys('/etc/ssh/ssh_known_hosts')
except IOError:
pass
ssh.load_system_host_keys() #default user location
except:
if logger:
logger.error(traceback.format_exc())
# as seen in #767, base64 raises generic Error.
#
# A corrupt pycrypto build can also cause this, though
# the cause of the corrupt builds has been fixed.
return "cannot load SSH host keys -- your known_hosts file may be corrupt"
# #3158: resolve the actual host using the user's ~/.ssh/config
ssh_config = paramiko.SSHConfig()
try:
with open(os.path.join(os.path.expanduser('~'), '.ssh', 'config')) as f:
ssh_config.parse(f)
config_lookup = ssh_config.lookup(address)
resolved_address = config_lookup['hostname'] if 'hostname' in config_lookup else address
except:
resolved_address = address
# #1849: paramiko will raise an SSHException with an 'Unknown
# server' message if the address is not in the known_hosts
# file. This causes a lot of confusion to users, so we try
# and diagnose this in advance and offer better guidance
# - ssh.get_host_keys() does not return the system host keys
hk = ssh._system_host_keys
override = os.environ.get('ROSLAUNCH_SSH_UNKNOWN', 0)
if override == '1':
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
elif hk.lookup(resolved_address) is None:
port_str = user_str = ''
if port != 22:
port_str = "-p %s "%port
if username:
user_str = username+'@'
return """%s is not in your SSH known_hosts file.
Please manually:
ssh %s%s%s
then try roslaunching again.
If you wish to configure roslaunch to automatically recognize unknown
hosts, please set the environment variable ROSLAUNCH_SSH_UNKNOWN=1"""%(resolved_address, port_str, user_str, resolved_address)
class SSHChildROSLaunchProcess(roslaunch.server.ChildROSLaunchProcess):
"""
Process wrapper for launching and monitoring a child roslaunch process over SSH
"""
def __init__(self, run_id, name, server_uri, machine, master_uri=None):
"""
:param machine: Machine instance. Must be fully configured.
machine.env_loader is required to be set.
"""
if not machine.env_loader:
raise ValueError("machine.env_loader must have been assigned before creating ssh child instance")
args = [machine.env_loader, 'roslaunch', '-c', name, '-u', server_uri, '--run_id', run_id]
# env is always empty dict because we only use env_loader
super(SSHChildROSLaunchProcess, self).__init__(name, args, {})
self.machine = machine
self.master_uri = master_uri
self.ssh = self.sshin = self.sshout = self.ssherr = None
self.started = False
self.uri = None
# self.is_dead is a flag set by is_alive that affects whether or not we
# log errors during a stop().
self.is_dead = False
def _ssh_exec(self, command, address, port, username=None, password=None):
"""
:returns: (ssh pipes, message). If error occurs, returns (None, error message).
"""
if self.master_uri:
env_command = 'env %s=%s' % (rosgraph.ROS_MASTER_URI, self.master_uri)
command = '%s %s' % (env_command, command)
try:
import Crypto
except ImportError as e:
_logger.error("cannot use SSH: pycrypto is not installed")
return None, "pycrypto is not installed"
try:
import paramiko
except ImportError as e:
_logger.error("cannot use SSH: paramiko is not installed")
return None, "paramiko is not installed"
#load user's ssh configuration
config_block = {'hostname': None, 'user': None, 'identityfile': None}
ssh_config = paramiko.SSHConfig()
try:
with open(os.path.join(os.path.expanduser('~'), '.ssh','config')) as f:
ssh_config.parse(f)
config_block.update(ssh_config.lookup(address))
except:
pass
address = config_block['hostname'] or address
username = username or config_block['user']
identity_file = None
if config_block.get('identityfile', None):
if isinstance(config_block['identityfile'], list):
identity_file = [os.path.expanduser(f) for f in config_block['identityfile']]
else:
identity_file = os.path.expanduser(config_block['identityfile'])
#load ssh client and connect
ssh = paramiko.SSHClient()
err_msg = ssh_check_known_hosts(ssh, address, port, username=username, logger=_logger)
if not err_msg:
username_str = '%s@'%username if username else ''
try:
if not password: #use SSH agent
ssh.connect(address, port, username, timeout=TIMEOUT_SSH_CONNECT, key_filename=identity_file)
else: #use SSH with login/pass
ssh.connect(address, port, username, password, timeout=TIMEOUT_SSH_CONNECT)
except paramiko.BadHostKeyException:
_logger.error(traceback.format_exc())
err_msg = "Unable to verify host key for remote computer[%s:%s]"%(address, port)
except paramiko.AuthenticationException:
_logger.error(traceback.format_exc())
err_msg = "Authentication to remote computer[%s%s:%s] failed.\nA common cause of this error is a missing key in your authorized_keys file."%(username_str, address, port)
except paramiko.SSHException as e:
_logger.error(traceback.format_exc())
if str(e).startswith("Unknown server"):
pass
err_msg = "Unable to establish ssh connection to [%s%s:%s]: %s"%(username_str, address, port, e)
except socket.error as e:
# #1824
if e[0] == 111:
err_msg = "network connection refused by [%s:%s]"%(address, port)
else:
err_msg = "network error connecting to [%s:%s]: %s"%(address, port, str(e))
if err_msg:
return None, err_msg
else:
printlog("launching remote roslaunch child with command: [%s]"%(str(command)))
sshin, sshout, ssherr = ssh.exec_command(command)
return (ssh, sshin, sshout, ssherr), "executed remotely"
def start(self):
"""
Start the remote process. This will create an SSH connection
to the remote host.
"""
self.started = False #won't set to True until we are finished
self.ssh = self.sshin = self.sshout = self.ssherr = None
with self.lock:
name = self.name
m = self.machine
if m.user is not None:
printlog("remote[%s]: creating ssh connection to %s:%s, user[%s]"%(name, m.address, m.ssh_port, m.user))
else:
printlog("remote[%s]: creating ssh connection to %s:%s"%(name, m.address, m.ssh_port))
_logger.info("remote[%s]: invoking with ssh exec args [%s]"%(name, ' '.join(self.args)))
sshvals, msg = self._ssh_exec(' '.join(self.args), m.address, m.ssh_port, m.user, m.password)
if sshvals is None:
printerrlog("remote[%s]: failed to launch on %s:\n\n%s\n\n"%(name, m.name, msg))
return False
self.ssh, self.sshin, self.sshout, self.ssherr = sshvals
printlog("remote[%s]: ssh connection created"%name)
self.started = True
return True
def getapi(self):
"""
:returns: ServerProxy to remote client XMLRPC server, `ServerProxy`
"""
if self.uri:
return ServerProxy(self.uri)
else:
return None
def is_alive(self):
"""
:returns: ``True`` if the process is alive. is_alive needs to be
called periodically as it drains the SSH buffer, ``bool``
"""
if self.started and not self.ssh:
return False
elif not self.started:
return True #not started is equivalent to alive in our logic
s = self.ssherr
s.channel.settimeout(0)
try:
#drain the pipes
data = s.read(2048)
if not len(data):
self.is_dead = True
return False
# #2012 il8n: ssh *should* be UTF-8, but often isn't
# (e.g. Japan)
data = data.decode('utf-8')
printerrlog("remote[%s]: %s"%(self.name, data))
except socket.timeout:
pass
except IOError:
return False
except UnicodeDecodeError:
# #2012: soft fail, printing is not essential. This occurs
# with older machines that don't send utf-8 over ssh
pass
s = self.sshout
s.channel.settimeout(0)
try:
#drain the pipes
#TODO: write to log file
data = s.read(2048)
if not len(data):
self.is_dead = True
return False
except socket.timeout:
pass
except IOError:
return False
return True
def stop(self, errors=None):
"""
Terminate this process, including the SSH connection.
"""
if errors is None:
errors = []
with self.lock:
if not self.ssh:
return
# call the shutdown API first as closing the SSH connection
# won't actually kill the process unless it triggers SIGPIPE
try:
api = self.getapi()
if api is not None:
#TODO: probably need a timeout on this
api.shutdown()
except socket.error:
# normal if process is already dead
address, port = self.machine.address, self.machine.ssh_port
if not self.is_dead:
printerrlog("remote[%s]: unable to contact [%s] to shutdown remote processes!"%(self.name, address))
else:
printlog("remote[%s]: unable to contact [%s] to shutdown cleanly. The remote roslaunch may have exited already."%(self.name, address))
except:
# temporary: don't really want to log here as this
# may occur during shutdown
traceback.print_exc()
_logger.info("remote[%s]: closing ssh connection", self.name)
self.sshin.close()
self.sshout.close()
self.ssherr.close()
self.ssh.close()
self.sshin = None
self.sshout = None
self.ssherr = None
self.ssh = None
_logger.info("remote[%s]: ssh connection closed", self.name)

View File

@@ -0,0 +1,270 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2009, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""
Uncategorized utility routines for roslaunch.
This API should not be considered stable.
"""
from __future__ import print_function
import os
import sys
import time
import roslib.packages
import rosclean
import rospkg
import rosgraph
import roslaunch.core
import roslaunch.config
import roslaunch.depends
from rosmaster import DEFAULT_MASTER_PORT
def check_log_disk_usage():
"""
Check size of log directory. If high, print warning to user
"""
try:
d = rospkg.get_log_dir()
roslaunch.core.printlog("Checking log directory for disk usage. This may take awhile.\nPress Ctrl-C to interrupt")
disk_usage = rosclean.get_disk_usage(d)
# warn if over a gig
if disk_usage > 1073741824:
roslaunch.core.printerrlog("WARNING: disk usage in log directory [%s] is over 1GB.\nIt's recommended that you use the 'rosclean' command."%d)
else:
roslaunch.core.printlog("Done checking log file disk usage. Usage is <1GB.")
except:
pass
def resolve_launch_arguments(args):
"""
Resolve command-line args to roslaunch filenames.
:returns: resolved filenames, ``[str]``
"""
# strip remapping args for processing
args = rosgraph.myargv(args)
# user can either specify:
# - filename + launch args
# - package + relative-filename + launch args
if not args:
return args
resolved_args = None
# try to resolve launch file in package first
if len(args) >= 2:
try:
resolved = roslib.packages.find_resource(args[0], args[1])
if len(resolved) > 1:
raise roslaunch.core.RLException("multiple files named [%s] in package [%s]:%s\nPlease specify full path instead" % (args[1], args[0], ''.join(['\n- %s' % r for r in resolved])))
if len(resolved) == 1:
resolved_args = [resolved[0]] + args[2:]
except rospkg.ResourceNotFound:
pass
# try to resolve launch file
if resolved_args is None and (args[0] == '-' or os.path.isfile(args[0])):
resolved_args = [args[0]] + args[1:]
# raise if unable to resolve
if resolved_args is None:
if len(args) >= 2:
raise roslaunch.core.RLException("[%s] is neither a launch file in package [%s] nor is [%s] a launch file name" % (args[1], args[0], args[0]))
else:
raise roslaunch.core.RLException("[%s] is not a launch file name" % args[0])
return resolved_args
def _wait_for_master():
"""
Block until ROS Master is online
:raise: :exc:`RuntimeError` If unexpected error occurs
"""
m = roslaunch.core.Master() # get a handle to the default master
is_running = m.is_running()
if not is_running:
roslaunch.core.printlog("roscore/master is not yet running, will wait for it to start")
while not is_running:
time.sleep(0.1)
is_running = m.is_running()
if is_running:
roslaunch.core.printlog("master has started, initiating launch")
else:
raise RuntimeError("unknown error waiting for master to start")
_terminal_name = None
def _set_terminal(s):
import platform
if platform.system() in ['FreeBSD', 'Linux', 'Darwin', 'Unix']:
try:
print('\033]2;%s\007'%(s))
except:
pass
def update_terminal_name(ros_master_uri):
"""
append master URI to the terminal name
"""
if _terminal_name:
_set_terminal(_terminal_name + ' ' + ros_master_uri)
def change_terminal_name(args, is_core):
"""
use echo (where available) to change the name of the terminal window
"""
global _terminal_name
_terminal_name = 'roscore' if is_core else ','.join(args)
_set_terminal(_terminal_name)
def get_or_generate_uuid(options_runid, options_wait_for_master):
"""
:param options_runid: run_id value from command-line or ``None``, ``str``
:param options_wait_for_master: the wait_for_master command
option. If this is True, it means that we must retrieve the
value from the parameter server and need to avoid any race
conditions with the roscore being initialized. ``bool``
"""
# Three possible sources of the run_id:
#
# - if we're a child process, we get it from options_runid
# - if there's already a roscore running, read from the param server
# - generate one if we're running the roscore
if options_runid:
return options_runid
# #773: Generate a run_id to use if we launch a master
# process. If a master is already running, we'll get the
# run_id from it instead
param_server = rosgraph.Master('/roslaunch')
val = None
while val is None:
try:
val = param_server.getParam('/run_id')
except:
if not options_wait_for_master:
val = roslaunch.core.generate_run_id()
return val
def check_roslaunch(f, use_test_depends=False):
"""
Check roslaunch file for errors, returning error message if check fails. This routine
is mainly to support rostest's roslaunch_check.
:param f: roslaunch file name, ``str``
:param use_test_depends: Consider test_depends, ``Bool``
:returns: error message or ``None``
"""
try:
rl_config = roslaunch.config.load_config_default([f], DEFAULT_MASTER_PORT, verbose=False)
except roslaunch.core.RLException as e:
return str(e)
rospack = rospkg.RosPack()
errors = []
# check for missing deps
try:
base_pkg, file_deps, missing = roslaunch.depends.roslaunch_deps([f], use_test_depends=use_test_depends)
except rospkg.common.ResourceNotFound as r:
errors.append("Could not find package [%s] included from [%s]"%(str(r), f))
missing = {}
file_deps = {}
except roslaunch.substitution_args.ArgException as e:
errors.append("Could not resolve arg [%s] in [%s]"%(str(e), f))
missing = {}
file_deps = {}
for pkg, miss in missing.items():
# even if the pkgs is not found in packges.xml, if other package.xml provdes that pkgs, then it will be ok
all_pkgs = []
try:
for file_dep in file_deps.keys():
file_pkg = rospkg.get_package_name(file_dep)
all_pkgs.extend(rospack.get_depends(file_pkg,implicit=False))
miss_all = list(set(miss) - set(all_pkgs))
except Exception as e:
print(e, file=sys.stderr)
miss_all = True
if miss_all:
print("Missing package dependencies: %s/package.xml: %s"%(pkg, ', '.join(miss)), file=sys.stderr)
errors.append("Missing package dependencies: %s/package.xml: %s"%(pkg, ', '.join(miss)))
elif miss:
print("Missing package dependencies: %s/package.xml: %s (notify upstream maintainer)"%(pkg, ', '.join(miss)), file=sys.stderr)
# load all node defs
nodes = []
for filename, rldeps in file_deps.items():
nodes.extend(rldeps.nodes)
# check for missing packages
for pkg, node_type in nodes:
try:
rospack.get_path(pkg)
except:
errors.append("cannot find package [%s] for node [%s]"%(pkg, node_type))
# check for missing nodes
for pkg, node_type in nodes:
try:
if not roslib.packages.find_node(pkg, node_type, rospack=rospack):
errors.append("cannot find node [%s] in package [%s]"%(node_type, pkg))
except Exception as e:
errors.append("unable to find node [%s/%s]: %s"%(pkg, node_type, str(e)))
# Check for configuration errors, #2889
for err in rl_config.config_errors:
errors.append('ROSLaunch config error: %s' % err)
if errors:
return '\n'.join(errors)
def print_file_list(roslaunch_files):
"""
:param roslaunch_files: list of launch files to load, ``str``
:returns: list of files involved in processing roslaunch_files, including the files themselves.
"""
from roslaunch.config import load_config_default, get_roscore_filename
import roslaunch.xmlloader
try:
loader = roslaunch.xmlloader.XmlLoader(resolve_anon=True)
config = load_config_default(roslaunch_files, None, loader=loader, verbose=False, assign_machines=False)
files = [os.path.abspath(x) for x in set(config.roslaunch_files) - set([get_roscore_filename()])]
print('\n'.join(files))
except roslaunch.core.RLException as e:
print(str(e), file=sys.stderr)
sys.exit(1)

View File

@@ -0,0 +1,77 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Revision $Id$
from __future__ import print_function
"""
Implementation for roslaunch-logs command-line utility.
"""
import os
import sys
import time
import traceback
import rospkg
import rosgraph
NAME = 'roslaunch-logs'
def get_run_id():
try:
param_server = rosgraph.Master('/roslaunch')
return param_server.getParam('/run_id')
except: # cannot contact parameter server
pass
def logs_main():
from optparse import OptionParser
parser = OptionParser(usage="usage: %prog", prog=NAME)
options, args = parser.parse_args()
if args:
parser.error("%s takes no arguments"%NAME)
log_dir = rospkg.get_log_dir()
if not log_dir:
print("Cannot determine ROS log directory", file=sys.stderr)
sys.exit(1)
run_id = get_run_id()
if not run_id:
# go ahead and print the log directory
print("No active roscore", file=sys.stderr)
print(log_dir)
sys.exit(2)
print(os.path.join(log_dir, run_id))

View File

@@ -0,0 +1,118 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Revision $Id$
"""
Scripting interface for roslaunch
"""
from roslaunch.core import Node, Master, RLException
import roslaunch.config
import roslaunch.parent
import roslaunch.xmlloader
class ROSLaunch(object):
"""
ROSLaunchParent represents the main 'parent' roslaunch process. It
is responsible for loading the launch files, assigning machines,
and then starting up any remote processes. The __main__ method
delegates most of runtime to ROSLaunchParent.
This must be called from the Python Main thread due to signal registration.
"""
def __init__(self):
"""
@raise RLException: if fails to initialize
"""
import rosgraph.masterapi
master = rosgraph.masterapi.Master('/roslaunch_script')
uuid = roslaunch.rlutil.get_or_generate_uuid(None, True)
self.parent = roslaunch.parent.ROSLaunchParent(uuid, [], is_core=False)
self.started = False
def load(self, f):
"""
Load roslaunch file
@param f: filename
@type f: str
"""
raise NotImplemented
def load_str(self, s):
"""
Load roslaunch string
@param s: string representation of roslaunch config
@type s: str
"""
raise NotImplemented
def launch(self, node):
"""
Launch a roslaunch node instance
@param node: roslaunch Node instance
@type node: roslaunch.Node
@return: node process
@rtype: roslaunch.Process
@raise RLException: if launch fails
"""
if not self.started:
raise RLException("please start ROSLaunch first")
elif not isinstance(node, Node):
raise ValueError("arg must be of type Node")
proc, success = self.parent.runner.launch_node(node)
if not success:
raise RLException("failed to launch %s/%s"%(node.package, node.type))
return proc
def start(self):
"""
Start roslaunch. This will launch any pre-configured launches and spin up the process monitor thread.
"""
self.parent.start(auto_terminate=False)
self.started = True
def spin(self):
self.parent.spin()
def spin_once(self):
self.parent.spin_once()
def stop(self):
self.parent.shutdown()

View File

@@ -0,0 +1,533 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Revision $Id$
from __future__ import print_function
"""
XML-RPC servers for parent and children
Following typical XmlRpcNode code, code is divided into:
a) Handlers: these actually define and execute the XML-RPC API
b) Nodes: these run the XML-RPC server
In this code you'll find 'Parent' and 'Child' code. The parent node
is the original roslaunch process. The child nodes are the child
processes it launches in order to handle remote launching (or
execution as a different user).
"""
import logging
import os
import socket
import sys
import time
import traceback
try:
from urllib.parse import urlparse
except ImportError:
from urlparse import urlparse
try:
from xmlrpc.client import ServerProxy
except ImportError:
from xmlrpclib import ServerProxy
import rosgraph.network as network
import rosgraph.xmlrpc as xmlrpc
import roslaunch.config
from roslaunch.pmon import ProcessListener, Process
import roslaunch.xmlloader
from roslaunch.launch import ROSLaunchRunner
from roslaunch.core import RLException, \
add_printlog_handler, add_printerrlog_handler, printlog, printerrlog, printlog_bold
#For using Log message level constants
from rosgraph_msgs.msg import Log
# interface class so that we don't have circular deps
class ChildROSLaunchProcess(Process):
"""
API for remote roslaunch processes
"""
def __init__(self, name, args, env):
super(ChildROSLaunchProcess, self).__init__('roslaunch', name, args, env, False)
self.uri = None
def set_uri(self, uri):
self.uri = uri
class ROSLaunchBaseHandler(xmlrpc.XmlRpcHandler):
"""
Common XML-RPC API for the roslaunch server and child node
"""
def __init__(self, pm):
self.pm = pm
self.logger = logging.getLogger('roslaunch.server')
if self.pm is None:
raise RLException("cannot create xmlrpc handler: pm is not initialized")
#TODO: kill process, restart (with optional prefix). list active, list dead. CPU usage
def list_processes(self):
"""
@return: code, msg, process list.
Process list is two lists, where first list of
active process names along with the number of times that
process has been spawned. Second list contains dead process
names and their spawn count.
@rtype: int, str, [[(str, int),], [(str,int),]]
"""
return 1, "processes on parent machine", self.pm.get_process_names_with_spawn_count()
def process_info(self, process_name):
"""
@return: dictionary of metadata about process. Keys vary by implementation
@rtype: int, str, dict
"""
p = self.pm.get_process(process_name)
if p is None:
return -1, "no process by that name", {}
else:
return 1, "process info", p.get_info()
def get_pid(self):
"""
@return: code, msg, pid
@rtype: int, str, int
"""
pid = os.getpid()
return 1, str(pid), pid
def get_node_names(self):
"""
@return: code, msg, list of node names
@rtype: int, str, [str]
"""
if self.pm is None:
return 0, "uninitialized", []
return 1, "node names", self.pm.get_active_names()
def _shutdown(self, reason):
"""
xmlrpc.XmlRpcHandler API: inform handler of shutdown
@param reason: human-readable shutdown reason
@type reason: str
"""
return 1, '', 1
# Uses camel-case network-API naming conventions
class ROSLaunchParentHandler(ROSLaunchBaseHandler):
"""
XML-RPC API for the roslaunch server node
"""
def __init__(self, pm, child_processes, listeners):
"""
@param child_processes: Map of remote processes so that server can update processes
with information as children register. Handler will not modify
keys.
@type child_processes: {name : ChildROSLaunchProcess}.
@param listeners [ProcessListener]: list of
listeners to notify when process_died events occur.
"""
super(ROSLaunchParentHandler, self).__init__(pm)
self.child_processes = child_processes
self.listeners = listeners
def register(self, client, uri):
"""
Registration callback from newly launched roslaunch clients
@param client: name of client
@type client: str
@param uri: XML-RPC URI of client
@type uri: str
@return: code, msg, ignore
@rtype: int, str, int
"""
if client not in self.child_processes:
self.logger.error("Unknown child [%s] registered with server", client)
return -1, "unknown child [%s]"%client, 0
else:
self.logger.info("child [%s] registered with server, uri[%s]", client, uri)
self.child_processes[client].set_uri(uri)
return 1, "registered", 1
def list_children(self):
"""
List the roslaunch child processes.
@return int, str, [str]: code, msg, list of the roslaunch children URIS
"""
return 1, 'roslaunch children', [v.uri for v in self.child_processes.values() if v.uri is not None]
def process_died(self, process_name, exit_code):
"""
Inform roslaunch server that a remote process has died
@param process_name: name of process that died
@type process_name: str
@param exit_code: exit code of remote process
@type exit_code: int
@return: code, msg, ignore
@rtype: int, str, int
"""
for l in self.listeners:
try:
l.process_died(process_name, exit_code)
except:
self.logger.error(traceback.format_exc())
return 1, '', 0
def log(self, client, level, message):
"""
Report a log message to the server
@param client: name of client
@type client: str
@param level: log level (uses rosgraph_msgs.msg.Log levels)
@type level: int
@param message: message to log
@type message: str
"""
try:
if level >= Log.ERROR:
printerrlog("[%s]: %s"%(client, message))
else:
#hack due to the fact that we only have one INFO level
if 'started with pid' in message:
printlog_bold("[%s]: %s"%(client, message))
else:
printlog("[%s]: %s"%(client, message))
except:
# can't trust the logging system at this point, so just dump to screen
traceback.print_exc()
return 1, '', 1
class ROSLaunchChildHandler(ROSLaunchBaseHandler):
"""
XML-RPC API implementation for child roslaunches
NOTE: the client handler runs a process monitor so that
it can track processes across requests
"""
def __init__(self, run_id, name, server_uri, pm):
"""
@param server_uri: XML-RPC URI of server
@type server_uri: str
@param pm: process monitor to use
@type pm: L{ProcessMonitor}
@raise RLException: If parameters are invalid
"""
super(ROSLaunchChildHandler, self).__init__(pm)
if server_uri is None:
raise RLException("server_uri is not initialized")
self.run_id = run_id
# parse the URI to make sure it's valid
_, urlport = network.parse_http_host_and_port(server_uri)
if urlport <= 0:
raise RLException("ERROR: roslaunch server URI is not a valid XML-RPC URI. Value is [%s]"%m.uri)
self.name = name
self.pm = pm
self.server_uri = server_uri
self.server = ServerProxy(server_uri)
def _shutdown(self, reason):
"""
xmlrpc.XmlRpcHandler API: inform handler of shutdown
@param reason: human-readable shutdown reason
@type reason: str
"""
if self.pm is not None:
self.pm.shutdown()
self.pm.join()
self.pm = None
def shutdown(self):
"""
@return: code, msg, ignore
@rtype: int, str, int
"""
self._shutdown("external call")
return 1, "success", 1
def _log(self, level, message):
"""
log message to log file and roslaunch server
@param level: log level
@type level: int
@param message: message to log
@type message: str
"""
try:
if self.logger is not None:
self.logger.debug(message)
if self.server is not None:
self.server.log(str(self.name), level, str(message))
except:
self.logger.error(traceback.format_exc())
def launch(self, launch_xml):
"""
Launch the roslaunch XML file. Because this is a child
roslaunch, it will not set parameters nor manipulate the
master. Call blocks until launch is complete
@param xml: roslaunch XML file to launch
@type xml: str
@return: code, msg, [ [ successful launches], [failed launches] ]
@rtype: int, str, [ [str], [str] ]
"""
if self.pm is None:
return 0, "uninitialized", -1
rosconfig = roslaunch.config.ROSLaunchConfig()
try:
roslaunch.xmlloader.XmlLoader().load_string(launch_xml, rosconfig)
except roslaunch.xmlloader.XmlParseException as e:
return -1, "ERROR: %s"%e, [[], []]
# won't actually do anything other than local, but still required
rosconfig.assign_machines()
try:
# roslaunch clients try to behave like normal roslaunches as much as possible. It's
# mainly the responsibility of the roslaunch server to not give us any XML that might
# cause conflict (e.g. master tags, param tags, etc...).
self._log(Log.INFO, "launching nodes...")
runner = ROSLaunchRunner(self.run_id, rosconfig, server_uri=self.server_uri, pmon=self.pm)
succeeded, failed = runner.launch()
self._log(Log.INFO, "... done launching nodes")
# enable the process monitor to exit of all processes die
self.pm.registrations_complete()
return 1, "launched", [ succeeded, failed ]
except Exception as e:
return 0, "ERROR: %s"%traceback.format_exc(), [[], []]
_STARTUP_TIMEOUT = 5.0 #seconds
class ROSLaunchNode(xmlrpc.XmlRpcNode):
"""
Base XML-RPC server for roslaunch parent/child processes
"""
def __init__(self, handler):
"""
@param handler: xmlrpc api handler
@type handler: L{ROSLaunchBaseHandler}
"""
super(ROSLaunchNode, self).__init__(0, handler)
def start(self):
"""
Startup roslaunch server XML-RPC services
@raise RLException: if server fails to start
"""
logger = logging.getLogger('roslaunch.server')
logger.info("starting roslaunch XML-RPC server")
super(ROSLaunchNode, self).start()
# wait for node thread to initialize
timeout_t = time.time() + _STARTUP_TIMEOUT
logger.info("waiting for roslaunch XML-RPC server to initialize")
while not self.uri and time.time() < timeout_t:
time.sleep(0.01)
if not self.uri:
raise RLException("XML-RPC initialization failed")
# Make sure our xmlrpc server is actually up. We've seen very
# odd cases where remote nodes are unable to contact the
# server but have been unable to prove this is the cause.
server_up = False
while not server_up and time.time() < timeout_t:
try:
code, msg, val = ServerProxy(self.uri).get_pid()
if val != os.getpid():
raise RLException("Server at [%s] did not respond with correct PID. There appears to be something wrong with the networking configuration"%self.uri)
server_up = True
except IOError:
# presumably this can occur if we call in a small time
# interval between the server socket port being
# assigned and the XMLRPC server initializing, but it
# is highly unlikely and unconfirmed
time.sleep(0.1)
except socket.error as e:
if e.errno == 113:
p = urlparse(self.uri)
raise RLException("Unable to contact the address [%s], which should be local.\nThis is generally caused by:\n * bad local network configuration\n * bad ROS_IP environment variable\n * bad ROS_HOSTNAME environment variable\nCan you ping %s?"%(self.uri, p.hostname))
else:
time.sleep(0.1)
if not server_up:
p = urlparse(self.uri)
raise RLException("""Unable to contact my own server at [%s].
This usually means that the network is not configured properly.
A common cause is that the machine cannot ping itself. Please check
for errors by running:
\tping %s
For more tips, please see
\thttp://www.ros.org/wiki/ROS/NetworkSetup
"""%(self.uri, p.hostname))
printlog_bold("started roslaunch server %s"%(self.uri))
def run(self):
"""
run() should not be called by higher-level code. ROSLaunchNode
overrides underlying xmlrpc.XmlRpcNode implementation in order
to log errors.
"""
try:
super(ROSLaunchNode, self).run()
except:
logging.getLogger("roslaunch.remote").error(traceback.format_exc())
print("ERROR: failed to launch XML-RPC server for roslaunch", file=sys.stderr)
class ROSLaunchParentNode(ROSLaunchNode):
"""
XML-RPC server for parent roslaunch.
"""
def __init__(self, rosconfig, pm):
"""
@param config: ROSConfig launch configuration
@type config: L{ROSConfig}
@param pm: process monitor
@type pm: L{ProcessMonitor}
"""
self.rosconfig = rosconfig
self.listeners = []
self.child_processes = {} #{ child-name : ChildROSLaunchProcess}.
if pm is None:
raise RLException("cannot create parent node: pm is not initialized")
handler = ROSLaunchParentHandler(pm, self.child_processes, self.listeners)
super(ROSLaunchParentNode, self).__init__(handler)
def add_child(self, name, p):
"""
@param name: child roslaunch's name. NOTE: \a name is not
the same as the machine config key.
@type name: str
@param p: process handle of child
@type p: L{Process}
"""
self.child_processes[name] = p
def add_process_listener(self, l):
"""
Listen to events about remote processes dying. Not
threadsafe. Must be called before processes started.
@param l: Process listener
@type l: L{ProcessListener}
"""
self.listeners.append(l)
class _ProcessListenerForwarder(ProcessListener):
"""
Simple listener that forwards ProcessListener events to a roslaunch server
"""
def __init__(self, server):
self.server = server
def process_died(self, process_name, exit_code):
try:
self.server.process_died(process_name, exit_code)
except Exception as e:
logging.getLogger("roslaunch.remote").error(traceback.format_exc())
class ROSLaunchChildNode(ROSLaunchNode):
"""
XML-RPC server for roslaunch child processes
"""
def __init__(self, run_id, name, server_uri, pm):
"""
## Startup roslaunch remote client XML-RPC services. Blocks until shutdown
## @param name: name of remote client
## @type name: str
## @param server_uri: XML-RPC URI of roslaunch server
## @type server_uri: str
## @return: XML-RPC URI
## @rtype: str
"""
self.logger = logging.getLogger("roslaunch.server")
self.run_id = run_id
self.name = name
self.server_uri = server_uri
self.pm = pm
if self.pm is None:
raise RLException("cannot create child node: pm is not initialized")
handler = ROSLaunchChildHandler(self.run_id, self.name, self.server_uri, self.pm)
super(ROSLaunchChildNode, self).__init__(handler)
def _register_with_server(self):
"""
Register child node with server
"""
name = self.name
self.logger.info("attempting to register with roslaunch parent [%s]"%self.server_uri)
try:
server = ServerProxy(self.server_uri)
code, msg, _ = server.register(name, self.uri)
if code != 1:
raise RLException("unable to register with roslaunch server: %s"%msg)
except Exception as e:
self.logger.error("Exception while registering with roslaunch parent [%s]: %s"%(self.server_uri, traceback.format_exc()))
# fail
raise RLException("Exception while registering with roslaunch parent [%s]: %s"%(self.server_uri, traceback.format_exc()))
self.logger.debug("child registered with server")
# register printlog handler so messages are funneled to remote
def serverlog(msg):
server.log(name, Log.INFO, msg)
def servererrlog(msg):
server.log(name, Log.ERROR, msg)
add_printlog_handler(serverlog)
add_printerrlog_handler(servererrlog)
# register process listener to forward process death events to main server
self.pm.add_process_listener(_ProcessListenerForwarder(server))
def start(self):
"""
Initialize child. Must be called before run
"""
self.logger.info("starting roslaunch child process [%s], server URI is [%s]", self.name, self.server_uri)
super(ROSLaunchChildNode, self).start()
self._register_with_server()

View File

@@ -0,0 +1,437 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Revision $Id: substitution_args.py 15178 2011-10-10 21:22:53Z kwc $
"""
Library for processing XML substitution args. This is currently used
by roslaunch and xacro, but it is not yet a top-level ROS feature.
"""
import os
try:
from cStringIO import StringIO # Python 2.x
except ImportError:
from io import StringIO # Python 3.x
import rosgraph.names
import rospkg
from roslaunch.loader import convert_value
import math
_rospack = None
class SubstitutionException(Exception):
"""
Base class for exceptions in substitution_args routines
"""
pass
class ArgException(SubstitutionException):
"""
Exception for missing $(arg) values
"""
pass
def _eval_env(name):
try:
return os.environ[name]
except KeyError as e:
raise SubstitutionException("environment variable %s is not set" % str(e))
def _env(resolved, a, args, context):
"""
process $(env) arg
@return: updated resolved argument
@rtype: str
@raise SubstitutionException: if arg invalidly specified
"""
if len(args) != 1:
raise SubstitutionException("$(env var) command only accepts one argument [%s]"%a)
return resolved.replace("$(%s)" % a, _eval_env(args[0]))
def _eval_optenv(name, default=''):
if name in os.environ:
return os.environ[name]
return default
def _optenv(resolved, a, args, context):
"""
process $(optenv) arg
@return: updated resolved argument
@rtype: str
@raise SubstitutionException: if arg invalidly specified
"""
if len(args) == 0:
raise SubstitutionException("$(optenv var) must specify an environment variable [%s]"%a)
return resolved.replace("$(%s)" % a, _eval_optenv(args[0], default=' '.join(args[1:])))
def _eval_anon(id, anons):
if id in anons:
return anons[id]
resolve_to = rosgraph.names.anonymous_name(id)
anons[id] = resolve_to
return resolve_to
def _anon(resolved, a, args, context):
"""
process $(anon) arg
@return: updated resolved argument
@rtype: str
@raise SubstitutionException: if arg invalidly specified
"""
# #1559 #1660
if len(args) == 0:
raise SubstitutionException("$(anon var) must specify a name [%s]"%a)
elif len(args) > 1:
raise SubstitutionException("$(anon var) may only specify one name [%s]"%a)
if 'anon' not in context:
context['anon'] = {}
anon_context = context['anon']
return resolved.replace("$(%s)" % a, _eval_anon(id=args[0], anons=anon_context))
def _eval_find(pkg):
rp = _get_rospack()
return rp.get_path(pkg)
def _find(resolved, a, args, context):
"""
process $(find PKG)
Resolves the path while considering the path following the command to provide backward compatible results.
If it is followed by a path it first tries to resolve it as an executable and than as a normal file under share.
Else it resolves to the source share folder of the PKG.
:returns: updated resolved argument, ``str``
:raises: :exc:SubstitutionException: if PKG invalidly specified
:raises: :exc:`rospkg.ResourceNotFound` If PKG requires resource (e.g. package) that does not exist
"""
if len(args) != 1:
raise SubstitutionException("$(find pkg) command only accepts one argument [%s]" % a)
before, after = _split_command(resolved, a)
path, after = _separate_first_path(after)
resolve_without_path = before + ('$(%s)' % a) + after
path = _sanitize_path(path)
if path.startswith('/') or path.startswith('\\'):
path = path[1:]
rp = _get_rospack()
if path:
source_path_to_packages = rp.get_custom_cache('source_path_to_packages', {})
res = None
try:
res = _find_executable(
resolve_without_path, a, [args[0], path], context,
source_path_to_packages=source_path_to_packages)
except SubstitutionException:
pass
if res is None:
try:
res = _find_resource(
resolve_without_path, a, [args[0], path], context,
source_path_to_packages=source_path_to_packages)
except SubstitutionException:
pass
# persist mapping of packages in rospack instance
if source_path_to_packages:
rp.set_custom_cache('source_path_to_packages', source_path_to_packages)
if res is not None:
return res
pkg_path = rp.get_path(args[0])
if path:
pkg_path = os.path.join(pkg_path, path)
return before + pkg_path + after
def _find_executable(resolved, a, args, _context, source_path_to_packages=None):
"""
process $(find-executable PKG PATH)
It finds the executable with the basename(PATH) in the libexec folder
or under the PATH relative to the package.xml file.
:returns: updated resolved argument, ``str``
:raises: :exc:SubstitutionException: if PKG/PATH invalidly specified or executable is not found for PKG
"""
if len(args) != 2:
raise SubstitutionException("$(find-executable pkg path) command only accepts two argument [%s]" % a)
before, after = _split_command(resolved, a)
path = _sanitize_path(args[1])
# we try to find the specific executable in libexec via catkin
# which will search in install/devel space
full_path = None
from catkin.find_in_workspaces import find_in_workspaces
paths = find_in_workspaces(
['libexec'], project=args[0], first_matching_workspace_only=True,
# implicitly first_match_only=True
source_path_to_packages=source_path_to_packages)
if paths:
full_path = _get_executable_path(paths[0], os.path.basename(path))
if not full_path:
# else we will look for the executable in the source folder of the package
rp = _get_rospack()
full_path = _get_executable_path(rp.get_path(args[0]), path)
if not full_path:
raise SubstitutionException("$(find-executable pkg path) could not find executable [%s]" % a)
return before + full_path + after
def _find_resource(resolved, a, args, _context, source_path_to_packages=None):
"""
process $(find-resource PKG PATH)
Resolves the relative PATH from the share folder of the PKG either from install space, devel space or from the source folder.
:returns: updated resolved argument, ``str``
:raises: :exc:SubstitutionException: if PKG and PATH invalidly specified or relative PATH is not found for PKG
"""
if len(args) != 2:
raise SubstitutionException("$(find-resource pkg path) command only accepts two argument [%s]" % a)
before, after = _split_command(resolved, a)
path = _sanitize_path(args[1])
# we try to find the specific path in share via catkin
# which will search in install/devel space and the source folder of the package
from catkin.find_in_workspaces import find_in_workspaces
paths = find_in_workspaces(
['share'], project=args[0], path=path, first_matching_workspace_only=True,
first_match_only=True, source_path_to_packages=source_path_to_packages)
if not paths:
raise SubstitutionException("$(find-resource pkg path) could not find path [%s]" % a)
return before + paths[0] + after
def _split_command(resolved, command_with_args):
cmd = '$(%s)' % command_with_args
idx1 = resolved.find(cmd)
idx2 = idx1 + len(cmd)
return resolved[0:idx1], resolved[idx2:]
def _separate_first_path(value):
idx = value.find(' ')
if idx < 0:
path, rest = value, ''
else:
path, rest = value[0:idx], value[idx:]
return path, rest
def _sanitize_path(path):
path = path.replace('/', os.sep)
path = path.replace('\\', os.sep)
return path
def _get_executable_path(base_path, path):
full_path = os.path.join(base_path, path)
if os.path.isfile(full_path) and os.access(full_path, os.X_OK):
return full_path
return None
def _get_rospack():
global _rospack
if _rospack is None:
_rospack = rospkg.RosPack()
return _rospack
def _eval_arg(name, args):
try:
return args[name]
except KeyError:
raise ArgException(name)
def _arg(resolved, a, args, context):
"""
process $(arg) arg
:returns: updated resolved argument, ``str``
:raises: :exc:`ArgException` If arg invalidly specified
"""
if len(args) == 0:
raise SubstitutionException("$(arg var) must specify a variable name [%s]"%(a))
elif len(args) > 1:
raise SubstitutionException("$(arg var) may only specify one arg [%s]"%(a))
if 'arg' not in context:
context['arg'] = {}
return resolved.replace("$(%s)" % a, _eval_arg(name=args[0], args=context['arg']))
# Create a dictionary of global symbols that will be available in the eval
# context. We disable all the builtins, then add back True and False, and also
# add true and false for convenience (because we accept those lower-case strings
# as boolean values in XML).
_eval_dict={
'true': True, 'false': False,
'True': True, 'False': False,
'__builtins__': {k: __builtins__[k] for k in ['list', 'dict', 'map', 'str', 'float', 'int']},
'env': _eval_env,
'optenv': _eval_optenv,
'find': _eval_find
}
# also define all math symbols and functions
_eval_dict.update(math.__dict__)
class _DictWrapper(object):
def __init__(self, args, functions):
self._args = args
self._functions = functions
def __getitem__(self, key):
try:
return self._functions[key]
except KeyError:
return convert_value(self._args[key], 'auto')
def _eval(s, context):
if 'anon' not in context:
context['anon'] = {}
if 'arg' not in context:
context['arg'] = {}
# inject correct anon context
def _eval_anon_context(id): return _eval_anon(id, anons=context['anon'])
# inject arg context
def _eval_arg_context(name): return convert_value(_eval_arg(name, args=context['arg']), 'auto')
functions = dict(anon=_eval_anon_context, arg=_eval_arg_context)
functions.update(_eval_dict)
# ignore values containing double underscores (for safety)
# http://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html
if s.find('__') >= 0:
raise SubstitutionException("$(eval ...) may not contain double underscore expressions")
return str(eval(s, {}, _DictWrapper(context['arg'], functions)))
def resolve_args(arg_str, context=None, resolve_anon=True):
"""
Resolves substitution args (see wiki spec U{http://ros.org/wiki/roslaunch}).
@param arg_str: string to resolve zero or more substitution args
in. arg_str may be None, in which case resolve_args will
return None
@type arg_str: str
@param context dict: (optional) dictionary for storing results of
the 'anon' and 'arg' substitution args. multiple calls to
resolve_args should use the same context so that 'anon'
substitions resolve consistently. If no context is provided, a
new one will be created for each call. Values for the 'arg'
context should be stored as a dictionary in the 'arg' key.
@type context: dict
@param resolve_anon bool: If True (default), will resolve $(anon
foo). If false, will leave these args as-is.
@type resolve_anon: bool
@return str: arg_str with substitution args resolved
@rtype: str
@raise SubstitutionException: if there is an error resolving substitution args
"""
if context is None:
context = {}
if not arg_str:
return arg_str
# special handling of $(eval ...)
if arg_str.startswith('$(eval ') and arg_str.endswith(')'):
return _eval(arg_str[7:-1], context)
# first resolve variables like 'env' and 'arg'
commands = {
'env': _env,
'optenv': _optenv,
'anon': _anon,
'arg': _arg,
}
resolved = _resolve_args(arg_str, context, resolve_anon, commands)
# then resolve 'find' as it requires the subsequent path to be expanded already
commands = {
'find': _find,
}
resolved = _resolve_args(resolved, context, resolve_anon, commands)
return resolved
def _resolve_args(arg_str, context, resolve_anon, commands):
valid = ['find', 'env', 'optenv', 'anon', 'arg']
resolved = arg_str
for a in _collect_args(arg_str):
splits = [s for s in a.split(' ') if s]
if not splits[0] in valid:
raise SubstitutionException("Unknown substitution command [%s]. Valid commands are %s"%(a, valid))
command = splits[0]
args = splits[1:]
if command in commands:
resolved = commands[command](resolved, a, args, context)
return resolved
_OUT = 0
_DOLLAR = 1
_LP = 2
_IN = 3
def _collect_args(arg_str):
"""
State-machine parser for resolve_args. Substitution args are of the form:
$(find package_name)/scripts/foo.py $(export some/attribute blar) non-relevant stuff
@param arg_str: argument string to parse args from
@type arg_str: str
@raise SubstitutionException: if args are invalidly specified
@return: list of arguments
@rtype: [str]
"""
buff = StringIO()
args = []
state = _OUT
for c in arg_str:
# No escapes supported
if c == '$':
if state == _OUT:
state = _DOLLAR
elif state == _DOLLAR:
pass
else:
raise SubstitutionException("Dollar signs '$' cannot be inside of substitution args [%s]"%arg_str)
elif c == '(':
if state == _DOLLAR:
state = _LP
elif state != _OUT:
raise SubstitutionException("Invalid left parenthesis '(' in substitution args [%s]"%arg_str)
elif c == ')':
if state == _IN:
#save contents of collected buffer
args.append(buff.getvalue())
buff.truncate(0)
buff.seek(0)
state = _OUT
else:
state = _OUT
elif state == _DOLLAR:
# left paren must immediately follow dollar sign to enter _IN state
state = _OUT
elif state == _LP:
state = _IN
if state == _IN:
buff.write(c)
return args

View File

@@ -0,0 +1,783 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Revision $Id$
"""
Roslaunch XML file parser.
"""
from __future__ import print_function
import itertools
import sys
import traceback
import logging
from xml.dom.minidom import parse, parseString
from xml.dom import Node as DomNode #avoid aliasing
from rosgraph.names import make_global_ns, ns_join, is_private, is_legal_name, get_ros_namespace
from .core import Param, Node, Test, Machine, RLException
from . import loader
from . import substitution_args
# use in our namespace
SubstitutionException = substitution_args.SubstitutionException
ArgException = substitution_args.ArgException
NS='ns'
CLEAR_PARAMS='clear_params'
def _get_text(tag):
buff = ''
for t in tag.childNodes:
if t.nodeType in [t.TEXT_NODE, t.CDATA_SECTION_NODE]:
buff += t.data
return buff
def ifunless_test(obj, tag, context):
"""
@return True: if tag should be processed according to its if/unless attributes
"""
if_val, unless_val = obj.opt_attrs(tag, context, ['if', 'unless'])
if if_val is not None and unless_val is not None:
raise XmlParseException("cannot set both 'if' and 'unless' on the same tag")
if if_val is not None:
if_val = loader.convert_value(if_val, 'bool')
if if_val:
return True
elif unless_val is not None:
unless_val = loader.convert_value(unless_val, 'bool')
if not unless_val:
return True
else:
return True
return False
def ifunless(f):
"""
Decorator for evaluating whether or not tag function should run based on if/unless attributes
"""
def call(*args, **kwds):
#TODO: logging, as well as check for verbose in kwds
if ifunless_test(args[0], args[1], args[2]):
return f(*args, **kwds)
return call
# This code has gotten a bit crufty as roslaunch has grown far beyond
# its original spec. It needs to be far more generic than it is in
# order to not replicate bugs in multiple places.
class XmlParseException(RLException):
"""Error with the XML syntax (e.g. invalid attribute/value combinations)"""
pass
def _bool_attr(v, default, label):
"""
Validate boolean xml attribute.
@param v: parameter value or None if no value provided
@type v: any
@param default: default value
@type default: bool
@param label: parameter name/label
@type label: str
@return: boolean value for attribute
@rtype: bool
@raise XmlParseException: if v is not in correct range or is empty.
"""
if v is None:
return default
if v.lower() == 'true':
return True
elif v.lower() == 'false':
return False
elif not v:
raise XmlParseException("bool value for %s must be non-empty"%(label))
else:
raise XmlParseException("invalid bool value for %s: %s"%(label, v))
def _float_attr(v, default, label):
"""
Validate float xml attribute.
@param v: parameter value or None if no value provided
@type v: any
@param default: default value
@type default: float
@param label: parameter name/label
@type label: str
@return: float value for attribute
@rtype: float
@raise XmlParseException: if v is not in correct range or is empty.
"""
if v is None:
return default
if not v:
raise XmlParseException("bool value for %s must be non-empty"%(label))
try:
x = float(v)
except ValueError:
raise XmlParseException("invalid float value for %s: %s"%(label, v))
return x
# maps machine 'default' attribute to Machine default property
_is_default = {'true': True, 'false': False, 'never': False }
# maps machine 'default' attribute to Machine assignable property
_assignable = {'true': True, 'false': True, 'never': False }
# NOTE: code is currently in a semi-refactored state. I'm slowly
# migrating common routines into the Loader class in the hopes it will
# make it easier to write alternate loaders and also test.
class XmlLoader(loader.Loader):
"""
Parser for roslaunch XML format. Loads parsed representation into ROSConfig model.
"""
def __init__(self, resolve_anon=True):
"""
@param resolve_anon: If True (default), will resolve $(anon foo). If
false, will leave these args as-is.
@type resolve_anon: bool
"""
# store the root XmlContext so that outside code can access it
self.root_context = None
self.resolve_anon = resolve_anon
def resolve_args(self, args, context):
"""
Wrapper around substitution_args.resolve_args to set common parameters
"""
# resolve_args gets called a lot, so we optimize by testing for dollar sign before resolving
if args and '$' in args:
return substitution_args.resolve_args(args, context=context.resolve_dict, resolve_anon=self.resolve_anon)
else:
return args
def opt_attrs(self, tag, context, attrs):
"""
Helper routine for fetching and resolving optional tag attributes
@param tag DOM tag
@param context LoaderContext
@param attrs (str): list of attributes to resolve
"""
def tag_value(tag, a):
if tag.hasAttribute(a):
# getAttribute returns empty string for non-existent
# attributes, which makes it impossible to distinguish
# with actual empty values
return tag.getAttribute(a)
else:
return None
return [self.resolve_args(tag_value(tag,a), context) for a in attrs]
def reqd_attrs(self, tag, context, attrs):
"""
Helper routine for fetching and resolving required tag attributes
@param tag: DOM tag
@param attrs: list of attributes to resolve
@type attrs: (str)
@raise KeyError: if required attribute is missing
"""
return [self.resolve_args(tag.attributes[a].value, context) for a in attrs]
def _check_attrs(self, tag, context, ros_config, attrs):
tag_attrs = tag.attributes.keys()
for t_a in tag_attrs:
if not t_a in attrs and not t_a in ['if', 'unless']:
ros_config.add_config_error("[%s] unknown <%s> attribute '%s'"%(context.filename, tag.tagName, t_a))
# 'ns' attribute is now deprecated and is an alias for
# 'param'. 'param' is required if the value is a non-dictionary
# type
ROSPARAM_OPT_ATTRS = ('command', 'ns', 'file', 'param', 'subst_value')
@ifunless
def _rosparam_tag(self, tag, context, ros_config, verbose=True):
try:
self._check_attrs(tag, context, ros_config, XmlLoader.ROSPARAM_OPT_ATTRS)
cmd, ns, file, param, subst_value = self.opt_attrs(tag, context, (XmlLoader.ROSPARAM_OPT_ATTRS))
subst_value = _bool_attr(subst_value, False, 'subst_value')
# ns atribute is a bit out-moded and is only left in for backwards compatibility
param = ns_join(ns or '', param or '')
# load is the default command
cmd = cmd or 'load'
value = _get_text(tag)
subst_function = None
if subst_value:
subst_function = lambda x: self.resolve_args(x, context)
self.load_rosparam(context, ros_config, cmd, param, file, value, verbose=verbose, subst_function=subst_function)
except ValueError as e:
raise loader.LoadException("error loading <rosparam> tag: \n\t"+str(e)+"\nXML is %s"%tag.toxml())
PARAM_ATTRS = ('name', 'value', 'type', 'value', 'textfile', 'binfile', 'command')
@ifunless
def _param_tag(self, tag, context, ros_config, force_local=False, verbose=True):
"""
@param force_local: if True, param must be added to context instead of ros_config
@type force_local: bool
"""
try:
self._check_attrs(tag, context, ros_config, XmlLoader.PARAM_ATTRS)
# compute name and value
ptype = (tag.getAttribute('type') or 'auto').lower().strip()
vals = self.opt_attrs(tag, context, ('value', 'textfile', 'binfile', 'command'))
if len([v for v in vals if v is not None]) != 1:
raise XmlParseException(
"<param> tag must have one and only one of value/textfile/binfile.")
# compute name. if name is a tilde name, it is placed in
# the context. otherwise it is placed in the ros config.
name = self.resolve_args(tag.attributes['name'].value.strip(), context)
value = self.param_value(verbose, name, ptype, *vals)
if is_private(name) or force_local:
p = Param(name, value)
context.add_param(p)
else:
p = Param(ns_join(context.ns, name), value)
ros_config.add_param(Param(ns_join(context.ns, name), value), filename=context.filename, verbose=verbose)
return p
except KeyError as e:
raise XmlParseException(
"<param> tag is missing required attribute: %s. \n\nParam xml is %s"%(e, tag.toxml()))
except ValueError as e:
raise XmlParseException(
"Invalid <param> tag: %s. \n\nParam xml is %s"%(e, tag.toxml()))
ARG_ATTRS = ('name', 'value', 'default', 'doc')
@ifunless
def _arg_tag(self, tag, context, ros_config, verbose=True):
"""
Process an <arg> tag.
"""
try:
self._check_attrs(tag, context, ros_config, XmlLoader.ARG_ATTRS)
(name,) = self.reqd_attrs(tag, context, ('name',))
value, default, doc = self.opt_attrs(tag, context, ('value', 'default', 'doc'))
if value is not None and default is not None:
raise XmlParseException(
"<arg> tag must have one and only one of value/default.")
context.add_arg(name, value=value, default=default, doc=doc)
except substitution_args.ArgException as e:
raise XmlParseException(
"arg '%s' is not defined. \n\nArg xml is %s"%(e, tag.toxml()))
except Exception as e:
raise XmlParseException(
"Invalid <arg> tag: %s. \n\nArg xml is %s"%(e, tag.toxml()))
def _test_attrs(self, tag, context):
"""
Process attributes of <test> tag not present in <node>
@return: test_name, time_limit
@rtype: str, int
"""
for attr in ['respawn', 'respawn_delay', 'output']:
if tag.hasAttribute(attr):
raise XmlParseException("<test> tags cannot have '%s' attribute"%attr)
test_name = self.resolve_args(tag.attributes['test-name'].value, context)
time_limit = self.resolve_args(tag.getAttribute('time-limit'), context)
retry = self.resolve_args(tag.getAttribute('retry'), context)
if time_limit:
try:
time_limit = float(time_limit)
except ValueError:
raise XmlParseException("'time-limit' must be a number: [%s]"%time_limit)
if time_limit <= 0.0:
raise XmlParseException("'time-limit' must be a positive number")
if retry:
try:
retry = int(retry)
except ValueError:
raise XmlParseException("'retry' must be a number: [%s]"%retry)
return test_name, time_limit, retry
NODE_ATTRS = ['pkg', 'type', 'machine', 'name', 'args', 'output', \
'respawn', 'respawn_delay', 'cwd', NS, CLEAR_PARAMS, \
'launch-prefix', 'required']
TEST_ATTRS = NODE_ATTRS + ['test-name','time-limit', 'retry']
@ifunless
def _node_tag(self, tag, context, ros_config, default_machine, is_test=False, verbose=True):
"""
Process XML <node> or <test> tag
@param tag: DOM node
@type tag: Node
@param context: namespace context
@type context: L{LoaderContext}
@param params: ROS parameter list
@type params: [L{Param}]
@param clear_params: list of ROS parameter names to clear before setting parameters
@type clear_params: [str]
@param default_machine: default machine to assign to node
@type default_machine: str
@param is_test: if set, will load as L{Test} object instead of L{Node} object
@type is_test: bool
"""
try:
if is_test:
self._check_attrs(tag, context, ros_config, XmlLoader.TEST_ATTRS)
(name,) = self.opt_attrs(tag, context, ('name',))
test_name, time_limit, retry = self._test_attrs(tag, context)
if not name:
name = test_name
else:
self._check_attrs(tag, context, ros_config, XmlLoader.NODE_ATTRS)
(name,) = self.reqd_attrs(tag, context, ('name',))
if not is_legal_name(name):
ros_config.add_config_error("WARN: illegal <node> name '%s'.\nhttp://ros.org/wiki/Names\nThis will likely cause problems with other ROS tools.\nNode xml is %s"%(name, tag.toxml()))
child_ns = self._ns_clear_params_attr('node', tag, context, ros_config, node_name=name)
param_ns = child_ns.child(name)
param_ns.params = [] # This is necessary because child() does not make a copy of the param list.
# required attributes
pkg, node_type = self.reqd_attrs(tag, context, ('pkg', 'type'))
# optional attributes
machine, args, output, respawn, respawn_delay, cwd, launch_prefix, \
required = self.opt_attrs(tag, context, ('machine', 'args',
'output', 'respawn', 'respawn_delay', 'cwd',
'launch-prefix', 'required'))
if tag.hasAttribute('machine') and not len(machine.strip()):
raise XmlParseException("<node> 'machine' must be non-empty: [%s]"%machine)
if not machine and default_machine:
machine = default_machine.name
# validate respawn, required
required, respawn = [_bool_attr(*rr) for rr in ((required, False, 'required'),\
(respawn, False, 'respawn'))]
respawn_delay = _float_attr(respawn_delay, 0.0, 'respawn_delay')
# each node gets its own copy of <remap> arguments, which
# it inherits from its parent
remap_context = context.child('')
# each node gets its own copy of <env> arguments, which
# it inherits from its parent
env_context = context.child('')
# nodes can have individual env args set in addition to
# the ROS-specific ones.
for t in [c for c in tag.childNodes if c.nodeType == DomNode.ELEMENT_NODE]:
tag_name = t.tagName.lower()
if tag_name == 'remap':
r = self._remap_tag(t, context, ros_config)
if r is not None:
remap_context.add_remap(r)
elif tag_name == 'param':
self._param_tag(t, param_ns, ros_config, force_local=True, verbose=verbose)
elif tag_name == 'rosparam':
self._rosparam_tag(t, param_ns, ros_config, verbose=verbose)
elif tag_name == 'env':
self._env_tag(t, env_context, ros_config)
else:
ros_config.add_config_error("WARN: unrecognized '%s' child tag in the parent tag element: %s"%(t.tagName, tag.toxml()))
# #1036 evaluate all ~params in context
# TODO: can we get rid of force_local (above), remove this for loop, and just rely on param_tag logic instead?
for p in itertools.chain(context.params, param_ns.params):
pkey = p.key
if is_private(pkey):
# strip leading ~, which is optional/inferred
pkey = pkey[1:]
pkey = param_ns.ns + pkey
ros_config.add_param(Param(pkey, p.value), verbose=verbose)
if not is_test:
return Node(pkg, node_type, name=name, namespace=child_ns.ns, machine_name=machine,
args=args, respawn=respawn,
respawn_delay=respawn_delay,
remap_args=remap_context.remap_args(), env_args=env_context.env_args,
output=output, cwd=cwd, launch_prefix=launch_prefix,
required=required, filename=context.filename)
else:
return Test(test_name, pkg, node_type, name=name, namespace=child_ns.ns,
machine_name=machine, args=args,
remap_args=remap_context.remap_args(), env_args=env_context.env_args,
time_limit=time_limit, cwd=cwd, launch_prefix=launch_prefix,
retry=retry, filename=context.filename)
except KeyError as e:
raise XmlParseException(
"<%s> tag is missing required attribute: %s. Node xml is %s"%(tag.tagName, e, tag.toxml()))
except XmlParseException as e:
raise XmlParseException(
"Invalid <node> tag: %s. \n\nNode xml is %s"%(e, tag.toxml()))
except ValueError as e:
raise XmlParseException(
"Invalid <node> tag: %s. \n\nNode xml is %s"%(e, tag.toxml()))
MACHINE_ATTRS = ('name', 'address', 'env-loader',
'ssh-port', 'user', 'password', 'default', 'timeout')
@ifunless
def _machine_tag(self, tag, context, ros_config, verbose=True):
try:
# clone context as <machine> tag sets up its own env args
context = context.child(None)
# pre-fuerte warning attributes
attrs = self.opt_attrs(tag, context,
('ros-root', 'ros-package-path', 'ros-ip', 'ros-hostname'))
if any(attrs):
raise XmlParseException("<machine>: ros-* attributes are not supported since ROS Fuerte.\nPlease use env-loader instead")
self._check_attrs(tag, context, ros_config, XmlLoader.MACHINE_ATTRS)
# required attributes
name, address = self.reqd_attrs(tag, context, ('name', 'address'))
# optional attributes
attrs = self.opt_attrs(tag, context,
('env-loader',
'ssh-port', 'user', 'password', 'default', 'timeout'))
env_loader, ssh_port, user, password, default, timeout = attrs
ssh_port = int(ssh_port or '22')
# check for default switch
default = (default or 'false').lower()
try:
assignable = _assignable[default]
is_default = _is_default[default]
except KeyError as e:
raise XmlParseException("Invalid value for 'attribute': %s"%default)
# load env args
for t in [c for c in tag.childNodes if c.nodeType == DomNode.ELEMENT_NODE]:
if t.tagName == 'env':
raise XmlParseException("<machine>: <env> tag is not supported since ROS Fuerte.\nPlease use env-loader instead")
else:
ros_config.add_config_error("unrecognized '%s' tag in <%s> tag"%(t.tagName, tag.tagName))
# cast timeout to float. make sure timeout wasn't an empty string or negative
if timeout:
try:
timeout = float(timeout)
except ValueError:
raise XmlParseException("'timeout' be a number: [%s]"%timeout)
elif timeout == '':
raise XmlParseException("'timeout' cannot be empty")
if timeout is not None and timeout <= 0.:
raise XmlParseException("'timeout' be a positive number: [%s]"%timeout)
m = Machine(name, address, env_loader=env_loader,
ssh_port=ssh_port, user=user, password=password,
assignable=assignable, env_args=context.env_args, timeout=timeout)
return (m, is_default)
except KeyError as e:
raise XmlParseException("<machine> tag is missing required attribute: %s"%e)
except SubstitutionException as e:
raise XmlParseException(
"%s. \n\nMachine xml is %s"%(e, tag.toxml()))
except RLException as e:
raise XmlParseException(
"%s. \n\nMachine xml is %s"%(e, tag.toxml()))
REMAP_ATTRS = ('from', 'to')
@ifunless
def _remap_tag(self, tag, context, ros_config):
try:
self._check_attrs(tag, context, ros_config, XmlLoader.REMAP_ATTRS)
return self.reqd_attrs(tag, context, XmlLoader.REMAP_ATTRS)
except KeyError as e:
raise XmlParseException("<remap> tag is missing required from/to attributes: %s"%tag.toxml())
ENV_ATTRS = ('name', 'value')
@ifunless
def _env_tag(self, tag, context, ros_config):
try:
self._check_attrs(tag, context, ros_config, XmlLoader.ENV_ATTRS)
self.load_env(context, ros_config, *self.reqd_attrs(tag, context, XmlLoader.ENV_ATTRS))
except ValueError as e:
raise XmlParseException("Invalid <env> tag: %s. \nXML is %s"%(str(e), tag.toxml()))
except KeyError as e:
raise XmlParseException("<env> tag is missing required name/value attributes: %s"%tag.toxml())
def _ns_clear_params_attr(self, tag_name, tag, context, ros_config, node_name=None, include_filename=None):
"""
Common processing routine for xml tags with NS and CLEAR_PARAMS attributes
@param tag: DOM Node
@type tag: Node
@param context: current namespace context
@type context: LoaderContext
@param clear_params: list of params to clear
@type clear_params: [str]
@param node_name: name of node (for use when tag_name == 'node')
@type node_name: str
@param include_filename: <include> filename if this is an <include> tag. If specified, context will use include rules.
@type include_filename: str
@return: loader context
@rtype: L{LoaderContext}
"""
if tag.hasAttribute(NS):
ns = self.resolve_args(tag.getAttribute(NS), context)
if not ns:
raise XmlParseException("<%s> tag has an empty '%s' attribute"%(tag_name, NS))
else:
ns = None
if include_filename is not None:
child_ns = context.include_child(ns, include_filename)
else:
child_ns = context.child(ns)
clear_p = self.resolve_args(tag.getAttribute(CLEAR_PARAMS), context)
if clear_p:
clear_p = _bool_attr(clear_p, False, 'clear_params')
if clear_p:
if tag_name == 'node':
if not node_name:
raise XmlParseException("<%s> tag must have a 'name' attribute to use '%s' attribute"%(tag_name, CLEAR_PARAMS))
# use make_global_ns to give trailing slash in order to be consistent with XmlContext.ns
ros_config.add_clear_param(make_global_ns(ns_join(child_ns.ns, node_name)))
else:
if not ns:
raise XmlParseException("'ns' attribute must be set in order to use 'clear_params'")
ros_config.add_clear_param(child_ns.ns)
return child_ns
@ifunless
def _launch_tag(self, tag, ros_config, filename=None):
# #2499
deprecated = tag.getAttribute('deprecated')
if deprecated:
if filename:
ros_config.add_config_error("[%s] DEPRECATED: %s"%(filename, deprecated))
else:
ros_config.add_config_error("Deprecation Warning: "+deprecated)
INCLUDE_ATTRS = ('file', NS, CLEAR_PARAMS, 'pass_all_args')
@ifunless
def _include_tag(self, tag, context, ros_config, default_machine, is_core, verbose):
self._check_attrs(tag, context, ros_config, XmlLoader.INCLUDE_ATTRS)
inc_filename = self.resolve_args(tag.attributes['file'].value, context)
if tag.hasAttribute('pass_all_args'):
pass_all_args = self.resolve_args(tag.attributes['pass_all_args'].value, context)
pass_all_args = _bool_attr(pass_all_args, False, 'pass_all_args')
else:
pass_all_args = False
child_ns = self._ns_clear_params_attr(tag.tagName, tag, context, ros_config, include_filename=inc_filename)
# If we're asked to pass all args, then we need to add them into the
# child context.
if pass_all_args:
if 'arg' in context.resolve_dict:
for name, value in context.resolve_dict['arg'].items():
child_ns.add_arg(name, value=value)
# Also set the flag that tells the child context to ignore (rather than
# error on) attempts to set the same arg twice.
child_ns.pass_all_args = True
for t in [c for c in tag.childNodes if c.nodeType == DomNode.ELEMENT_NODE]:
tag_name = t.tagName.lower()
if tag_name == 'env':
self._env_tag(t, child_ns, ros_config)
elif tag_name == 'arg':
self._arg_tag(t, child_ns, ros_config, verbose=verbose)
else:
print("WARN: unrecognized '%s' tag in <%s> tag"%(t.tagName, tag.tagName), file=sys.stderr)
# setup arg passing
loader.process_include_args(child_ns)
try:
launch = self._parse_launch(inc_filename, verbose=verbose)
ros_config.add_roslaunch_file(inc_filename)
self._launch_tag(launch, ros_config, filename=inc_filename)
default_machine = \
self._recurse_load(ros_config, launch.childNodes, child_ns, \
default_machine, is_core, verbose)
# check for unused args
loader.post_process_include_args(child_ns)
except ArgException as e:
raise XmlParseException("included file [%s] requires the '%s' arg to be set"%(inc_filename, str(e)))
except XmlParseException as e:
raise XmlParseException("while processing %s:\n%s"%(inc_filename, str(e)))
if verbose:
print("... done importing include file [%s]"%inc_filename)
return default_machine
GROUP_ATTRS = (NS, CLEAR_PARAMS)
def _recurse_load(self, ros_config, tags, context, default_machine, is_core, verbose):
"""
@return: new default machine for current context
@rtype: L{Machine}
"""
for tag in [t for t in tags if t.nodeType == DomNode.ELEMENT_NODE]:
name = tag.tagName
if name == 'group':
if ifunless_test(self, tag, context):
self._check_attrs(tag, context, ros_config, XmlLoader.GROUP_ATTRS)
child_ns = self._ns_clear_params_attr(name, tag, context, ros_config)
child_ns.params = list(child_ns.params) # copy is needed here to enclose new params
default_machine = \
self._recurse_load(ros_config, tag.childNodes, child_ns, \
default_machine, is_core, verbose)
elif name == 'node':
n = self._node_tag(tag, context, ros_config, default_machine, verbose=verbose)
if n is not None:
ros_config.add_node(n, core=is_core, verbose=verbose)
elif name == 'test':
t = self._node_tag(tag, context, ros_config, default_machine, is_test=True, verbose=verbose)
if t is not None:
ros_config.add_test(t, verbose=verbose)
elif name == 'param':
self._param_tag(tag, context, ros_config, verbose=verbose)
elif name == 'remap':
try:
r = self._remap_tag(tag, context, ros_config)
if r is not None:
context.add_remap(r)
except RLException as e:
raise XmlParseException("Invalid <remap> tag: %s.\nXML is %s"%(str(e), tag.toxml()))
elif name == 'machine':
val = self._machine_tag(tag, context, ros_config, verbose=verbose)
if val is not None:
(m, is_default) = val
if is_default:
default_machine = m
ros_config.add_machine(m, verbose=verbose)
elif name == 'rosparam':
self._rosparam_tag(tag, context, ros_config, verbose=verbose)
elif name == 'master':
pass #handled non-recursively
elif name == 'include':
val = self._include_tag(tag, context, ros_config, default_machine, is_core, verbose)
if val is not None:
default_machine = val
elif name == 'env':
self._env_tag(tag, context, ros_config)
elif name == 'arg':
self._arg_tag(tag, context, ros_config, verbose=verbose)
else:
ros_config.add_config_error("unrecognized tag "+tag.tagName)
return default_machine
def _load_launch(self, launch, ros_config, is_core=False, filename=None, argv=None, verbose=True):
"""
subroutine of launch for loading XML DOM into config. Load_launch assumes that it is
creating the root XmlContext, and is thus affected by command-line arguments.
@param launch: DOM node of the root <launch> tag in the file
@type launch: L{Node}
@param ros_config: launch configuration to load XML file into
@type ros_config: L{ROSLaunchConfig}
@param is_core: (optional) if True, load file using ROS core rules. Default False.
@type is_core: bool
@param filename: (optional) name of file being loaded
@type filename: str
@param verbose: (optional) print verbose output. Default False.
@type verbose: bool
@param argv: (optional) command-line args. Default sys.argv.
"""
if argv is None:
argv = sys.argv
self._launch_tag(launch, ros_config, filename)
self.root_context = loader.LoaderContext(get_ros_namespace(), filename)
loader.load_sysargs_into_context(self.root_context, argv)
if len(launch.getElementsByTagName('master')) > 0:
print("WARNING: ignoring defunct <master /> tag", file=sys.stderr)
self._recurse_load(ros_config, launch.childNodes, self.root_context, None, is_core, verbose)
def _parse_launch(self, filename, verbose):
try:
if verbose:
print("... loading XML file [%s]"%filename)
root = parse(filename).getElementsByTagName('launch')
except Exception as e:
raise XmlParseException("Invalid roslaunch XML syntax: %s"%e)
if len(root) != 1:
raise XmlParseException("Invalid roslaunch XML syntax: no root <launch> tag")
return root[0]
def load(self, filename, ros_config, core=False, argv=None, verbose=True):
"""
load XML file into launch configuration
@param filename: XML config file to load
@type filename: str
@param ros_config: launch configuration to load XML file into
@type ros_config: L{ROSLaunchConfig}
@param core: if True, load file using ROS core rules
@type core: bool
@param argv: override command-line arguments (mainly for arg testing)
@type argv: [str]
"""
try:
launch = self._parse_launch(filename, verbose)
ros_config.add_roslaunch_file(filename)
self._load_launch(launch, ros_config, is_core=core, filename=filename, argv=argv, verbose=verbose)
except ArgException as e:
raise XmlParseException("[%s] requires the '%s' arg to be set"%(filename, str(e)))
except SubstitutionException as e:
raise XmlParseException(str(e))
def load_string(self, xml_text, ros_config, core=False, verbose=True):
"""
Load XML text into launch configuration
@param xml_text: XML configuration
@type xml_text: str
@param ros_config: launch configuration to load XML file into
@type ros_config: L{ROSLaunchConfig}
@param core: if True, load file using ROS core rules
@type core: bool
"""
try:
if verbose:
print("... loading XML")
try:
if hasattr(xml_text,'encode') and isinstance(xml_text, unicode):
# #3799: xml_text comes in a unicode object, which
# #fails since XML text is expected to be encoded.
# that's why force encoding to utf-8 here (make sure XML header is utf-8)
xml_text = xml_text.encode('utf-8')
except NameError:
pass
root = parseString(xml_text).getElementsByTagName('launch')
except Exception as e:
logging.getLogger('roslaunch').error("Invalid roslaunch XML syntax:\nstring[%s]\ntraceback[%s]"%(xml_text, traceback.format_exc()))
raise XmlParseException("Invalid roslaunch XML syntax: %s"%e)
if len(root) != 1:
raise XmlParseException("Invalid roslaunch XML syntax: no root <launch> tag")
self._load_launch(root[0], ros_config, core, filename='string', verbose=verbose)

View File

@@ -0,0 +1,2 @@
string1: bar
dict1: { head: 1, shoulders: 2, knees: 3, toes: 4}

View File

@@ -0,0 +1,22 @@
string1: bar
string2: !!str 10
preformattedtext: |
This is the first line
This is the second line
Line breaks are preserved
Indentation is stripped
list1:
- head
- shoulders
- knees
- toes
list2: [1, 1, 2, 3, 5, 8]
dict1: { head: 1, shoulders: 2, knees: 3, toes: 4}
integer1: 1
integer2: 2
float1: 3.14159
float2: 1.2345e+3
robots:
childparam: a child namespace parameter
child:
grandchildparam: a grandchild namespace param

View File

@@ -0,0 +1,3 @@
#
#
#

View File

@@ -0,0 +1 @@
string1: $(anon foo)

View File

@@ -0,0 +1,201 @@
#!/usr/bin/env python
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
NAME = 'test_core'
import os
import sys
import unittest
import rospkg
_lastmsg = None
def printlog_cb(msg):
global _lastmsg
_lastmsg = msg
def printlog_cb_exception(msg):
raise Exception(msg)
## Test roslaunch.core
class TestCore(unittest.TestCase):
def test_xml_escape(self):
from roslaunch.core import _xml_escape
self.assertEquals('', _xml_escape(''))
self.assertEquals(' ', _xml_escape(' '))
self.assertEquals('&quot;', _xml_escape('"'))
self.assertEquals('&quot;hello world&quot;', _xml_escape('"hello world"'))
self.assertEquals('&gt;', _xml_escape('>'))
self.assertEquals('&lt;', _xml_escape('<'))
self.assertEquals('&amp;', _xml_escape('&'))
self.assertEquals('&amp;amp;', _xml_escape('&amp;'))
self.assertEquals('&amp;&quot;&gt;&lt;&quot;', _xml_escape('&"><"'))
def test_child_mode(self):
from roslaunch.core import is_child_mode, set_child_mode
set_child_mode(False)
self.failIf(is_child_mode())
set_child_mode(True)
self.assert_(is_child_mode())
set_child_mode(False)
self.failIf(is_child_mode())
def test_printlog(self):
from roslaunch.core import add_printlog_handler, add_printerrlog_handler, printlog, printlog_bold, printerrlog
add_printlog_handler(printlog_cb)
add_printlog_handler(printlog_cb_exception)
add_printerrlog_handler(printlog_cb)
add_printerrlog_handler(printlog_cb_exception)
#can't really test functionality, just make sure it doesn't crash
global _lastmsg
_lastmsg = None
printlog('foo')
self.assertEquals('foo', _lastmsg)
printlog_bold('bar')
self.assertEquals('bar', _lastmsg)
printerrlog('baz')
self.assertEquals('baz', _lastmsg)
def test_setup_env(self):
from roslaunch.core import setup_env, Node, Machine
master_uri = 'http://masteruri:1234'
n = Node('nodepkg','nodetype')
m = Machine('name1', '1.2.3.4')
d = setup_env(n, m, master_uri)
self.assertEquals(d['ROS_MASTER_URI'], master_uri)
m = Machine('name1', '1.2.3.4')
d = setup_env(n, m, master_uri)
val = os.environ['ROS_ROOT']
self.assertEquals(d['ROS_ROOT'], val)
# test ROS_NAMESPACE
# test stripping
n = Node('nodepkg','nodetype', namespace="/ns2/")
d = setup_env(n, m, master_uri)
self.assertEquals(d['ROS_NAMESPACE'], "/ns2")
# test node.env_args
n = Node('nodepkg','nodetype', env_args=[('NENV1', 'val1'), ('NENV2', 'val2'), ('ROS_ROOT', '/new/root')])
d = setup_env(n, m, master_uri)
self.assertEquals(d['ROS_ROOT'], "/new/root")
self.assertEquals(d['NENV1'], "val1")
self.assertEquals(d['NENV2'], "val2")
def test_local_machine(self):
try:
env_copy = os.environ.copy()
from roslaunch.core import local_machine
lm = local_machine()
self.failIf(lm is None)
#singleton
self.assert_(lm == local_machine())
self.assertEquals(lm.name, '')
self.assertEquals(lm.assignable, True)
self.assertEquals(lm.env_loader, None)
self.assertEquals(lm.user, None)
self.assertEquals(lm.password, None)
finally:
os.environ = env_copy
def test_Master(self):
from roslaunch.core import Master
old_env = os.environ.get('ROS_MASTER_URI', None)
try:
os.environ['ROS_MASTER_URI'] = 'http://foo:789'
m = Master()
self.assertEquals(m.type, Master.ROSMASTER)
self.assertEquals(m.uri, 'http://foo:789')
self.assertEquals(m, m)
self.assertEquals(m, Master())
m = Master(Master.ROSMASTER, 'http://foo:1234')
self.assertEquals(m.type, Master.ROSMASTER)
self.assertEquals(m.uri, 'http://foo:1234')
self.assertEquals(m, m)
self.assertEquals(m, Master(Master.ROSMASTER, 'http://foo:1234'))
try:
from xmlrpc.client import ServerProxy
except ImportError:
from xmlrpclib import ServerProxy
self.assert_(isinstance(m.get(), ServerProxy))
m.uri = 'http://foo:567'
self.assertEquals(567, m.get_port())
self.failIf(m.is_running())
finally:
if old_env is None:
del os.environ['ROS_MASTER_URI']
else:
os.environ['ROS_MASTER_URI'] = old_env
def test_Executable(self):
from roslaunch.core import Executable, PHASE_SETUP, PHASE_RUN, PHASE_TEARDOWN
e = Executable('ls', ['-alF'])
self.assertEquals('ls', e.command)
self.assertEquals(['-alF'], e.args)
self.assertEquals(PHASE_RUN, e.phase)
self.assertEquals('ls -alF', str(e))
self.assertEquals('ls -alF', repr(e))
e = Executable('ls', ['-alF', 'b*'], PHASE_TEARDOWN)
self.assertEquals('ls', e.command)
self.assertEquals(['-alF', 'b*'], e.args)
self.assertEquals(PHASE_TEARDOWN, e.phase)
self.assertEquals('ls -alF b*', str(e))
self.assertEquals('ls -alF b*', repr(e))
def test_RosbinExecutable(self):
from roslaunch.core import RosbinExecutable, PHASE_SETUP, PHASE_RUN, PHASE_TEARDOWN
e = RosbinExecutable('ls', ['-alF'])
self.assertEquals('ls', e.command)
self.assertEquals(['-alF'], e.args)
self.assertEquals(PHASE_RUN, e.phase)
self.assertEquals('ros/bin/ls -alF', str(e))
self.assertEquals('ros/bin/ls -alF', repr(e))
e = RosbinExecutable('ls', ['-alF', 'b*'], PHASE_TEARDOWN)
self.assertEquals('ls', e.command)
self.assertEquals(['-alF', 'b*'], e.args)
self.assertEquals(PHASE_TEARDOWN, e.phase)
self.assertEquals('ros/bin/ls -alF b*', str(e))
self.assertEquals('ros/bin/ls -alF b*', repr(e))

View File

@@ -0,0 +1,280 @@
#!/usr/bin/env python
# Software License Agreement (BSD License)
#
# Copyright (c) 2009, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import os
import sys
import unittest
import rospkg
import roslib.packages
import logging
logging.getLogger('roslaunch').setLevel(logging.CRITICAL)
## Test roslaunch.nodeprocess
class TestNodeprocess(unittest.TestCase):
def test_create_master_process(self):
from roslaunch.core import Node, Machine, Master, RLException
from roslaunch.nodeprocess import create_master_process, LocalProcess
ros_root = '/ros/root'
port = 1234
type = Master.ROSMASTER
run_id = 'foo'
# test invalid params
try:
create_master_process(run_id, type, ros_root, -1)
self.fail("shoud have thrown RLException")
except RLException: pass
try:
create_master_process(run_id, type, ros_root, 10000000)
self.fail("shoud have thrown RLException")
except RLException: pass
try:
create_master_process(run_id, 'foo', ros_root, port)
self.fail("shoud have thrown RLException")
except RLException: pass
# test valid params
p = create_master_process(run_id, type, ros_root, port)
self.assert_(isinstance(p, LocalProcess))
self.assertEquals(p.args[0], 'rosmaster')
idx = p.args.index('-p')
self.failIf(idx < 1)
self.assertEquals(p.args[idx+1], str(port))
self.assert_('--core' in p.args)
self.assertEquals(p.package, 'rosmaster')
p = create_master_process(run_id, type, ros_root, port)
# TODO: have to think more as to the correct environment for the master process
def test_create_node_process(self):
from roslaunch.core import Node, Machine, RLException
from roslaunch.node_args import NodeParamsException
from roslaunch.nodeprocess import create_node_process, LocalProcess
# have to use real ROS configuration for these tests
ros_root = os.environ['ROS_ROOT']
rpp = os.environ.get('ROS_PACKAGE_PATH', None)
master_uri = 'http://masteruri:1234'
m = Machine('name1', ros_root, rpp, '1.2.3.4')
run_id = 'id'
# test invalid params
n = Node('not_a_real_package','not_a_node')
n.machine = m
n.name = 'foo'
try: # should fail b/c node cannot be found
create_node_process(run_id, n, master_uri)
self.fail("should have failed")
except NodeParamsException:
pass
# have to specify a real node
n = Node('rospy','talker.py')
n.machine = m
try: # should fail b/c n.name is not set
create_node_process(run_id, n, master_uri)
self.fail("should have failed")
except ValueError:
pass
# have to specify a real node
n = Node('rospy','talker.py')
n.machine = None
n.name = 'talker'
try: # should fail b/c n.machine is not set
create_node_process(run_id, n, master_uri)
self.fail("should have failed")
except RLException:
pass
# basic integration test
n.machine = m
p = create_node_process(run_id, n, master_uri)
self.assert_(isinstance(p, LocalProcess))
# repeat some setup_local_process_env tests
d = p.env
self.assertEquals(d['ROS_MASTER_URI'], master_uri)
self.assertEquals(d['ROS_ROOT'], ros_root)
self.assertEquals(d['PYTHONPATH'], os.environ['PYTHONPATH'])
if rpp:
self.assertEquals(d['ROS_PACKAGE_PATH'], rpp)
for k in ['ROS_IP', 'ROS_NAMESPACE']:
if k in d:
self.fail('%s should not be set: %s'%(k,d[k]))
# test package and name
self.assertEquals(p.package, 'rospy')
# - no 'correct' full answer here
self.assert_(p.name.startswith('talker'), p.name)
# test log_output
n.output = 'log'
self.assert_(create_node_process(run_id, n, master_uri).log_output)
n.output = 'screen'
self.failIf(create_node_process(run_id, n, master_uri).log_output)
# test respawn
n.respawn = True
self.assert_(create_node_process(run_id, n, master_uri).respawn)
n.respawn = False
self.failIf(create_node_process(run_id, n, master_uri).respawn)
# test cwd
n.cwd = None
self.assertEquals(create_node_process(run_id, n, master_uri).cwd, None)
n.cwd = 'ros-root'
self.assertEquals(create_node_process(run_id, n, master_uri).cwd, 'ros-root')
n.cwd = 'node'
self.assertEquals(create_node_process(run_id, n, master_uri).cwd, 'node')
# test args
# - simplest test (no args)
n.args = ''
p = create_node_process(run_id, n, master_uri)
# - the first arg should be the path to the node executable
rospack = rospkg.RosPack()
cmd = roslib.packages.find_node('rospy', 'talker.py', rospack)[0]
self.assertEquals(p.args[0], cmd)
# - test basic args
n.args = "arg1 arg2 arg3"
p = create_node_process(run_id, n, master_uri)
self.assertEquals(p.args[0], cmd)
for a in "arg1 arg2 arg3".split():
self.assert_(a in p.args)
# - test remap args
n.remap_args = [('KEY1', 'VAL1'), ('KEY2', 'VAL2')]
p = create_node_process(run_id, n, master_uri)
self.assert_('KEY1:=VAL1' in p.args)
self.assert_('KEY2:=VAL2' in p.args)
# - test __name
n = Node('rospy','talker.py')
n.name = 'fooname'
n.machine = m
self.assert_('__name:=fooname' in create_node_process(run_id, n, master_uri).args)
# - test substitution args
os.environ['SUB_TEST'] = 'subtest'
os.environ['SUB_TEST2'] = 'subtest2'
n.args = 'foo $(env SUB_TEST) $(env SUB_TEST2)'
p = create_node_process(run_id, n, master_uri)
self.failIf('SUB_TEST' in p.args)
self.assert_('foo' in p.args)
self.assert_('subtest' in p.args)
self.assert_('subtest2' in p.args)
def test__cleanup_args(self):
# #1595
from roslaunch.nodeprocess import _cleanup_remappings
args = [
'ROS_PACKAGE_PATH=foo',
'/home/foo/monitor',
'__log:=/home/pr2/21851/ros-0.7.2/log/8d769688-897d-11de-8e93-00238bdfe0ab/monitor-3.log',
'__name:=bar',
'__log:=/home/pr2/21851/ros-0.7.2/log/8d769688-897d-11de-8e93-00238bdfe0ab/monitor-3.log',
'topic:=topic2',
'__log:=/home/pr2/21851/ros-0.7.2/log/8d769688-897d-11de-8e93-00238bdfe0ab/monitor-3.log',
'__log:=/home/pr2/21851/ros-0.7.2/log/8d769688-897d-11de-8e93-00238bdfe0ab/monitor-3.log',
'__log:=/home/pr2/21851/ros-0.7.2/log/8d769688-897d-11de-8e93-00238bdfe0ab/monitor-3.log',
'__log:=/home/pr2/21851/ros-0.7.2/log/8d769688-897d-11de-8e93-00238bdfe0ab/monitor-3.log',
'__log:=/home/pr2/21851/ros-0.7.2/log/8d769688-897d-11de-8e93-00238bdfe0ab/monitor-3.log']
clean_args = [
'ROS_PACKAGE_PATH=foo',
'/home/foo/monitor',
'__name:=bar',
'topic:=topic2']
self.assertEquals([], _cleanup_remappings([], '__log:='))
self.assertEquals(clean_args, _cleanup_remappings(args, '__log:='))
self.assertEquals(clean_args, _cleanup_remappings(clean_args, '__log:='))
self.assertEquals(args, _cleanup_remappings(args, '_foo'))
def test__next_counter(self):
from roslaunch.nodeprocess import _next_counter
x = _next_counter()
y = _next_counter()
self.assert_(x +1 == y)
self.assert_(x > 0)
def test_create_master_process2(self):
# accidentally wrote two versions of this, need to merge
from roslaunch.core import Master, RLException
import rospkg
from roslaunch.nodeprocess import create_master_process
ros_root = rospkg.get_ros_root()
# test failures
failed = False
try:
create_master_process('runid-unittest', Master.ROSMASTER, rospkg.get_ros_root(), 0)
failed = True
except RLException: pass
self.failIf(failed, "invalid port should have triggered error")
# test success with ROSMASTER
m1 = create_master_process('runid-unittest', Master.ROSMASTER, ros_root, 1234)
self.assertEquals('runid-unittest', m1.run_id)
self.failIf(m1.started)
self.failIf(m1.stopped)
self.assertEquals(None, m1.cwd)
self.assertEquals('master', m1.name)
master_p = 'rosmaster'
self.assert_(master_p in m1.args)
# - it should have the default environment
self.assertEquals(os.environ, m1.env)
# - check args
self.assert_('--core' in m1.args)
# - make sure port arguent is correct
idx = m1.args.index('-p')
self.assertEquals('1234', m1.args[idx+1])
# test port argument
m2 = create_master_process('runid-unittest', Master.ROSMASTER, ros_root, 1234)
self.assertEquals('runid-unittest', m2.run_id)
# test ros_root argument
m3 = create_master_process('runid-unittest', Master.ROSMASTER, ros_root, 1234)
self.assertEquals('runid-unittest', m3.run_id)
master_p = 'rosmaster'
self.assert_(master_p in m3.args)

View File

@@ -0,0 +1,190 @@
#!/usr/bin/env python
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import os, sys, unittest
import time
import rosgraph
import roslaunch.child
import roslaunch.server
## Fake RemoteProcess object
class ChildProcessMock(roslaunch.server.ChildROSLaunchProcess):
def __init__(self, name, args=[], env={}):
super(ChildProcessMock, self).__init__(name, args, env)
self.stopped = False
def stop(self):
self.stopped = True
## Fake ProcessMonitor object
class ProcessMonitorMock(object):
def __init__(self):
self.core_procs = []
self.procs = []
self.listeners = []
def join(self, timeout=0):
pass
def add_process_listener(self, l):
self.listeners.append(l)
def register(self, p):
self.procs.append(p)
def register_core_proc(self, p):
self.core_procs.append(p)
def registrations_complete(self):
pass
def unregister(self, p):
self.procs.remove(p)
def has_process(self, name):
return len([p for p in self.procs if p.name == name]) > 0
def get_process(self, name):
val = [p for p in self.procs if p.name == name]
if val:
return val[0]
return None
def has_main_thread_jobs(self):
return False
def do_main_thread_jobs(self):
pass
def kill_process(self, name):
pass
def shutdown(self):
pass
def get_active_names(self):
return [p.name for p in self.procs]
def get_process_names_with_spawn_count(self):
actives = [(p.name, p.spawn_count) for p in self.procs]
deads = []
return [actives, deads]
def mainthread_spin_once(self):
pass
def mainthread_spin(self):
pass
def run(self):
pass
## Test roslaunch.server
class TestRoslaunchChild(unittest.TestCase):
def setUp(self):
self.pmon = ProcessMonitorMock()
try:
# if there is a core up, we have to use its run id
m = rosgraph.Master('/roslaunch')
self.run_id = m.getParam('/run_id')
except:
self.run_id = 'foo-%s'%time.time()
def test_roslaunchChild(self):
# this is mainly a code coverage test to try and make sure that we don't
# have any uninitialized references, etc...
from roslaunch.child import ROSLaunchChild
name = 'child-%s'%time.time()
server_uri = 'http://unroutable:1234'
c = ROSLaunchChild(self.run_id, name, server_uri)
self.assertEquals(self.run_id, c.run_id)
self.assertEquals(name, c.name)
self.assertEquals(server_uri, c.server_uri)
# - this check tests our assumption about c's process monitor field
self.assertEquals(None, c.pm)
self.assertEquals(None, c.child_server)
# should be a noop
c.shutdown()
# create a new child to test _start_pm() and shutdown()
c = ROSLaunchChild(self.run_id, name, server_uri)
# - test _start_pm and shutdown logic
c._start_pm()
self.assert_(c.pm is not None)
c.shutdown()
# create a new child to test run() with a fake process
# monitor. this requires an actual parent server to be running
import roslaunch.config
server = roslaunch.server.ROSLaunchParentNode(roslaunch.config.ROSLaunchConfig(), self.pmon)
# - register a fake child with the server so that it accepts registration from ROSLaunchChild
server.add_child(name, ChildProcessMock('foo'))
try:
server.start()
self.assert_(server.uri, "server URI did not initialize")
c = ROSLaunchChild(self.run_id, name, server.uri)
c.pm = self.pmon
# - run should speed through
c.run()
finally:
server.shutdown('test done')
# one final test for code completness: raise an exception during run()
c = ROSLaunchChild(self.run_id, name, server_uri)
def bad():
raise Exception('haha')
# - violate some encapsulation here just to make sure the exception happens
c._start_pm = bad
try:
# this isn't really a correctness test, this just manually
# tests that the exception is logged
c.run()
except:
pass
def kill_parent(p, delay=1.0):
# delay execution so that whatever pmon method we're calling has time to enter
import time
time.sleep(delay)
print("stopping parent")
p.shutdown()

View File

@@ -0,0 +1,311 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2012, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import os
import sys
import rospkg
try:
from xmlrpc.client import MultiCall, ServerProxy
except ImportError:
from xmlrpclib import MultiCall, ServerProxy
import roslaunch.core
def test_Executable():
from roslaunch.core import Executable, PHASE_SETUP
e = Executable('cmd', ('arg1', 'arg2'), PHASE_SETUP)
assert e.command == 'cmd'
assert e.args == ('arg1', 'arg2')
assert e.phase == PHASE_SETUP
assert 'cmd' in str(e)
assert 'arg2' in str(e)
assert 'cmd' in repr(e)
assert 'arg2' in repr(e)
def test__xml_escape():
# this is a really bad xml escaper
from roslaunch.core import _xml_escape
assert _xml_escape("&<foo>") == "&amp;&lt;foo&gt;"
def test_RosbinExecutable():
from roslaunch.core import RosbinExecutable, PHASE_SETUP, PHASE_RUN
e = RosbinExecutable('cmd', ('arg1', 'arg2'), PHASE_SETUP)
assert e.command == 'cmd'
assert e.args == ('arg1', 'arg2')
assert e.phase == PHASE_SETUP
assert 'cmd' in str(e)
assert 'arg2' in str(e)
assert 'cmd' in repr(e)
assert 'arg2' in repr(e)
assert 'ros/bin' in str(e)
assert 'ros/bin' in repr(e)
e = RosbinExecutable('cmd', ('arg1', 'arg2'))
assert e.phase == PHASE_RUN
def test_generate_run_id():
from roslaunch.core import generate_run_id
assert generate_run_id()
def test_Node():
from roslaunch.core import Node
n = Node('package', 'node_type')
assert n.package == 'package'
assert n.type == 'node_type'
assert n.xmltype() == 'node'
assert n.xmlattrs() == [('pkg', 'package'), ('type', 'node_type'),
('machine', None), ('ns', '/'), ('args', ''), ('output', None),
('cwd', None), ('respawn', False), ('respawn_delay', 0.0),
('name', None), ('launch-prefix', None), ('required', False)], \
n.xmlattrs()
assert n.output == None
#tripwire for now
n.to_xml()
val = n.to_remote_xml()
assert 'machine' not in val
# test bad constructors
try:
n = Node('package', 'node_type', cwd='foo')
assert False
except ValueError:
pass
try:
n = Node('package', 'node_type', output='foo')
assert False
except ValueError:
pass
try:
n = Node('package', '')
assert False
except ValueError:
pass
try:
n = Node('', 'node_type')
assert False
except ValueError:
pass
try:
n = Node('package', 'node_type', name='ns/node')
assert False
except ValueError:
pass
try:
n = Node('package', 'node_type', respawn=True, required=True)
assert False
except ValueError:
pass
def test_Param():
from roslaunch.core import Param
p = Param('key/', 'value')
assert p.key == 'key'
assert p.value == 'value'
assert p == Param('key', 'value')
assert p != Param('key2', 'value')
assert p != 1
assert 'key' in str(p)
assert 'key' in repr(p)
assert 'value' in str(p)
assert 'value' in repr(p)
def test_local_machine():
from roslaunch.core import local_machine
m = local_machine()
assert m is local_machine()
assert m == local_machine()
assert m.env_loader == None
assert m.name == ''
assert m.address == 'localhost'
assert m.ssh_port == 22
def test_Machine():
from roslaunch.core import Machine
m = Machine('foo', 'localhost')
assert 'foo' in str(m)
assert m.name == 'foo'
assert m.env_loader == None
assert m.assignable == True
assert m == m
assert not m.__eq__(1)
assert not m.config_equals(1)
assert m == Machine('foo', 'localhost')
assert m.config_equals(Machine('foo', 'localhost'))
assert m.config_key() == Machine('foo', 'localhost').config_key()
assert m.config_equals(Machine('foo', 'localhost'))
assert m.config_key() == Machine('foo', 'localhost', ssh_port=22).config_key()
assert m.config_equals(Machine('foo', 'localhost', ssh_port=22))
assert m.config_key() == Machine('foo', 'localhost', assignable=False).config_key()
assert m.config_equals(Machine('foo', 'localhost', assignable=False))
# original test suite
m = Machine('name1', '1.2.3.4')
str(m), m.config_key() #test for error
assert m == m
assert m == Machine('name1', '1.2.3.4')
# verify that config_equals is not tied to name or assignable, but is tied to other properties
assert m.config_equals(Machine('name1b', '1.2.3.4'))
assert m.config_equals(Machine('name1c', '1.2.3.4', assignable=False))
assert not m.config_equals(Machine('name1f', '2.2.3.4'))
assert m.name == 'name1'
assert m.address == '1.2.3.4'
assert m.assignable == True
assert m.ssh_port == 22
assert m.env_loader == None
for p in ['user', 'password']:
assert getattr(m, p) is None
m = Machine('name2', '2.2.3.4', assignable=False)
assert not m.assignable
str(m), m.config_key() #test for error
assert m == m
assert m == Machine('name2', '2.2.3.4', assignable=False)
assert m.config_equals(Machine('name2b', '2.2.3.4', assignable=False))
assert m.config_equals(Machine('name2c', '2.2.3.4', assignable=True))
m = Machine('name3', '3.3.3.4')
str(m) #test for error
assert m == m
assert m == Machine('name3', '3.3.3.4')
assert m.config_equals(Machine('name3b', '3.3.3.4'))
m = Machine('name4', '4.4.3.4', user='user4')
assert m.user == 'user4'
str(m), m.config_key() #test for error
assert m == m
assert m == Machine('name4', '4.4.3.4', user='user4')
assert m.config_equals(Machine('name4b', '4.4.3.4', user='user4'))
assert not m.config_equals(Machine('name4b', '4.4.3.4', user='user4b'))
m = Machine('name5', '5.5.3.4', password='password5')
assert m.password == 'password5'
str(m), m.config_key() #test for error
assert m == m
assert m == Machine('name5', '5.5.3.4', password='password5')
assert m.config_equals(Machine('name5b', '5.5.3.4', password='password5'))
assert not m.config_equals(Machine('name5c', '5.5.3.4', password='password5c'))
m = Machine('name6', '6.6.3.4', ssh_port=23)
assert m.ssh_port == 23
str(m) #test for error
m.config_key()
assert m == m
assert m == Machine('name6', '6.6.3.4', ssh_port=23)
assert m.config_equals(Machine('name6b', '6.6.3.4', ssh_port=23))
assert not m.config_equals(Machine('name6c', '6.6.3.4', ssh_port=24))
m = Machine('name6', '6.6.3.4', env_loader='/opt/ros/fuerte/setup.sh')
assert m.env_loader == '/opt/ros/fuerte/setup.sh'
str(m) #test for error
m.config_key()
assert m == m
assert m == Machine('name6', '6.6.3.4', env_loader='/opt/ros/fuerte/setup.sh')
assert m.config_equals(Machine('name6b', '6.6.3.4', env_loader='/opt/ros/fuerte/setup.sh'))
assert not m.config_equals(Machine('name6c', '6.6.3.4', env_loader='/opt/ros/groovy/setup.sh'))
def test_Master():
from roslaunch.core import Master
# can't verify value of is_running for actual master
m = Master(uri='http://localhost:11311')
assert m .type == Master.ROSMASTER
m.get_host() == 'localhost'
m.get_port() == 11311
assert m.is_running() in [True, False]
assert isinstance(m.get(), ServerProxy)
assert isinstance(m.get_multi(), MultiCall)
m = Master(uri='http://badhostname:11312')
m.get_host() == 'badhostname'
m.get_port() == 11312
assert m.is_running() == False
def test_Test():
from roslaunch.core import Test, TEST_TIME_LIMIT_DEFAULT
t = Test('test_name', 'package', 'node_type')
assert t.xmltype() == 'test'
assert t.xmlattrs() == [('pkg', 'package'), ('type', 'node_type'), ('machine', None), ('ns', '/'), ('args', ''), ('output', 'log'), ('cwd', None), ('name', None), ('launch-prefix', None), ('required', False), ('test-name', 'test_name')]
assert t.output == 'log'
assert t.test_name == 'test_name'
assert t.package == 'package'
assert t.type == 'node_type'
assert t.name == None
assert t.namespace == '/'
assert t.machine_name == None
assert t.args == ''
assert t.remap_args == [], t.remap_args
assert t.env_args == []
assert t.time_limit == TEST_TIME_LIMIT_DEFAULT
assert t.cwd is None
assert t.launch_prefix is None
assert t.retry == 0
assert t.filename == '<unknown>'
t = Test('test_name', 'package', 'node_type', 'name',
'/namespace', 'machine_name', 'arg1 arg2',
remap_args=[('from', 'to')], env_args=[('ENV', 'val')], time_limit=1.0, cwd='ros_home',
launch_prefix='foo', retry=1, filename="filename.txt")
xmlattrs = t.xmlattrs()
assert ('time-limit', 1.0) in xmlattrs
assert ('retry', '1') in xmlattrs
assert t.launch_prefix == 'foo'
assert t.remap_args == [('from', 'to')]
assert t.env_args == [('ENV', 'val')]
assert t.name == 'name'
assert t.namespace == '/namespace/', t.namespace
assert t.machine_name == 'machine_name'
assert t.args == 'arg1 arg2'
assert t.filename == 'filename.txt'
assert t.time_limit == 1.0
assert t.cwd == 'ROS_HOME'
assert t.retry == 1
try:
t = Test('test_name', 'package', 'node_type', time_limit=-1.0)
assert False
except ValueError:
pass
try:
t = Test('test_name', 'package', 'node_type', time_limit=True)
assert False
except ValueError:
pass

View File

@@ -0,0 +1,161 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2012, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import os
import sys
import rospkg
class Foo: pass
def test_RoslaunchDeps():
from roslaunch.depends import RoslaunchDeps
min_deps = RoslaunchDeps(nodes=[('rospy', 'talker.py')], pkgs=['rospy'])
assert min_deps == min_deps
assert min_deps == RoslaunchDeps(nodes=[('rospy', 'talker.py')], pkgs=['rospy'])
assert not min_deps.__eq__(Foo())
assert Foo() != min_deps
assert min_deps != RoslaunchDeps(nodes=[('rospy', 'talker.py')])
assert min_deps != RoslaunchDeps(pkgs=['rospy'])
assert 'talker.py' in repr(min_deps)
assert 'talker.py' in str(min_deps)
try:
from cStringIO import StringIO
except ImportError:
from io import StringIO
from contextlib import contextmanager
@contextmanager
def fakestdout():
realstdout = sys.stdout
fakestdout = StringIO()
sys.stdout = fakestdout
yield fakestdout
sys.stdout = realstdout
def test_roslaunch_deps_main():
from roslaunch.depends import roslaunch_deps_main
roslaunch_d = rospkg.RosPack().get_path('roslaunch')
rosmaster_d = rospkg.RosPack().get_path('rosmaster')
f = os.path.join(roslaunch_d, 'resources', 'example.launch')
rosmaster_f = os.path.join(rosmaster_d, 'test', 'rosmaster.test')
invalid_f = os.path.join(roslaunch_d, 'test', 'xml', 'invalid-xml.xml')
not_f = os.path.join(roslaunch_d, 'test', 'xml', 'not-launch.xml')
with fakestdout() as b:
roslaunch_deps_main(['roslaunch-deps', f])
s = b.getvalue()
assert s.strip() == 'rospy', "buffer value [%s]"%(s)
with fakestdout() as b:
roslaunch_deps_main(['roslaunch-deps', f, '--warn'])
s = b.getvalue()
assert s.strip() == """Dependencies:
rospy
Missing declarations:
roslaunch/manifest.xml:
<depend package="rospy" />""", s
# tripwire, don't really care about exact verbose output
with fakestdout() as b:
roslaunch_deps_main(['roslaunch-deps', f, '--verbose'])
# try with no file
with fakestdout() as b:
try:
roslaunch_deps_main(['roslaunch-deps'])
assert False, "should have failed"
except SystemExit:
pass
# try with non-existent file
with fakestdout() as b:
try:
roslaunch_deps_main(['roslaunch-deps', 'fakefile', '--verbose'])
assert False, "should have failed"
except SystemExit:
pass
# try with bad file
with fakestdout() as b:
try:
roslaunch_deps_main(['roslaunch-deps', invalid_f, '--verbose'])
assert False, "should have failed: %s"%b.getvalue()
except SystemExit:
pass
with fakestdout() as b:
try:
roslaunch_deps_main(['roslaunch-deps', not_f, '--verbose'])
assert False, "should have failed: %s"%b.getvalue()
except SystemExit:
pass
# try with files from different pacakges
with fakestdout() as b:
try:
roslaunch_deps_main(['roslaunch-deps', f, rosmaster_f, '--verbose'])
assert False, "should have failed"
except SystemExit:
pass
def test_roslaunch_deps():
from roslaunch.depends import roslaunch_deps, RoslaunchDeps
example_d = os.path.join(rospkg.RosPack().get_path('roslaunch'), 'resources')
min_deps = RoslaunchDeps(nodes=[('rospy', 'talker.py')], pkgs=['rospy'])
include_deps = RoslaunchDeps(nodes=[('rospy', 'talker.py'), ('rospy', 'listener.py')], pkgs=['rospy'])
example_deps = RoslaunchDeps(nodes=[('rospy', 'talker.py'), ('rospy', 'listener.py')], pkgs=['rospy'],
includes=[os.path.join(example_d, 'example-include.launch')])
example_file_deps = {
os.path.join(example_d, 'example.launch') : example_deps,
os.path.join(example_d, 'example-include.launch') : include_deps,
}
example_min_file_deps = {
os.path.join(example_d, 'example-min.launch') : min_deps,
}
r_missing = {'roslaunch': set(['rospy'])}
tests = [
([os.path.join(example_d, 'example-min.launch')], ('roslaunch', example_min_file_deps, r_missing)),
([os.path.join(example_d, 'example.launch')], ('roslaunch', example_file_deps, r_missing)),
]
for files, results in tests:
for v in [True, False]:
base_pkg, file_deps, missing = roslaunch_deps(files, verbose=v)
assert base_pkg == results[0]
assert file_deps == results[1], "\n%s vs \n%s"%(file_deps, results[1])
assert missing == results[2], "\n%s vs \n%s"%(missing, results[2])

View File

@@ -0,0 +1,108 @@
#!/usr/bin/env python
# Software License Agreement (BSD License)
#
# Copyright (c) 2010, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import os
import sys
import time
import unittest
import yaml
from subprocess import Popen, PIPE, check_call, call
class TestDumpParams(unittest.TestCase):
def setUp(self):
pass
def test_roslaunch(self):
# network is initialized
cmd = 'roslaunch'
# Smoke test for testing parameters
p = Popen([cmd, '--dump-params', 'roslaunch', 'noop.launch'], stdout = PIPE)
o, e = p.communicate()
self.assert_(p.returncode == 0, "Return code nonzero for param dump! Code: %d" % (p.returncode))
self.assertEquals({'/noop': 'noop'}, yaml.load(o))
p = Popen([cmd, '--dump-params', 'roslaunch', 'test-dump-rosparam.launch'], stdout = PIPE)
o, e = p.communicate()
self.assert_(p.returncode == 0, "Return code nonzero for param dump! Code: %d" % (p.returncode))
val = {
'/string1': 'bar',
'/dict1/head': 1,
'/dict1/shoulders': 2,
'/dict1/knees': 3,
'/dict1/toes': 4,
'/rosparam/string1': 'bar',
'/rosparam/dict1/head': 1,
'/rosparam/dict1/shoulders': 2,
'/rosparam/dict1/knees': 3,
'/rosparam/dict1/toes': 4,
'/node_rosparam/string1': 'bar',
'/node_rosparam/dict1/head': 1,
'/node_rosparam/dict1/shoulders': 2,
'/node_rosparam/dict1/knees': 3,
'/node_rosparam/dict1/toes': 4,
'/node_rosparam/tilde1': 'foo',
'/node_rosparam/local_param': 'baz',
'/node_rosparam2/tilde1': 'foo',
'/inline_str': 'value1',
'/inline_list': [1, 2, 3, 4],
'/inline_dict/key1': 'value1',
'/inline_dict/key2': 'value2',
'/inline_dict2/key3': 'value3',
'/inline_dict2/key4': 'value4',
'/override/key1': 'override1',
'/override/key2': 'value2',
'/noparam1': 'value1',
'/noparam2': 'value2',
}
output_val = yaml.load(o)
if not val == output_val:
for k, v in val.items():
if k not in output_val:
self.fail("key [%s] not in output: %s"%(k, output_val))
elif v != output_val[k]:
self.fail("key [%s] value [%s] does not match output: %s"%(k, v, output_val[k]))
self.assertEquals(val, output_val)
for k in ('/node_rosparam/tilde2', '/node_rosparam2/tilde2', '/node_rosparam2/local_param'):
if k in output_val:
self.fail("key [%s] should not be in output: %s"%(k, output_val))

View File

@@ -0,0 +1,179 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import os
import sys
import unittest
## Test roslaunch.launch
class TestRoslaunchLaunch(unittest.TestCase):
def setUp(self):
self.printerrlog_msg = None
def my_printerrlog(self, msg):
self.printerrlog_msg = msg
def test_validate_master_launch(self):
import roslaunch.launch
from roslaunch.core import Master
from roslaunch.launch import validate_master_launch
roslaunch.launch.printerrlog = self.my_printerrlog
# Good configurations
os.environ['ROS_MASTER_URI'] = 'http://localhost:11311'
m = Master(uri='http://localhost:11311')
validate_master_launch(m, True)
self.assertEquals(None, self.printerrlog_msg)
validate_master_launch(m, False)
self.assertEquals(None, self.printerrlog_msg)
# roscore with mismatched port in environment
os.environ['ROS_MASTER_URI'] = 'http://localhost:11312'
validate_master_launch(m, True)
self.assert_('port' in self.printerrlog_msg)
self.printerrlog_msg = None
# roscore with mismatched hostname in environment
os.environ['ROS_MASTER_URI'] = 'http://fake:11311'
validate_master_launch(m, True)
self.assert_('host' in self.printerrlog_msg)
self.printerrlog_msg = None
# roslaunch with remote master that cannot be contacted
os.environ['ROS_MASTER_URI'] = 'http://fake:11311'
self.assertEquals(None, self.printerrlog_msg)
# environment doesn't matter for remaining tests
os.environ['ROS_MASTER_URI'] = 'http://localhost:11311'
m = Master(uri="http://fake:11311")
# roscore with hostname that points elsewhere, warn user. This
# generally could only happen if the user has a bad local host
# config.
validate_master_launch(m, True)
self.assert_("WARNING" in self.printerrlog_msg)
self.printerrlog_msg = None
# roscore with host that is not ours
m = Master(uri="http://willowgarage.com:11311")
validate_master_launch(m, True)
self.assert_("WARNING" in self.printerrlog_msg)
self.printerrlog_msg = None
# roslaunch with remote master that is out of contact, fail
try:
validate_master_launch(m, False)
self.fail("should not pass if remote master cannot be contacted")
except roslaunch.RLException:
pass
def test__unify_clear_params(self):
from roslaunch.launch import _unify_clear_params
self.assertEquals([], _unify_clear_params([]))
for t in [['/foo'], ['/foo/'], ['/foo/', '/foo'],
['/foo/', '/foo/'], ['/foo/', '/foo/bar', '/foo/'],
['/foo/', '/foo/bar', '/foo/bar/baz']]:
self.assertEquals(['/foo/'], _unify_clear_params(t))
for t in [['/'], ['/', '/foo/'], ['/foo/', '/', '/baz', '/car/dog']]:
self.assertEquals(['/'], _unify_clear_params(t))
self.assertEquals(['/foo/', '/bar/', '/baz/'], _unify_clear_params(['/foo', '/bar', '/baz']))
self.assertEquals(['/foo/', '/bar/', '/baz/'], _unify_clear_params(['/foo', '/bar', '/baz', '/bar/delta', '/baz/foo']))
self.assertEquals(['/foo/bar/'], _unify_clear_params(['/foo/bar', '/foo/bar/baz']))
def test__hostname_to_rosname(self):
from roslaunch.launch import _hostname_to_rosname
self.assertEquals("host_ann", _hostname_to_rosname('ann'))
self.assertEquals("host_ann", _hostname_to_rosname('ANN'))
self.assertEquals("host_", _hostname_to_rosname(''))
self.assertEquals("host_1", _hostname_to_rosname('1'))
self.assertEquals("host__", _hostname_to_rosname('_'))
self.assertEquals("host__", _hostname_to_rosname('-'))
self.assertEquals("host_foo_laptop", _hostname_to_rosname('foo-laptop'))
def test_roslaunchListeners(self):
import roslaunch.launch
class L(roslaunch.launch.ROSLaunchListener):
def process_died(self, process_name, exit_code):
self.process_name = process_name
self.exit_code = exit_code
class LBad(roslaunch.launch.ROSLaunchListener):
def process_died(self, process_name, exit_code):
raise Exception("foo")
listeners = roslaunch.launch._ROSLaunchListeners()
l1 = L()
l2 = L()
lbad = L()
l3 = L()
# test with no listeners
listeners.process_died('p0', 0)
# test with 1 listener
listeners.add_process_listener(l1)
listeners.process_died('p1', 1)
self.assertEquals(l1.process_name, 'p1')
self.assertEquals(l1.exit_code, 1)
# test with 2 listeners
listeners.add_process_listener(l2)
listeners.process_died('p2', 2)
for l in [l1, l2]:
self.assertEquals(l.process_name, 'p2')
self.assertEquals(l.exit_code, 2)
listeners.add_process_listener(lbad)
# make sure that this catches errors
listeners.process_died('p3', 3)
for l in [l1, l2]:
self.assertEquals(l.process_name, 'p3')
self.assertEquals(l.exit_code, 3)
# also add a third listener to make sure that listeners continues after lbad throws
listeners.add_process_listener(l3)
listeners.process_died('p4', 4)
for l in [l1, l2, l3]:
self.assertEquals(l.process_name, 'p4')
self.assertEquals(l.exit_code, 4)
# this is just to get coverage, it's an empty class
def test_ROSRemoteRunnerIF():
from roslaunch.launch import ROSRemoteRunnerIF
r = ROSRemoteRunnerIF()
r.setup()
r.add_process_listener(1)
r.launch_remote_nodes()
def test_ROSLaunchListener():
from roslaunch.launch import ROSLaunchListener
r = ROSLaunchListener()
r.process_died(1, 2)

View File

@@ -0,0 +1,76 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2011, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import print_function
import os
import sys
import time
import unittest
import roslib.packages
from subprocess import Popen, PIPE, check_call, call
def get_test_path():
return os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'xml'))
class TestListFiles(unittest.TestCase):
def setUp(self):
pass
def test_list_files(self):
cmd = 'roslaunch'
# check error behavior
p = Popen([cmd, '--files'], stdout = PIPE)
p.communicate()
self.assert_(p.returncode != 0, "Should have failed w/o file argument. Code: %d" % (p.returncode))
d = get_test_path()
p = Popen([cmd, '--files', 'roslaunch', 'test-valid.xml'], stdout = PIPE)
o = p.communicate()[0]
o = o.decode()
self.assert_(p.returncode == 0, "Return code nonzero for list files! Code: %d" % (p.returncode))
self.assertEquals(os.path.realpath(os.path.join(d, 'test-valid.xml')), os.path.realpath(o.strip()))
print("check 1", o)
p = Popen([cmd, '--files', 'roslaunch', 'test-env.xml'], stdout = PIPE)
o = p.communicate()[0]
o = o.decode()
self.assert_(p.returncode == 0, "Return code nonzero for list files! Code: %d" % (p.returncode))
self.assertEquals(set([os.path.realpath(os.path.join(d, 'test-env.xml')), os.path.realpath(os.path.join(d, 'test-env-include.xml'))]),
set([os.path.realpath(x.strip()) for x in o.split() if x.strip()]))
print("check 2", o)

View File

@@ -0,0 +1,122 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2012, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import os
import sys
try:
from cStringIO import StringIO
except ImportError:
from io import StringIO
from contextlib import contextmanager
@contextmanager
def fakestdout():
realstdout = sys.stdout
fakestdout = StringIO()
sys.stdout = fakestdout
yield fakestdout
sys.stdout = realstdout
import rospkg
import logging
SAMPLE1 = """/rosparam_load/dict1/head: 1
/rosparam_load/dict1/knees: 3
/rosparam_load/dict1/shoulders: 2
/rosparam_load/dict1/toes: 4
/rosparam_load/integer1: 1
/rosparam_load/integer2: 2
/rosparam_load/list1: [head, shoulders, knees, toes]
/rosparam_load/list2: [1, 1, 2, 3, 5, 8]
/rosparam_load/preformattedtext: 'This is the first line
This is the second line
Line breaks are preserved
Indentation is stripped
'
/rosparam_load/robots/child/grandchildparam: a grandchild namespace param
/rosparam_load/robots/childparam: a child namespace parameter
/rosparam_load/string1: bar
/rosparam_load/string2: '10'"""
SAMPLE2 = """/load_ns/subns/dict1/head: 1
/load_ns/subns/dict1/knees: 3
/load_ns/subns/dict1/shoulders: 2
/load_ns/subns/dict1/toes: 4
/load_ns/subns/integer1: 1
/load_ns/subns/integer2: 2
/load_ns/subns/list1: [head, shoulders, knees, toes]
/load_ns/subns/list2: [1, 1, 2, 3, 5, 8]
/load_ns/subns/preformattedtext: 'This is the first line
This is the second line
Line breaks are preserved
Indentation is stripped
'
/load_ns/subns/robots/child/grandchildparam: a grandchild namespace param
/load_ns/subns/robots/childparam: a child namespace parameter
/load_ns/subns/string1: bar
/load_ns/subns/string2: '10'"""
def test_dump_params():
# normal entrypoint has logging configured
logger = logging.getLogger('roslaunch').setLevel(logging.CRITICAL)
from roslaunch.param_dump import dump_params
roslaunch_d = rospkg.RosPack().get_path('roslaunch')
test_d = os.path.join(roslaunch_d, 'test', 'xml')
node_rosparam_f = os.path.join(test_d, 'test-node-rosparam-load.xml')
with fakestdout() as b:
assert dump_params([node_rosparam_f])
s = b.getvalue().strip()
# remove float vals as serialization is not stable
s = '\n'.join([x for x in s.split('\n') if not 'float' in x])
assert str(s) == str(SAMPLE1), "[%s]\nvs\n[%s]"%(s, SAMPLE1)
node_rosparam_f = os.path.join(test_d, 'test-node-rosparam-load-ns.xml')
with fakestdout() as b:
assert dump_params([node_rosparam_f])
s = b.getvalue().strip()
# remove float vals as serialization is not stable
s = '\n'.join([x for x in s.split('\n') if not 'float' in x])
assert str(s) == str(SAMPLE2), "[%s]\nvs\n[%s]"%(s, SAMPLE2)
invalid_f = os.path.join(test_d, 'invalid-xml.xml')
with fakestdout() as b:
assert not dump_params([invalid_f])

View File

@@ -0,0 +1,244 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import os
import sys
import unittest
import time
import rospkg
import rosgraph.network
import roslaunch.parent
import rosgraph
master = rosgraph.Master('test_roslaunch_parent')
def get_param(*args):
return master.getParam(*args)
## Fake Process object
class ProcessMock(roslaunch.pmon.Process):
def __init__(self, package, name, args, env, respawn=False):
super(ProcessMock, self).__init__(package, name, args, env, respawn)
self.stopped = False
def stop(self):
self.stopped = True
## Fake ProcessMonitor object
class ProcessMonitorMock(object):
def __init__(self):
self.core_procs = []
self.procs = []
self.listeners = []
self.is_shutdown = False
def join(self, timeout=0):
pass
def add_process_listener(self, l):
self.listeners.append(l)
def register(self, p):
self.procs.append(p)
def register_core_proc(self, p):
self.core_procs.append(p)
def registrations_complete(self):
pass
def unregister(self, p):
self.procs.remove(p)
def has_process(self, name):
return len([p for p in self.procs if p.name == name]) > 0
def get_process(self, name):
val = [p for p in self.procs if p.name == name]
if val:
return val[0]
return None
def has_main_thread_jobs(self):
return False
def do_main_thread_jobs(self):
pass
def kill_process(self, name):
pass
def shutdown(self):
self.is_shutdown = True
def get_active_names(self):
return [p.name for p in self.procs]
def get_process_names_with_spawn_count(self):
actives = [(p.name, p.spawn_count) for p in self.procs]
deads = []
return [actives, deads]
def mainthread_spin_once(self):
pass
def mainthread_spin(self):
pass
def run(self):
pass
## Test roslaunch.server
class TestRoslaunchParent(unittest.TestCase):
def setUp(self):
self.pmon = ProcessMonitorMock()
def test_roslaunchParent(self):
try:
self._subroslaunchParent()
finally:
self.pmon.shutdown()
def _subroslaunchParent(self):
from roslaunch.parent import ROSLaunchParent
pmon = self.pmon
try:
# if there is a core up, we have to use its run id
run_id = get_param('/run_id')
except:
run_id = 'test-rl-parent-%s'%time.time()
name = 'foo-bob'
server_uri = 'http://localhost:12345'
p = ROSLaunchParent(run_id, [], is_core = True, port=None, local_only=False)
self.assertEquals(run_id, p.run_id)
self.assertEquals(True, p.is_core)
self.assertEquals(False, p.local_only)
rl_dir = rospkg.RosPack().get_path('roslaunch')
rl_file = os.path.join(rl_dir, 'resources', 'example.launch')
self.assert_(os.path.isfile(rl_file))
# validate load_config logic
p = ROSLaunchParent(run_id, [rl_file], is_core = False, port=None, local_only=True)
self.assertEquals(run_id, p.run_id)
self.assertEquals(False, p.is_core)
self.assertEquals(True, p.local_only)
self.assert_(p.config is None)
p._load_config()
self.assert_(p.config is not None)
self.assert_(p.config.nodes)
# try again with port override
p = ROSLaunchParent(run_id, [rl_file], is_core = False, port=11312, local_only=True)
self.assertEquals(11312, p.port)
self.assert_(p.config is None)
p._load_config()
# - make sure port got passed into master
_, port = rosgraph.network.parse_http_host_and_port(p.config.master.uri)
self.assertEquals(11312, port)
# try again with bad file
p = ROSLaunchParent(run_id, ['non-existent-fake.launch'])
self.assert_(p.config is None)
try:
p._load_config()
self.fail("load config should have failed due to bad rl file")
except roslaunch.core.RLException: pass
# try again with bad xml
rl_dir = rospkg.RosPack().get_path('roslaunch')
rl_file = os.path.join(rl_dir, 'test', 'xml', 'test-params-invalid-1.xml')
self.assert_(os.path.isfile(rl_file))
p = ROSLaunchParent(run_id, [rl_file])
self.assert_(p.config is None)
try:
p._load_config()
self.fail("load config should have failed due to bad rl file")
except roslaunch.core.RLException: pass
# Mess around with internal repr to get code coverage on _init_runner/_init_remote
p = ROSLaunchParent(run_id, [], is_core = False, port=None, local_only=True)
# no config, _init_runner/_init_remote/_start_server should fail
for m in ['_init_runner', '_init_remote', '_start_server']:
try:
getattr(p, m)()
self.fail('should have raised')
except roslaunch.core.RLException: pass
# - initialize p.config
p.config = roslaunch.config.ROSLaunchConfig()
# no pm, _init_runner/_init_remote/_start_server should fail
for m in ['_init_runner', '_init_remote', '_start_server']:
try:
getattr(p, m)()
self.fail('should have raised')
except roslaunch.core.RLException: pass
# - initialize p.pm
p.pm = pmon
for m in ['_init_runner', '_init_remote']:
try:
getattr(p, m)()
self.fail('should have raised')
except roslaunch.core.RLException: pass
from roslaunch.server import ROSLaunchParentNode
p.server = ROSLaunchParentNode(p.config, pmon)
p._init_runner()
# roslaunch runner should be initialized
self.assert_(p.runner is not None)
# test _init_remote
p.local_only = True
p._init_remote()
p.local_only = False
# - this violates many abstractions to do this
def ftrue():
return True
p.config.has_remote_nodes = ftrue
p._init_remote()
self.assert_(p.remote_runner is not None)
self.failIf(pmon.is_shutdown)
p.shutdown()
self.assert_(pmon.is_shutdown)
def kill_parent(p, delay=1.0):
# delay execution so that whatever pmon method we're calling has time to enter
time.sleep(delay)
print("stopping parent")
p.shutdown()

View File

@@ -0,0 +1,580 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import os
import sys
import unittest
import time
import threading
import roslaunch.server
## handy class for kill_pmon to check whether it was called
class Marker(object):
def __init__(self):
self.marked = False
def mark(self):
self.marked = True
## Fake ProcessMonitor object
class ProcessMonitorMock(object):
def __init__(self):
self.core_procs = []
self.procs = []
self.listeners = []
self.alive = False
self.is_shutdown = False
def isAlive(self):
return self.alive
def join(self, *args):
return
def add_process_listener(self, l):
self.listeners.append(l)
def register(self, p):
self.procs.append(p)
def register_core_proc(self, p):
self.core_procs.append(p)
def registrations_complete(self):
pass
def unregister(self, p):
self.procs.remove(p)
def has_process(self, name):
return len([p for p in self.procs if p.name == name]) > 0
def get_process(self, name):
return self.procs.get(p, None)
def has_main_thread_jobs(self):
return False
def do_main_thread_jobs(self):
pass
def kill_process(self, name):
pass
def shutdown(self):
pass
def get_active_names(self):
return [p.name for p in self.procs]
def get_process_names_with_spawn_count(self):
actives = [(p.name, p.spawn_count) for p in self.procs]
deads = []
retval = [actives, deads]
def mainthread_spin_once(self):
pass
def mainthread_spin(self):
pass
def run(self):
pass
class ProcessMock(roslaunch.pmon.Process):
def __init__(self, package, name, args, env, respawn=False):
super(ProcessMock, self).__init__(package, name, args, env, respawn)
self.stopped = False
def stop(self, errors):
self.stopped = True
class RespawnOnceProcessMock(ProcessMock):
def __init__(self, package, name, args, env, respawn=True):
super(ProcessMock, self).__init__(package, name, args, env, respawn)
self.spawn_count = 0
def is_alive(self):
self.time_of_death = time.time()
return False
def start(self):
self.spawn_count += 1
if self.spawn_count > 1:
self.respawn = False
self.time_of_death = None
class RespawnOnceWithDelayProcessMock(ProcessMock):
def __init__(self, package, name, args, env, respawn=True,
respawn_delay=1.0):
super(ProcessMock, self).__init__(package, name, args, env, respawn,
respawn_delay=respawn_delay)
self.spawn_count = 0
self.respawn_interval = None
def is_alive(self):
if self.time_of_death is None:
self.time_of_death = time.time()
return False
def start(self):
self.spawn_count += 1
if self.spawn_count > 1:
self.respawn = False
self.respawn_interval = time.time() - self.time_of_death
self.time_of_death = None
## Test roslaunch.server
class TestRoslaunchPmon(unittest.TestCase):
def setUp(self):
self.pmon = roslaunch.pmon.ProcessMonitor()
## test all apis of Process instance. part coverage/sanity test
def _test_Process(self, p, package, name, args, env, respawn, respawn_delay):
self.assertEquals(package, p.package)
self.assertEquals(name, p.name)
self.assertEquals(args, p.args)
self.assertEquals(env, p.env)
self.assertEquals(respawn, p.respawn)
self.assertEquals(respawn_delay, p.respawn_delay)
self.assertEquals(0, p.spawn_count)
self.assertEquals(None, p.exit_code)
self.assert_(p.get_exit_description())
self.failIf(p.is_alive())
info = p.get_info()
self.assertEquals(package, info['package'])
self.assertEquals(name, info['name'])
self.assertEquals(args, info['args'])
self.assertEquals(env, info['env'])
self.assertEquals(respawn, info['respawn'])
self.assertEquals(respawn_delay, info['respawn_delay'])
self.assertEquals(0, info['spawn_count'])
self.failIf('exit_code' in info)
p.start()
self.assertEquals(1, p.spawn_count)
self.assertEquals(1, p.get_info()['spawn_count'])
p.start()
self.assertEquals(2, p.spawn_count)
self.assertEquals(2, p.get_info()['spawn_count'])
# noop
p.stop()
p.exit_code = 0
self.assertEquals(0, p.get_info()['exit_code'])
self.assert_('cleanly' in p.get_exit_description())
p.exit_code = 1
self.assertEquals(1, p.get_info()['exit_code'])
self.assert_('[exit code 1]' in p.get_exit_description())
## tests to make sure that our Process base class has 100% coverage
def test_Process(self):
from roslaunch.pmon import Process
# test constructor params
respawn = False
package = 'foo-%s'%time.time()
name = 'name-%s'%time.time()
args = [time.time(), time.time(), time.time()]
env = { 'key': time.time(), 'key2': time.time() }
p = Process(package, name, args, env, 0.0)
self._test_Process(p, package, name, args, env, False, 0.0)
p = Process(package, name, args, env, True, 0.0)
self._test_Process(p, package, name, args, env, True, 0.0)
p = Process(package, name, args, env, False, 0.0)
self._test_Process(p, package, name, args, env, False, 0.0)
p = Process(package, name, args, env, True, 1.0)
self._test_Process(p, package, name, args, env, True, 1.0)
def _test_DeadProcess(self, p0, package, name, args, env, respawn,
respawn_delay):
from roslaunch.pmon import DeadProcess
p0.exit_code = -1
dp = DeadProcess(p0)
self.assertEquals(package, dp.package)
self.assertEquals(name, dp.name)
self.assertEquals(args, dp.args)
self.assertEquals(env, dp.env)
self.assertEquals(respawn, dp.respawn)
self.assertEquals(respawn_delay, dp.respawn_delay)
self.assertEquals(0, dp.spawn_count)
self.assertEquals(-1, dp.exit_code)
self.failIf(dp.is_alive())
info = dp.get_info()
info0 = p0.get_info()
self.assertEquals(info0['package'], info['package'])
self.assertEquals(info0['name'], info['name'])
self.assertEquals(info0['args'], info['args'])
self.assertEquals(info0['env'], info['env'])
self.assertEquals(info0['respawn'], info['respawn'])
self.assertEquals(info0['respawn_delay'], info['respawn_delay'])
self.assertEquals(0, info['spawn_count'])
try:
dp.start()
self.fail("should not be able to start a dead process")
except: pass
# info should be frozen
p0.package = 'dead package'
p0.name = 'dead name'
p0.spawn_count = 1
self.assertEquals(package, dp.package)
self.assertEquals(name, dp.name)
self.assertEquals(0, dp.spawn_count)
self.assertEquals(package, dp.get_info()['package'])
self.assertEquals(name, dp.get_info()['name'])
self.assertEquals(0, dp.get_info()['spawn_count'])
p0.start()
self.assertEquals(0, dp.spawn_count)
self.assertEquals(0, dp.get_info()['spawn_count'])
# noop
p0.stop()
def test_DeadProcess(self):
from roslaunch.pmon import Process, DeadProcess
# test constructor params
respawn = False
package = 'foo-%s'%time.time()
name = 'name-%s'%time.time()
args = [time.time(), time.time(), time.time()]
env = { 'key': time.time(), 'key2': time.time() }
p = Process(package, name, args, env, 0.0)
self._test_DeadProcess(p, package, name, args, env, False, 0.0)
p = Process(package, name, args, env, True, 0.0)
self._test_DeadProcess(p, package, name, args, env, True, 0.0)
p = Process(package, name, args, env, False, 0.0)
self._test_DeadProcess(p, package, name, args, env, False, 0.0)
p = Process(package, name, args, env, True, 1.0)
self._test_DeadProcess(p, package, name, args, env, True, 1.0)
def test_start_shutdown_process_monitor(self):
def failer():
raise Exception("oops")
# noop
self.failIf(roslaunch.pmon.shutdown_process_monitor(None))
# test with fake pmon so we can get branch-complete
pmon = ProcessMonitorMock()
# - by setting alive to true, shutdown fails, though it can't really do anything about it
pmon.alive = True
self.failIf(roslaunch.pmon.shutdown_process_monitor(pmon))
# make sure that exceptions get trapped
pmon.shutdown = failer
# should cause an exception during execution, but should get caught
self.failIf(roslaunch.pmon.shutdown_process_monitor(pmon))
# Test with a real process monitor
pmon = roslaunch.pmon.start_process_monitor()
self.assert_(pmon.isAlive())
self.assert_(roslaunch.pmon.shutdown_process_monitor(pmon))
self.failIf(pmon.isAlive())
# fiddle around with some state that would shouldn't be
roslaunch.pmon._shutting_down = True
pmon = roslaunch.pmon.start_process_monitor()
if pmon != None:
self.failIf(roslaunch.pmon.shutdown_process_monitor(pmon))
self.fail("start_process_monitor should fail if during shutdown sequence")
def test_pmon_shutdown(self):
# should be noop
roslaunch.pmon.pmon_shutdown()
# start two real process monitors and kill them
# pmon_shutdown
pmon1 = roslaunch.pmon.start_process_monitor()
pmon2 = roslaunch.pmon.start_process_monitor()
self.assert_(pmon1.isAlive())
self.assert_(pmon2.isAlive())
roslaunch.pmon.pmon_shutdown()
self.failIf(pmon1.isAlive())
self.failIf(pmon2.isAlive())
def test_add_process_listener(self):
# coverage test, not a functionality test as that would be much more difficult to simulate
from roslaunch.pmon import ProcessListener
l = ProcessListener()
self.pmon.add_process_listener(l)
def test_kill_process(self):
from roslaunch.core import RLException
pmon = self.pmon
# should return False
self.failIf(pmon.kill_process('foo'))
p1 = ProcessMock('foo', 'name1', [], {})
p2 = ProcessMock('bar', 'name2', [], {})
pmon.register(p1)
pmon.register(p2)
self.failIf(p1.stopped)
self.failIf(p2.stopped)
self.assert_(p1.name in pmon.get_active_names())
self.assert_(p2.name in pmon.get_active_names())
# should fail as pmon API is string-based
try:
self.assert_(pmon.kill_process(p1))
self.fail("kill_process should have thrown RLException")
except RLException: pass
self.assert_(pmon.kill_process(p1.name))
self.assert_(p1.stopped)
# - pmon should not have removed yet as the pmon thread cannot catch the death
self.assert_(pmon.has_process(p1.name))
self.assert_(p1.name in pmon.get_active_names())
self.failIf(p2.stopped)
self.assert_(p2.name in pmon.get_active_names())
pmon.kill_process(p2.name)
self.assert_(p2.stopped)
# - pmon should not have removed yet as the pmon thread cannot catch the death
self.assert_(pmon.has_process(p2.name))
self.assert_(p2.name in pmon.get_active_names())
p3 = ProcessMock('bar', 'name3', [], {})
def bad(x):
raise Exception("ha ha ha")
p3.stop = bad
pmon.register(p3)
# make sure kill_process is safe
pmon.kill_process(p3.name)
def f():
return False
p1.is_alive = f
p2.is_alive = f
p3.is_alive = f
# Now that we've 'killed' all the processes, we should be able
# to run through the ProcessMonitor run loop and it should
# exit. But first, important that we check that pmon has no
# other extra state in it
self.assertEquals(3, len(pmon.get_active_names()))
# put pmon into exitable state
pmon.registrations_complete()
# and run it -- but setup a safety timer to kill it if it doesn't exit
marker = Marker()
t = threading.Thread(target=kill_pmon, args=(pmon, marker, 10.))
t.setDaemon(True)
t.start()
pmon.run()
self.failIf(marker.marked, "pmon had to be externally killed")
self.assert_(pmon.done)
self.failIf(pmon.has_process(p1.name))
self.failIf(pmon.has_process(p2.name))
alive, dead = pmon.get_process_names_with_spawn_count()
self.failIf(alive)
self.assert_((p1.name, p1.spawn_count) in dead)
self.assert_((p2.name, p2.spawn_count) in dead)
def test_run(self):
# run is really hard to test...
pmon = self.pmon
# put pmon into exitable state
pmon.registrations_complete()
# give the pmon a process that raises an exception when it's
# is_alive is checked. this should be marked as a dead process
p1 = ProcessMock('bar', 'name1', [], {})
def bad():
raise Exception('ha ha')
p1.is_alive = bad
pmon.register(p1)
# give pmon a well-behaved but dead process
p2 = ProcessMock('bar', 'name2', [], {})
def f():
return False
p2.is_alive = f
pmon.register(p2)
# give pmon a process that wants to respawn once
p3 = RespawnOnceProcessMock('bar', 'name3', [], {})
pmon.register(p3)
# give pmon a process that wants to respawn once after a delay
p4 = RespawnOnceWithDelayProcessMock('bar', 'name4', [], {})
pmon.register(p4)
# test assumptions about pmon's internal data structures
# before we begin test
self.assert_(p1 in pmon.procs)
self.assert_(pmon._registrations_complete)
self.failIf(pmon.is_shutdown)
# and run it -- but setup a safety timer to kill it if it doesn't exit
marker = Marker()
t = threading.Thread(target=kill_pmon, args=(self.pmon, marker, 10.))
t.setDaemon(True)
t.start()
pmon.run()
self.failIf(marker.marked, "pmon had to be externally killed")
self.failIf(p3.spawn_count < 2, "process did not respawn")
self.failIf(p4.respawn_interval < p4.respawn_delay,
"Respawn delay not respected: %s %s" % (p4.respawn_interval,
p4.respawn_delay))
# retest assumptions
self.failIf(pmon.procs)
self.assert_(pmon.is_shutdown)
pmon.is_shutdown = False
def test_get_process_names_with_spawn_count(self):
p1 = ProcessMock('foo', 'name1', [], {})
p2 = ProcessMock('bar', 'name2', [], {})
pmon = self.pmon
self.assertEquals([[], []], pmon.get_process_names_with_spawn_count())
pmon.register(p1)
self.assertEquals([[('name1', 0),], []], pmon.get_process_names_with_spawn_count())
pmon.register(p2)
alive, dead = pmon.get_process_names_with_spawn_count()
self.assertEquals([], dead)
self.assert_(('name1', 0) in alive)
self.assert_(('name2', 0) in alive)
import random
p1.spawn_count = random.randint(1, 10000)
p2.spawn_count = random.randint(1, 10000)
alive, dead = pmon.get_process_names_with_spawn_count()
self.assertEquals([], dead)
self.assert_((p1.name, p1.spawn_count) in alive)
self.assert_((p2.name, p2.spawn_count) in alive)
#TODO figure out how to test dead_list
## Tests ProcessMonitor.register(), unregister(), has_process(), and get_process()
def test_registration(self):
from roslaunch.core import RLException
from roslaunch.pmon import Process
pmon = self.pmon
p1 = Process('foo', 'name1', [], {})
p2 = Process('bar', 'name2', [], {})
corep1 = Process('core', 'core1', [], {})
corep2 = Process('core', 'core2', [], {})
pmon.register(p1)
self.assert_(pmon.has_process('name1'))
self.assertEquals(p1, pmon.get_process('name1'))
self.failIf(pmon.has_process('name2'))
self.assertEquals(['name1'], pmon.get_active_names())
try:
pmon.register(Process('foo', p1.name, [], {}))
self.fail("should not allow duplicate process name")
except RLException: pass
pmon.register(p2)
self.assert_(pmon.has_process('name2'))
self.assertEquals(p2, pmon.get_process('name2'))
self.assertEquals(set(['name1', 'name2']), set(pmon.get_active_names()))
pmon.register_core_proc(corep1)
self.assert_(pmon.has_process('core1'))
self.assertEquals(corep1, pmon.get_process('core1'))
self.assertEquals(set(['name1', 'name2', 'core1']), set(pmon.get_active_names()))
pmon.register_core_proc(corep2)
self.assert_(pmon.has_process('core2'))
self.assertEquals(corep2, pmon.get_process('core2'))
self.assertEquals(set(['name1', 'name2', 'core1', 'core2']), set(pmon.get_active_names()))
pmon.unregister(p2)
self.failIf(pmon.has_process('name2'))
pmon.unregister(p1)
self.failIf(pmon.has_process('name1'))
pmon.unregister(corep1)
self.failIf(pmon.has_process('core1'))
pmon.unregister(corep2)
self.failIf(pmon.has_process('core2'))
pmon.shutdown()
try:
pmon.register(Process('shutdown_fail', 'shutdown_fail', [], {}))
self.fail("registration should fail post-shutdown")
except RLException: pass
def test_mainthread_spin_once(self):
# shouldn't do anything
self.pmon.done = False
self.pmon.mainthread_spin_once()
self.pmon.done = True
self.pmon.mainthread_spin_once()
def test_mainthread_spin(self):
# can't test actual spin as that would go forever
self.pmon.done = False
t = threading.Thread(target=kill_pmon, args=(self.pmon, Marker()))
t.setDaemon(True)
t.start()
self.pmon.mainthread_spin()
def kill_pmon(pmon, marker, delay=1.0):
# delay execution so that whatever pmon method we're calling has time to enter
time.sleep(delay)
if not pmon.is_shutdown:
marker.mark()
print("stopping pmon")
# pmon has two states that need to be set, as many of the tests do not start the actual process monitor
pmon.shutdown()
pmon.done = True

View File

@@ -0,0 +1,146 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import os
import sys
import unittest
import roslaunch.core
import roslaunch.remote
## Unit tests for roslaunch.remote
class TestRoslaunchRemote(unittest.TestCase):
def test_remote_node_xml(self):
# these are fairly brittle tests, but need to make sure there aren't regressions here
Node = roslaunch.core.Node
n = Node('pkg1', 'type1')
self.assertEquals('<node pkg="pkg1" type="type1" ns="/" args="" respawn="False" respawn_delay="0.0" required="False">\n</node>', n.to_remote_xml())
n = Node('pkg2', 'type2', namespace="/ns2/")
self.assertEquals('<node pkg="pkg2" type="type2" ns="/ns2/" args="" respawn="False" respawn_delay="0.0" required="False">\n</node>', n.to_remote_xml())
# machine_name should be a noop for remote xml
n = Node('pkg3', 'type3', namespace="/ns3/", machine_name="machine3")
self.assertEquals('<node pkg="pkg3" type="type3" ns="/ns3/" args="" respawn="False" respawn_delay="0.0" required="False">\n</node>', n.to_remote_xml())
# test args
n = Node('pkg4', 'type4', args="arg4a arg4b")
self.assertEquals('<node pkg="pkg4" type="type4" ns="/" args="arg4a arg4b" respawn="False" respawn_delay="0.0" required="False">\n</node>', n.to_remote_xml())
# test respawn
n = Node('pkg5', 'type5', respawn=True)
self.assertEquals('<node pkg="pkg5" type="type5" ns="/" args="" respawn="True" respawn_delay="0.0" required="False">\n</node>', n.to_remote_xml())
n = Node('pkg5', 'type5', respawn=True, respawn_delay=1.0)
self.assertEquals('<node pkg="pkg5" type="type5" ns="/" args="" respawn="True" respawn_delay="1.0" required="False">\n</node>', n.to_remote_xml())
n = Node('pkg6', 'type6', respawn=False)
self.assertEquals('<node pkg="pkg6" type="type6" ns="/" args="" respawn="False" respawn_delay="0.0" required="False">\n</node>', n.to_remote_xml())
# test remap_args
n = Node('pkg6', 'type6', remap_args=[('from6a', 'to6a'), ('from6b', 'to6b')])
self.assertEquals("""<node pkg="pkg6" type="type6" ns="/" args="" respawn="False" respawn_delay="0.0" required="False">
<remap from="from6a" to="to6a" />
<remap from="from6b" to="to6b" />
</node>""", n.to_remote_xml())
# test env args
n = Node('pkg7', 'type7', env_args=[('key7a', 'val7a'), ('key7b', 'val7b')])
self.assertEquals("""<node pkg="pkg7" type="type7" ns="/" args="" respawn="False" respawn_delay="0.0" required="False">
<env name="key7a" value="val7a" />
<env name="key7b" value="val7b" />
</node>""", n.to_remote_xml())
# test cwd
n = Node('pkg8', 'type8', cwd='ROS_HOME')
self.assertEquals('<node pkg="pkg8" type="type8" ns="/" args="" cwd="ROS_HOME" respawn="False" respawn_delay="0.0" required="False">\n</node>', n.to_remote_xml())
n = Node('pkg9', 'type9', cwd='node')
self.assertEquals('<node pkg="pkg9" type="type9" ns="/" args="" cwd="node" respawn="False" respawn_delay="0.0" required="False">\n</node>', n.to_remote_xml())
# test output
n = Node('pkg10', 'type10', output='screen')
self.assertEquals('<node pkg="pkg10" type="type10" ns="/" args="" output="screen" respawn="False" respawn_delay="0.0" required="False">\n</node>', n.to_remote_xml())
n = Node('pkg11', 'type11', output='log')
self.assertEquals('<node pkg="pkg11" type="type11" ns="/" args="" output="log" respawn="False" respawn_delay="0.0" required="False">\n</node>', n.to_remote_xml())
# test launch-prefix
n = Node('pkg12', 'type12', launch_prefix='xterm -e')
self.assertEquals('<node pkg="pkg12" type="type12" ns="/" args="" respawn="False" respawn_delay="0.0" launch-prefix="xterm -e" required="False">\n</node>', n.to_remote_xml())
# test required
n = Node('pkg13', 'type13', required=True)
self.assertEquals('<node pkg="pkg13" type="type13" ns="/" args="" respawn="False" respawn_delay="0.0" required="True">\n</node>', n.to_remote_xml())
#test everything
n = Node('pkg20', 'type20', namespace="/ns20/", machine_name="foo", remap_args=[('from20a', 'to20a'), ('from20b', 'to20b')], env_args=[('key20a', 'val20a'), ('key20b', 'val20b')], output="screen", cwd="ROS_HOME", respawn=True, args="arg20a arg20b", launch_prefix="nice", required=False)
self.assertEquals("""<node pkg="pkg20" type="type20" ns="/ns20/" args="arg20a arg20b" output="screen" cwd="ROS_HOME" respawn="True" respawn_delay="0.0" launch-prefix="nice" required="False">
<remap from="from20a" to="to20a" />
<remap from="from20b" to="to20b" />
<env name="key20a" value="val20a" />
<env name="key20b" value="val20b" />
</node>""", n.to_remote_xml())
def test_remote_test_node_xml(self):
Test = roslaunch.core.Test
#TODO: will certainly fail right now
# these are fairly brittle tests, but need to make sure there aren't regressions here
Test = roslaunch.core.Test
n = Test('test1', 'pkg1', 'type1')
self.assertEquals('<test pkg="pkg1" type="type1" ns="/" args="" output="log" required="False" test-name="test1">\n</test>', n.to_remote_xml())
n = Test('test2', 'pkg2', 'type2', namespace="/ns2/")
self.assertEquals('<test pkg="pkg2" type="type2" ns="/ns2/" args="" output="log" required="False" test-name="test2">\n</test>', n.to_remote_xml())
# machine_name should be a noop for remote xml
n = Test('test3', 'pkg3', 'type3', namespace="/ns3/", machine_name="machine3")
self.assertEquals('<test pkg="pkg3" type="type3" ns="/ns3/" args="" output="log" required="False" test-name="test3">\n</test>', n.to_remote_xml())
# test args
n = Test('test4', 'pkg4', 'type4', args="arg4a arg4b")
self.assertEquals('<test pkg="pkg4" type="type4" ns="/" args="arg4a arg4b" output="log" required="False" test-name="test4">\n</test>', n.to_remote_xml())
# test remap_args
n = Test('test6', 'pkg6', 'type6', remap_args=[('from6a', 'to6a'), ('from6b', 'to6b')])
self.assertEquals("""<test pkg="pkg6" type="type6" ns="/" args="" output="log" required="False" test-name="test6">
<remap from="from6a" to="to6a" />
<remap from="from6b" to="to6b" />
</test>""", n.to_remote_xml())
# test env args
n = Test('test7', 'pkg7', 'type7', env_args=[('key7a', 'val7a'), ('key7b', 'val7b')])
self.assertEquals("""<test pkg="pkg7" type="type7" ns="/" args="" output="log" required="False" test-name="test7">
<env name="key7a" value="val7a" />
<env name="key7b" value="val7b" />
</test>""", n.to_remote_xml())
# test cwd
n = Test('test8', 'pkg8', 'type8', cwd='ROS_HOME')
self.assertEquals('<test pkg="pkg8" type="type8" ns="/" args="" output="log" cwd="ROS_HOME" required="False" test-name="test8">\n</test>', n.to_remote_xml())
n = Test('test9', 'pkg9', 'type9', cwd='node')
self.assertEquals('<test pkg="pkg9" type="type9" ns="/" args="" output="log" cwd="node" required="False" test-name="test9">\n</test>', n.to_remote_xml())
#test everything
n = Test('test20', 'pkg20', 'type20', namespace="/ns20/", machine_name="foo", remap_args=[('from20a', 'to20a'), ('from20b', 'to20b')], env_args=[('key20a', 'val20a'), ('key20b', 'val20b')], cwd="ROS_HOME", args="arg20a arg20b")
self.assertEquals("""<test pkg="pkg20" type="type20" ns="/ns20/" args="arg20a arg20b" output="log" cwd="ROS_HOME" required="False" test-name="test20">
<remap from="from20a" to="to20a" />
<remap from="from20b" to="to20b" />
<env name="key20a" value="val20a" />
<env name="key20b" value="val20b" />
</test>""", n.to_remote_xml())

View File

@@ -0,0 +1,91 @@
#!/usr/bin/env python
# Software License Agreement (BSD License)
#
# Copyright (c) 2009, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Revision $Id: roslaunch_node_args.py 5229 2009-07-16 22:31:17Z sfkwc $
import os
import sys
import unittest
import roslaunch
import roslaunch.rlutil
def get_test_path():
# two directories up from here
return os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
# path to example.launch directory
def get_example_path():
return os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources'))
## Test roslaunch.node_args
class TestRoslaunchRlutil(unittest.TestCase):
def test_resolve_launch_arguments(self):
from roslaunch.rlutil import resolve_launch_arguments
roslaunch_dir = get_test_path()
example_xml_p = os.path.join(get_example_path(), 'example.launch')
tests = [
([], []),
(['roslaunch', 'example.launch'], [example_xml_p]),
([example_xml_p], [example_xml_p]),
(['roslaunch', 'example.launch', 'foo', 'bar'], [example_xml_p, 'foo', 'bar']),
([example_xml_p, 'foo', 'bar'], [example_xml_p,'foo', 'bar']),
]
bad = [
['does_not_exist'],
['does_not_exist', 'foo.launch'],
['roslaunch', 'does_not_exist.launch'],
]
for test, result in tests:
for v1, v2 in zip(result, resolve_launch_arguments(test)):
# workaround for nfs
if os.path.exists(v1):
self.assert_(os.path.samefile(v1, v2))
else:
self.assertEquals(v1, v2)
for test in bad:
try:
resolve_launch_arguments(test)
self.fail("should have failed")
except roslaunch.RLException:
pass
def test_roslaunch_check_pass_all_args(self):
filename = os.path.join(get_example_path(), 'example-pass_all_args.launch')
error_msg = roslaunch.rlutil.check_roslaunch(filename)
assert error_msg is None

View File

@@ -0,0 +1,482 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import os
import sys
import unittest
import time
import roslaunch.pmon
import roslaunch.server
from roslaunch.server import ProcessListener
try:
from xmlrpc.client import ServerProxy
except ImportError:
from xmlrpclib import ServerProxy
import rosgraph
master = rosgraph.Master('test_roslaunch_server')
def get_param(*args):
return master.getParam(*args)
class MockProcessListener(ProcessListener):
def process_died(self, name, code):
self.process_name = name
self.exit_code = code
## Fake Process object
class ProcessMock(roslaunch.pmon.Process):
def __init__(self, package, name, args, env, respawn=False):
super(ProcessMock, self).__init__(package, name, args, env, respawn)
self.stopped = False
def stop(self):
self.stopped = True
## Fake ProcessMonitor object
class ProcessMonitorMock(object):
def __init__(self):
self.core_procs = []
self.procs = []
self.listeners = []
self.is_shutdown = False
def join(self, timeout=0):
pass
def add_process_listener(self, l):
self.listeners.append(l)
def register(self, p):
self.procs.append(p)
def register_core_proc(self, p):
self.core_procs.append(p)
def registrations_complete(self):
pass
def unregister(self, p):
self.procs.remove(p)
def has_process(self, name):
return len([p for p in self.procs if p.name == name]) > 0
def get_process(self, name):
val = [p for p in self.procs if p.name == name]
if val:
return val[0]
return None
def has_main_thread_jobs(self):
return False
def do_main_thread_jobs(self):
pass
def kill_process(self, name):
pass
def shutdown(self):
self.is_shutdown = True
def get_active_names(self):
return [p.name for p in self.procs]
def get_process_names_with_spawn_count(self):
actives = [(p.name, p.spawn_count) for p in self.procs]
deads = []
return [actives, deads]
def mainthread_spin_once(self):
pass
def mainthread_spin(self):
pass
def run(self):
pass
class TestHandler(roslaunch.server.ROSLaunchBaseHandler):
def __init__(self, pm):
super(TestHandler, self).__init__(pm)
self.pinged = None
def ping(self, val):
self.pinged = val
return True
## Test roslaunch.server
class TestRoslaunchServer(unittest.TestCase):
def setUp(self):
self.pmon = ProcessMonitorMock()
roslaunch.core.clear_printlog_handlers()
roslaunch.core.clear_printerrlog_handlers()
def test_ChildRoslaunchProcess(self):
# test that constructor is wired up correctly
from roslaunch.server import ChildROSLaunchProcess
name = 'foo-%s'%time.time()
args = [time.time(), time.time()]
env = { 'key-%s'%time.time() : str(time.time()) }
cp = ChildROSLaunchProcess(name, args, env)
self.assertEquals(name, cp.name)
self.assertEquals(args, cp.args)
self.assertEquals(env, cp.env)
self.assertEquals(None, cp.uri)
uri = 'http://foo:1234'
cp.set_uri(uri)
self.assertEquals(uri, cp.uri)
def _succeed(self, retval):
code, msg, val = retval
self.assertEquals(1, code)
self.assert_(type(msg) == str)
return val
def test_ROSLaunchBaseHandler(self):
from roslaunch.server import ROSLaunchBaseHandler
try:
ROSLaunchBaseHandler(None)
self.fail("should not allow pm as None")
except: pass
pmon = self.pmon
h = ROSLaunchBaseHandler(pmon)
self._test_ROSLaunchBaseHandler(h)
# reusable parent class test
def _test_ROSLaunchBaseHandler(self, h):
pmon = self.pmon
# - test get pid
self.assertEquals(os.getpid(), self._succeed(h.get_pid()))
# - test list processes
process_list = self._succeed(h.list_processes())
self.assertEquals([[], []], process_list)
p = ProcessMock('pack', 'name', [], {})
p.spawn_count = 1
pmon.register(p)
process_list = self._succeed(h.list_processes())
self.assertEquals([('name', 1),], process_list[0])
self.assertEquals([], process_list[1])
p.spawn_count = 2
process_list = self._succeed(h.list_processes())
self.assertEquals([('name', 2),], process_list[0])
self.assertEquals([], process_list[1])
# - cleanup our work
pmon.unregister(p)
# - test process_info
code, msg, val = h.process_info('fubar')
self.assertEquals(-1, code)
self.assert_(type(msg) == str)
self.assertEquals({}, val)
pmon.register(p)
self.assertEquals(p.get_info(), self._succeed(h.process_info(p.name)))
pmon.unregister(p)
# - test get_node_names
# - reach into instance to get branch-complete
h.pm = None
code, msg, val = h.get_node_names()
self.assertEquals(0, code)
self.assert_(type(msg) == str)
self.assertEquals([], val)
h.pm = pmon
self.assertEquals(pmon.get_active_names(), self._succeed(h.get_node_names()))
pmon.register(p)
self.assertEquals(pmon.get_active_names(), self._succeed(h.get_node_names()))
pmon.unregister(p)
def test_ROSLaunchParentHandler(self):
from roslaunch.server import ROSLaunchParentHandler
from roslaunch.server import ChildROSLaunchProcess
try:
ROSLaunchParentHandler(None, {}, [])
self.fail("should not allow pm as None")
except: pass
pmon = self.pmon
child_processes = {}
listeners = []
h = ROSLaunchParentHandler(pmon, child_processes, listeners)
self.assertEquals(child_processes, h.child_processes)
self.assertEquals(listeners, h.listeners)
self._test_ROSLaunchBaseHandler(h)
from rosgraph_msgs.msg import Log
h.log('client-1', Log.FATAL, "message")
h.log('client-1', Log.ERROR, "message")
h.log('client-1', Log.DEBUG, "message")
h.log('client-1', Log.INFO, "started with pid 1234")
# test process_died
# - no listeners
self._succeed(h.process_died('dead1', -1))
# - well-behaved listener
l = roslaunch.pmon.ProcessListener()
listeners.append(l)
self._succeed(h.process_died('dead2', -1))
# - bad listener with exception
def bad():
raise Exception("haha")
l.process_died = bad
self._succeed(h.process_died('dead3', -1))
# test register
# - verify clean slate with list_children
self.assertEquals([], self._succeed(h.list_children()))
# - first register with unknown
code, msg, val = h.register('client-unknown', 'http://unroutable:1234')
self.assertEquals(-1, code)
self.assertEquals([], self._succeed(h.list_children()))
# - now register with known
uri = 'http://unroutable:1324'
child_processes['client-1'] = ChildROSLaunchProcess('foo', [], {})
val = self._succeed(h.register('client-1', uri))
self.assert_(type(val) == int)
self.assertEquals([uri], self._succeed(h.list_children()))
def test_ROSLaunchChildHandler(self):
from roslaunch.server import ROSLaunchChildHandler
pmon = self.pmon
try:
# if there is a core up, we have to use its run id
run_id = get_param('/run_id')
except:
run_id = 'foo-%s'%time.time()
name = 'foo-bob'
server_uri = 'http://localhost:12345'
try:
h = ROSLaunchChildHandler(run_id, name, server_uri, None)
self.fail("should not allow pm as None")
except: pass
try:
h = ROSLaunchChildHandler(run_id, name, 'http://bad_uri:0', pmon)
self.fail("should not allow bad uri")
except: pass
try:
h = ROSLaunchChildHandler(run_id, name, None, pmon)
self.fail("should not allow None server_uri")
except: pass
h = ROSLaunchChildHandler(run_id, name, server_uri, pmon)
self.assertEquals(run_id, h.run_id)
self.assertEquals(name, h.name)
self.assertEquals(server_uri, h.server_uri)
self._test_ROSLaunchBaseHandler(h)
# test _log()
from rosgraph_msgs.msg import Log
h._log(Log.INFO, 'hello')
# test shutdown()
# should uninitialize pm
h.shutdown()
self.assert_(pmon.is_shutdown)
# - this check is mostly to make sure that the launch() call below will exit
self.assert_(h.pm is None)
code, msg, val = h.launch('<launch></launch>')
self.assertEquals(0, code)
# TODO: actual launch. more difficult as we need a core
def test__ProcessListenerForwarder(self):
from roslaunch.server import _ProcessListenerForwarder, ProcessListener
# test with bad logic first
f = _ProcessListenerForwarder(None)
# - should trap exceptions
f.process_died("foo", -1)
# test with actual listener
l = MockProcessListener()
f = _ProcessListenerForwarder(l)
# - should run normally
f.process_died("foo", -1)
self.assertEquals(l.process_name, 'foo')
self.assertEquals(l.exit_code, -1)
def test_ROSLaunchNode(self):
#exercise the basic ROSLaunchNode API
from roslaunch.server import ROSLaunchNode
# - create a node with a handler
handler = TestHandler(self.pmon)
node = ROSLaunchNode(handler)
# - start the node
node.start()
self.assert_(node.uri)
# - call the ping API that we added
s = ServerProxy(node.uri)
test_val = 'test-%s'%time.time()
s.ping(test_val)
self.assertEquals(handler.pinged, test_val)
# - call the pid API
code, msg, pid = s.get_pid()
self.assertEquals(1, code)
self.assert_(type(msg) == str)
self.assertEquals(os.getpid(), pid)
# - shut it down
node.shutdown('test done')
def test_ROSLaunchParentNode(self):
from roslaunch.server import ROSLaunchParentNode
from roslaunch.server import ChildROSLaunchProcess
from roslaunch.config import ROSLaunchConfig
from roslaunch.pmon import ProcessListener
rosconfig = ROSLaunchConfig()
try:
ROSLaunchParentNode(rosconfig, None)
self.fail("should not allow pm as None")
except: pass
pmon = self.pmon
n = ROSLaunchParentNode(rosconfig, pmon)
self.assertEquals(rosconfig, n.rosconfig)
self.assertEquals([], n.listeners)
self.assertEquals({}, n.child_processes)
self.assertEquals(n.handler.listeners, n.listeners)
self.assertEquals(n.handler.child_processes, n.child_processes)
# test add listener
self.assertEquals(n.handler.listeners, n.listeners)
l = ProcessListener()
n.add_process_listener(l)
self.assertEquals([l], n.listeners)
self.assertEquals(n.handler.listeners, n.listeners)
# now, lets make some xmlrpc calls against it
import roslaunch.config
server = roslaunch.server.ROSLaunchParentNode(roslaunch.config.ROSLaunchConfig(), self.pmon)
# it's really dangerous for logging when both a parent and a
# child are in the same process, so out of paranoia, clear the
# logging handlers
roslaunch.core.clear_printlog_handlers()
roslaunch.core.clear_printerrlog_handlers()
# - register a fake child with the server so that it accepts registration from ROSLaunchChild
child_name = 'child-%s'%time.time()
child_proc = ChildROSLaunchProcess('foo', [], {})
server.add_child(child_name, child_proc)
try:
server.start()
self.assert_(server.uri, "server URI did not initialize")
s = ServerProxy(server.uri)
child_uri = 'http://fake-unroutable:1324'
# - list children should be empty
val = self._succeed(s.list_children())
self.assertEquals([], val)
# - register
val = self._succeed(s.register(child_name, child_uri))
self.assertEquals(1, val)
# - list children
val = self._succeed(s.list_children())
self.assertEquals([child_uri], val)
finally:
server.shutdown('test done')
def test_ROSLaunchChildNode(self):
from roslaunch.server import ROSLaunchChildNode
from roslaunch.server import ChildROSLaunchProcess
pmon = self.pmon
try:
# if there is a core up, we have to use its run id
run_id = get_param('/run_id')
except:
run_id = 'foo-%s'%time.time()
name = 'foo-bob'
server_uri = 'http://localhost:12345'
try:
ROSLaunchChildNode(run_id, name, server_uri, None)
self.fail("should not allow pm as None")
except: pass
try:
ROSLaunchChildNode(run_id, name, 'http://bad_uri:0', pmon)
self.fail("should not allow bad uri")
except: pass
n = ROSLaunchChildNode(run_id, name, server_uri, pmon)
self.assertEquals(run_id, n.run_id)
self.assertEquals(name, n.name)
self.assertEquals(server_uri, n.server_uri)
# tests for actual registration with server
import roslaunch.config
server = roslaunch.server.ROSLaunchParentNode(roslaunch.config.ROSLaunchConfig(), self.pmon)
# - register a fake child with the server so that it accepts registration from ROSLaunchChild
child_proc = ChildROSLaunchProcess('foo', [], {})
server.add_child(name, child_proc)
try:
server.start()
self.assert_(server.uri, "server URI did not initialize")
s = ServerProxy(server.uri)
print("SERVER STARTED")
# start the child
n = ROSLaunchChildNode(run_id, name, server.uri, pmon)
n.start()
print("CHILD STARTED")
self.assert_(n.uri, "child URI did not initialize")
# verify registration
print("VERIFYING REGISTRATION")
self.assertEquals(child_proc.uri, n.uri)
child_uri = 'http://fake-unroutable:1324'
# - list children
val = self._succeed(s.list_children())
self.assertEquals([n.uri], val)
finally:
print("SHUTTING DOWN")
n.shutdown('test done')
server.shutdown('test done')

View File

@@ -0,0 +1,199 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2009, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import os
import sys
import rospkg
def test__arg():
import random
from roslaunch.substitution_args import _arg, ArgException, SubstitutionException
ctx = { 'arg': {
'foo': '12345',
'bar': 'hello world',
'baz': 'sim',
'empty': '',
}
}
# test invalid
try:
_arg('$(arg)', 'arg', [], ctx)
assert False, "should have thrown"
except SubstitutionException:
pass
# test normal
tests = [
('12345', ('$(arg foo)', 'arg foo', ['foo'], ctx)),
('', ('$(arg empty)', 'arg empty', ['empty'], ctx)),
('sim', ('$(arg baz)', 'arg baz', ['baz'], ctx)),
# test with other args present, should only resolve match
('1234512345', ('$(arg foo)$(arg foo)', 'arg foo', ['foo'], ctx)),
('12345$(arg baz)', ('$(arg foo)$(arg baz)', 'arg foo', ['foo'], ctx)),
('$(arg foo)sim', ('$(arg foo)$(arg baz)', 'arg baz', ['baz'], ctx)),
# test double-resolve safe
('12345', ('12345', 'arg foo', ['foo'], ctx)),
]
for result, test in tests:
resolved, a, args, context = test
assert result == _arg(resolved, a, args, context)
# - test that all fail if ctx is not set
for result, test in tests:
resolved, a, args, context = test
try:
_arg(resolved, a, args, {})
assert False, "should have thrown"
except ArgException as e:
assert args[0] == str(e)
try:
_arg(resolved, a, args, {'arg': {}})
assert False, "should have thrown"
except ArgException as e:
assert args[0] == str(e)
def test_resolve_args():
from roslaunch.substitution_args import resolve_args, SubstitutionException
r = rospkg.RosPack()
roslaunch_dir = r.get_path('roslaunch')
assert roslaunch_dir
anon_context = {'foo': 'bar'}
arg_context = {'fuga': 'hoge', 'car': 'cdr', 'arg': 'foo', 'True': 'False'}
context = {'anon': anon_context, 'arg': arg_context }
tests = [
('$(find roslaunch)', roslaunch_dir),
('hello$(find roslaunch)', 'hello'+roslaunch_dir),
('$(find roslaunch )', roslaunch_dir),
('$$(find roslaunch )', '$'+roslaunch_dir),
('$( find roslaunch )', roslaunch_dir),
('$(find roslaunch )', roslaunch_dir),
('$(find roslaunch)$(find roslaunch)', roslaunch_dir+os.sep+roslaunch_dir),
('$(find roslaunch)/foo/bar.xml', roslaunch_dir+os.sep+'foo'+os.sep+'bar.xml'),
(r'$(find roslaunch)\foo\bar.xml $(find roslaunch)\bar.xml', roslaunch_dir+os.sep+'foo'+os.sep+'bar.xml '+roslaunch_dir+os.sep+'bar.xml'),
('$(find roslaunch)\\foo\\bar.xml more/stuff\\here', roslaunch_dir+os.sep+'foo'+os.sep+'bar.xml more/stuff\\here'),
('$(env ROS_ROOT)', os.environ['ROS_ROOT']),
('$(env ROS_ROOT)', os.environ['ROS_ROOT']),
('$(env ROS_ROOT )', os.environ['ROS_ROOT']),
('$(optenv ROS_ROOT)', os.environ['ROS_ROOT']),
('$(optenv ROS_ROOT)$(optenv ROS_ROOT)', os.environ['ROS_ROOT']+os.environ['ROS_ROOT']),
('$(optenv ROS_ROOT alternate text)', os.environ['ROS_ROOT']),
('$(optenv NOT_ROS_ROOT)', ''),
('$(optenv NOT_ROS_ROOT)more stuff', 'more stuff'),
('$(optenv NOT_ROS_ROOT alternate)', 'alternate'),
('$(optenv NOT_ROS_ROOT alternate text)', 'alternate text'),
# #1776
('$(anon foo)', 'bar'),
('$(anon foo)/baz', 'bar/baz'),
('$(anon foo)/baz/$(anon foo)', 'bar/baz/bar'),
# arg
('$(arg fuga)', 'hoge'),
('$(arg fuga)$(arg fuga)', 'hogehoge'),
('$(arg car)$(arg fuga)', 'cdrhoge'),
('$(arg fuga)hoge', 'hogehoge'),
# $(eval ...) versions of those tests
("$(eval find('roslaunch'))", roslaunch_dir),
("$(eval env('ROS_ROOT'))", os.environ['ROS_ROOT']),
("$(eval optenv('ROS_ROOT', 'alternate text'))", os.environ['ROS_ROOT']),
("$(eval optenv('NOT_ROS_ROOT', 'alternate text'))", "alternate text"),
("$(eval optenv('NOT_ROS_ROOT'))", ""),
("$(eval anon('foo'))", 'bar'),
("$(eval arg('fuga'))", 'hoge'),
('$(eval arg("fuga"))', 'hoge'),
('$(eval arg("arg"))', 'foo'),
('$(eval arg("True"))', 'False'),
('$(eval 1==1)', 'True'),
('$(eval [0,1,2][1])', '1'),
# test implicit arg access
('$(eval fuga)', 'hoge'),
('$(eval True)', 'True'),
# math expressions
('$(eval round(sin(pi),1))', '0.0'),
('$(eval cos(0))', '1.0'),
# str, map
("$(eval ''.join(map(str, [4,2])))", '42'),
]
for arg, val in tests:
assert val == resolve_args(arg, context=context), arg
# more #1776
r = resolve_args('$(anon foo)/bar')
assert '/bar' in r
assert not '$(anon foo)' in r
# test against strings that should not match
noop_tests = [
'$(find roslaunch', '$find roslaunch', '', ' ', 'noop', 'find roslaunch', 'env ROS_ROOT', '$$', ')', '(', '()',
None,
]
for t in noop_tests:
assert t == resolve_args(t)
failures = [
'$((find roslaunch))', '$(find $roslaunch)',
'$(find)', '$(find roslaunch roslaunch)', '$(export roslaunch)',
'$(env)', '$(env ROS_ROOT alternate)',
'$(env NOT_SET)',
'$(optenv)',
'$(anon)',
'$(anon foo bar)',
]
for f in failures:
try:
resolve_args(f)
assert False, "resolve_args(%s) should have failed"%f
except SubstitutionException: pass
def test_resolve_duplicate_anon():
from roslaunch.config import ROSLaunchConfig
from roslaunch.core import RLException
from roslaunch.xmlloader import XmlLoader
loader = XmlLoader()
config = ROSLaunchConfig()
test_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'xml'))
try:
loader.load(os.path.join(test_path, 'test-substitution-duplicate-anon-names.xml'), config)
assert False, 'loading a launch file with duplicate anon node names should have raised an exception'
except RLException:
pass

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
<blah>

View File

@@ -0,0 +1,4 @@
<launch>
<!-- roslaunch file that loads no nodes, and thus should exit immediately -->
<param name="noop" value="noop" />
</launch>

View File

@@ -0,0 +1 @@
<foo />

View File

@@ -0,0 +1,48 @@
<launch>
<arg name="required" />
<arg name="optional" default="not_set" />
<arg name="grounded" value="parent" />
<arg name="include_arg" value="dontcare" />
<arg name="if_test" value="dontcare" />
<arg name="param1_name" value="p1" />
<arg name="param2_name" value="p2" />
<arg name="param3_name" value="p3" />
<arg name="param1_value" value="$(arg required)" />
<arg name="param2_value" value="$(arg optional)" />
<arg name="param3_value" value="$(arg grounded)" />
<param name="$(arg param1_name)_test" value="$(arg param1_value)" />
<param name="$(arg param2_name)_test" value="$(arg param2_value)" />
<param name="$(arg param3_name)_test" value="$(arg param3_value)" />
<!-- Normal include: explicitly set args -->
<include ns="notall" file="$(find roslaunch)/test/xml/test-arg-include.xml">
<arg name="required" value="$(arg required)" />
<arg name="include_arg" value="$(arg include_arg)" />
<arg name="if_test" value="$(arg if_test)" />
</include>
<!-- Normal include: explicitly set args, including an optional one -->
<include ns="notall_optional" file="$(find roslaunch)/test/xml/test-arg-include.xml">
<arg name="required" value="$(arg required)" />
<arg name="optional" value="$(arg optional)" />
<arg name="include_arg" value="$(arg include_arg)" />
<arg name="if_test" value="$(arg if_test)" />
</include>
<!-- Include with passing in all args in my namespace -->
<include ns="all" file="$(find roslaunch)/test/xml/test-arg-include.xml"
pass_all_args="true">
</include>
<!-- Include with passing in all args in my namespace, and then override one
of them explicitly -->
<include ns="all_override" file="$(find roslaunch)/test/xml/test-arg-include.xml"
pass_all_args="true">
<arg name="required" value="override" />
</include>
</launch>

View File

@@ -0,0 +1,26 @@
<launch>
<arg name="required" />
<arg name="if_test" />
<arg name="include_arg" />
<arg name="optional" default="not_set" />
<arg name="grounded" value="set" />
<arg name="param1_name" value="p1" />
<arg name="param2_name" value="p2" />
<arg name="param3_name" value="p3" />
<arg name="param4_name" value="p4" />
<arg name="param1_value" value="$(arg required)" />
<arg name="param2_value" value="$(arg optional)" />
<arg name="param3_value" value="$(arg grounded)" />
<group ns="include_test">
<param name="$(arg param1_name)_test" value="$(arg param1_value)" />
<param name="$(arg param2_name)_test" value="$(arg param2_value)" />
<param name="$(arg param3_name)_test" value="$(arg param3_value)" />
<param name="$(arg param4_name)_test" value="$(arg include_arg)" />
</group>
</launch>

View File

@@ -0,0 +1,5 @@
<launch>
<include file="$(find roslaunch)/test/xml/test-arg-invalid-included.xml">
<arg name="grounded" value="not_set"/>
</include>
</launch>

View File

@@ -0,0 +1,6 @@
<launch>
<include file="$(find roslaunch)/test/xml/test-arg-invalid-included.xml" pass_all_args="true">
<arg name="grounded" value="not_set"/>
</include>
</launch>

View File

@@ -0,0 +1,3 @@
<launch>
<arg name="grounded" value="set"/>
</launch>

View File

@@ -0,0 +1,6 @@
<launch>
<arg name="grounded" value="1"/>
<arg name="grounded" value="regrounded"/>
</launch>

View File

@@ -0,0 +1,5 @@
<launch>
<arg name="invalid" value="$(arg missing)"/>
</launch>

View File

@@ -0,0 +1,5 @@
<launch>
<arg name="grounded" value="not_set"/>
<include file="$(find roslaunch)/test/xml/test-arg-invalid-included.xml" pass_all_args="true"/>
</launch>

View File

@@ -0,0 +1,98 @@
<launch>
<arg name="required" />
<arg name="if_test" />
<arg name="optional" default="not_set" />
<arg name="grounded" value="set" />
<arg name="include_arg" value="initial" />
<arg name="param1_name" value="p1" />
<arg name="param2_name" value="p2" />
<arg name="param3_name" value="p3" />
<arg name="param1_value" value="$(arg required)" />
<arg name="param2_value" value="$(arg optional)" />
<arg name="param3_value" value="$(arg grounded)" />
<arg name="int_value" value="1234" />
<arg name="float_value" value="3.0" />
<arg if="$(arg if_test)" name="if_param_value" value="true" />
<arg unless="$(arg if_test)" name="if_param_value" value="false" />
<arg if="$(arg if_test)" name="py_if_param_value" value="$(eval 1 == 1)"/>
<arg unless="$(arg if_test)" name="py_if_param_value" value="$(eval 1 == 0)"/>
<param name="$(arg param1_name)_test" value="$(arg param1_value)" />
<param name="$(arg param2_name)_test" value="$(arg param2_value)" />
<param name="$(arg param3_name)_test" value="$(arg param3_value)" />
<param name="if_param" value="$(arg if_param_value)" />
<param name="py_if_param" value="$(arg py_if_param_value)" />
<param name="int_param" value="$(arg int_value)" />
<param name="float_param" value="$(arg float_value)" />
<group>
<arg name="context" value="group1" />
<param name="context1" value="$(arg context)" />
</group>
<group>
<arg name="context" value="group2" />
<param name="context2" value="$(arg context)" />
</group>
<arg name="fail" value="0" />
<group if="$(arg fail)">
<param name="fail" value="fail" />
</group>
<group unless="$(arg fail)">
<param name="succeed" value="yes" />
</group>
<group if="$(arg if_test)">
<param name="if_test" value="ran" />
</group>
<group unless="$(arg if_test)">
<param name="if_test" value="not_ran" />
</group>
<arg name="py_fail" value="$(eval 0 &gt; 1)"/>
<group if="$(eval py_fail == True)">
<param name="py_fail" value="fail" />
</group>
<group unless="$(eval arg('py_fail') == True)">
<param name="py_succeed" value="yes" />
</group>
<group if="$(eval if_test == True)">
<param name="py_if_test" value="ran" />
</group>
<group unless="$(eval arg('if_test') == True)">
<param name="py_if_test" value="not_ran" />
</group>
<include file="$(find roslaunch)/test/xml/test-arg-include.xml">
<arg name="required" value="required1" />
<arg name="include_arg" value="$(arg include_arg)" />
</include>
<include ns="include2" file="$(find roslaunch)/test/xml/test-arg-include.xml">
<arg name="required" value="required2" />
<arg name="optional" value="optional2" />
<arg name="include_arg" value="new2" />
</include>
<include if="$(arg if_test)" ns="include3" file="$(find roslaunch)/test/xml/test-arg-include.xml">
<arg name="required" value="required3" />
<arg name="optional" value="optional3" />
<arg name="include_arg" value="new3" />
</include>
</launch>

View File

@@ -0,0 +1,3 @@
<launch>
<!-- intentionally empty -->
</launch>

View File

@@ -0,0 +1,3 @@
<launch>
<node pkg="package" type="test_clear_params_no_name" clear_params="true" />
</launch>

View File

@@ -0,0 +1,4 @@
<launch>
<!-- group with no 'ns' attribute -->
<group clear_params="true" />
</launch>

View File

@@ -0,0 +1,4 @@
<launch>
<!-- include with no 'ns' attribute -->
<include clear_params="true" file="$(find roslaunch)/test/xml/test-clear-params-include.xml" />
</launch>

View File

@@ -0,0 +1,3 @@
<launch>
<node pkg="package" name="node_name" type="test_clear_params_invalid_clear_params_attr" clear_params="foo" />
</launch>

View File

@@ -0,0 +1,28 @@
<launch>
<node pkg="package" type="type" name="test_clear_params1" clear_params="true" />
<node pkg="package" type="type" name="test_clear_params2" clear_params="True" />
<node pkg="package" type="type" name="test_clear_params_ns" ns="clear_params_ns" clear_params="true"/>
<node pkg="package" type="type" name="test_clear_params_default" ns="clear_params_default" />
<node pkg="package" type="type" name="test_clear_params_false" ns="clear_params_false" clear_params="False"/>
<group ns="group_test" clear_params="true">
</group>
<group ns="group_test_default">
</group>
<group ns="group_test_false" clear_params="false">
</group>
<group ns="embed_group_test">
<group ns="embedded_group" clear_params="true" />
</group>
<include ns="include_test" clear_params="true" file="$(find roslaunch)/test/xml/test-clear-params-include.xml" />
<include ns="include_test_false" clear_params="false" file="$(find roslaunch)/test/xml/test-clear-params-include.xml" />
<include ns="include_test_default" file="$(find roslaunch)/test/xml/test-clear-params-include.xml" />
</launch>

View File

@@ -0,0 +1,41 @@
<launch>
<param name="~tilde1" value="foo" />
<rosparam file="$(find roslaunch)/test/dump-params.yaml" command="load" />
<group ns="rosparam">
<param name="~tilde2" value="bar" />
<rosparam file="$(find roslaunch)/test/dump-params.yaml" command="load" />
</group>
<node pkg="package" type="test_base" name="node_rosparam">
<param name="local_param" value="baz" />
<rosparam file="$(find roslaunch)/test/dump-params.yaml" command="load" />
</node>
<node pkg="package" type="test_base" name="node_rosparam2">
</node>
<rosparam param="inline_str">value1</rosparam>
<rosparam param="inline_list">[1, 2, 3, 4]</rosparam>
<rosparam param="inline_dict">{key1: value1, key2: value2}</rosparam>
<rosparam param="inline_dict2">
key3: value3
key4: value4
</rosparam>
<rosparam param="override">{key1: value1, key2: value2}</rosparam>
<rosparam param="override">{key1: override1}</rosparam>
<!-- no key tests -->
<rosparam>
noparam1: value1
noparam2: value2
</rosparam>
<!-- allow empty files. we test absense of loading in test-rosparam-empty.
Here we include to make sure this doesn't blank out other parameters -->
<rosparam file="$(find roslaunch)/test/params_empty1.yaml" command="load" />
</launch>

View File

@@ -0,0 +1,3 @@
<launch>
<node name="n12i" pkg="package" type="test_one_two_include" />
</launch>

View File

@@ -0,0 +1,18 @@
<launch>
<node name="n0" pkg="package" type="test_none" />
<env name="ONE" value="one" />
<node name="n1" pkg="package" type="test_one">
</node>
<env name="TWO" value="two" />
<node name="n2" pkg="package" type="test_one_two_priv">
<env name="PRIVATE_TWO" value="private_two" />
</node>
<node name="n3" pkg="package" type="test_one_two" />
<include file="$(find roslaunch)/test/xml/test-env-include.xml">
<env name="INCLUDE" value="include" />
</include>
</launch>

View File

@@ -0,0 +1,6 @@
<launch>
<group if="1" unless="1">
</group>
</launch>

View File

@@ -0,0 +1,63 @@
<launch>
<group if="1">
<param name="group_if_pass" value="1" />
</group>
<group if="0">
<param name="group_if_fail" value="1" />
</group>
<group unless="0">
<param name="group_unless_pass" value="1" />
</group>
<group unless="1">
<param name="group_unless_fail" value="1" />
</group>
<param name="param_if_pass" value="1" if="1" />
<param name="param_if_fail" value="1" if="0" />
<param name="param_unless_pass" value="1" unless="0" />
<param name="param_unless_fail" value="1" unless="1" />
<remap from="from_if_pass" to="to_if_pass" if="1" />
<remap from="from_if_fail" to="to_if_fail" if="0" />
<remap from="from_unless_pass" to="to_unless_pass" unless="0" />
<remap from="from_unless_fail" to="to_unless_fail" unless="1" />
<!-- Test Python parsing -->
<group if="$(eval 1 == 1)">
<param name="py_group_if_pass" value="1" />
</group>
<group if="$(eval 0 == 1)">
<param name="py_group_if_fail" value="1" />
</group>
<group unless="$(eval 0 == 1)">
<param name="py_group_unless_pass" value="1" />
</group>
<group unless="$(eval 1 == 1)">
<param name="py_group_unless_fail" value="1" />
</group>
<arg name="true_lower" value="true"/>
<arg name="true_upper" value="True"/>
<arg name="false_lower" value="false"/>
<arg name="false_upper" value="False"/>
<param name="py_param_if_pass" value="1" if="$(eval arg('true_lower') and (40+2) == 42)"/>
<param name="py_param_if_fail" value="1" if="$(eval false_lower or (40-2) != 38)"/>
<param name="py_param_unless_pass" value="1" unless="$(eval arg('false_upper') or 1 &lt; 1)"/>
<param name="py_param_unless_fail" value="1" unless="$(eval true_upper and 1 &gt;= 1)"/>
<remap from="py_from_if_pass" to="py_to_if_pass" if="$(eval arg('true_upper') == True)"/>
<remap from="py_from_if_fail" to="py_to_if_fail" if="$(eval false_upper != False)"/>
<remap from="py_from_unless_pass" to="py_to_unless_pass"
unless="$(eval arg('false_lower') or false_upper or arg('false_upper') != False)"/>
<remap from="py_from_unless_fail" to="py_to_unless_fail"
unless="$(eval arg('true_lower') and true_upper and arg('true_upper') == True)"/>
<node name="remap" pkg="rospy" type="talker.py" />
</launch>

View File

@@ -0,0 +1,20 @@
<launch>
<!-- no local params -->
<node pkg="pkg" type="type0" name="node0"/>
<group ns="group1">
<!-- basic test, named nodes -->
<node pkg="pkg" type="gtype0" name="g1node0" />
<param name="~gparam1" value="val1" />
<node pkg="pkg" type="g1type1" name="g1node1" />
<param name="~gparam2" value="val1" />
<node pkg="pkg" type="g1type2" name="g1node2" />
</group>
<!-- make sure scope is cleared by end of group -->
<param name="~param1" value="val1" />
<node pkg="pkg" type="type1" name="node1" />
</launch>

View File

@@ -0,0 +1,3 @@
<launch>
<machine name="machine3" address="address3" ros-ip="hostname" />
</launch>

View File

@@ -0,0 +1,9 @@
<launch>
<machine name="machine4" address="address4">
<env name="ENV1" value="value1" />
<env name="ENV2" value="value2" />
<env name="ENV3" value="value3" />
</machine>
</launch>

View File

@@ -0,0 +1,6 @@
<launch>
<!-- if ros-root is explicitly set and ros-package-path is not, ros-package-path should not get a default value -->
<machine name="machine5" address="address5" ros-root="/ros/root" />
</launch>

View File

@@ -0,0 +1,5 @@
<launch>
<!-- address is required -->
<machine name="machine1" />
</launch>

View File

@@ -0,0 +1,5 @@
<launch>
<!-- name is required -->
<machine address="address1" />
</launch>

View File

@@ -0,0 +1,5 @@
<launch>
<!-- timeout must be non-empty -->
<machine timeout="" name="one" address="address1" />
</launch>

View File

@@ -0,0 +1,5 @@
<launch>
<!-- timeout must be a number -->
<machine timeout="ten" name="one" address="address1" />
</launch>

View File

@@ -0,0 +1,5 @@
<launch>
<!-- timeout must be a number -->
<machine timeout="-1" name="one" address="address1" />
</launch>

Some files were not shown because too many files have changed in this diff Show More