├── .gitignore ├── .travis.yml ├── CHANGELOG.rst ├── CMakeLists.txt ├── README.md ├── bin ├── appmaster └── rosget ├── launch └── app_manager.launch ├── msg ├── App.msg ├── AppInstallationState.msg ├── AppList.msg ├── AppStatus.msg ├── ClientApp.msg ├── ExchangeApp.msg ├── Icon.msg ├── KeyValue.msg └── StatusCodes.msg ├── package.xml ├── scripts ├── app_manager └── test_app.py ├── setup.py ├── src └── app_manager │ ├── __init__.py │ ├── app.py │ ├── app_list.py │ ├── app_manager.py │ ├── app_manager_plugin.py │ ├── exceptions.py │ ├── exchange.py │ └── master_sync.py ├── srv ├── GetAppDetails.srv ├── GetInstallationState.srv ├── InstallApp.srv ├── ListApps.srv ├── StartApp.srv ├── StopApp.srv └── UninstallApp.srv └── test ├── appA.app ├── applist0 └── .gitignore ├── applist1 └── apps1.installed ├── applistbad ├── bad.installed └── bad2.installed ├── empty.interface ├── plugin ├── __init__.py ├── package.xml ├── plugin.installed ├── plugin.yaml ├── sample_node.py ├── sample_node.xml ├── test_plugin.app ├── test_plugin.interface ├── test_plugin.py └── test_plugin_timeout.app ├── resources └── example-min.launch ├── test1.interface ├── test_app.py ├── test_app.test ├── test_app_list.py ├── test_plugin.py ├── test_plugin.test ├── test_plugin_fail.test ├── test_plugin_success.test ├── test_plugin_timeout.test ├── test_start_fail.py ├── test_start_fail.test └── test_stop_app.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *~ 3 | .*~ 4 | \#* 5 | .\#* 6 | 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | 2 | # this is .traivs.yml written by - 3 | 4 | # https://github.com/ros-infrastructure/ros_buildfarm/blob/master/doc/jobs/devel_jobs.rst 5 | # https://github.com/ros-infrastructure/ros_buildfarm/blob/master/doc/jobs/prerelease_jobs.rst 6 | # while this doesn't require sudo we don't want to run within a Docker container 7 | sudo: true 8 | dist: bionic 9 | language: python 10 | addons: 11 | apt: 12 | packages: 13 | - 2to3 14 | env: 15 | global: 16 | - JOB_PATH=/tmp/devel_job 17 | - ABORT_ON_TEST_FAILURE=1 18 | - INDEX_URL=https://raw.githubusercontent.com/ros-infrastructure/ros_buildfarm_config/production/index.yaml 19 | matrix: 20 | - CHECK_PYTHON2_COMPILE=true 21 | - CHECK_PYTHON3_COMPILE=true 22 | - ROS_DISTRO_NAME=kinetic OS_NAME=ubuntu OS_CODE_NAME=xenial ARCH=amd64 INDEX_URL=https://raw.githubusercontent.com/ros-infrastructure/ros_buildfarm_config/7e6385e/index.yaml 23 | - ROS_DISTRO_NAME=melodic OS_NAME=ubuntu OS_CODE_NAME=bionic ARCH=amd64 24 | - ROS_DISTRO_NAME=noetic OS_NAME=ubuntu OS_CODE_NAME=focal ARCH=amd64 25 | # matrix: 26 | # allow_failures: 27 | # - env: ROS_DISTRO_NAME=kinetic OS_NAME=ubuntu OS_CODE_NAME=xenial ARCH=amd64 28 | install: 29 | # check python2 compatibility 30 | - if [ "${CHECK_PYTHON2_COMPILE}" == "true" ]; then python2 -m compileall . ; exit $?; fi 31 | # check python3 compatibility 32 | - if [ "${CHECK_PYTHON3_COMPILE}" == "true" ]; then bash -c "ret=0; trap 'ret=1' ERR; python3 -m compileall .; 2to3 -w -f except -f execfile -f has_key -f import -f raw_input -f zip .; git diff --exit-code . > /dev/null; echo Exitting with \$ret; exit \$ret"; exit $?; fi 33 | # either install the latest released version of ros_buildfarm 34 | # - pip install ros_buildfarm 35 | # or checkout a specific branch 36 | - git clone -b master https://github.com/ros-infrastructure/ros_buildfarm /tmp/ros_buildfarm 37 | # force enable `rosdep update --include-eol-distros` until https://github.com/ros-infrastructure/ros_buildfarm/pull/890 released 38 | - (cd /tmp/ros_buildfarm; git checkout f7a12d8) 39 | - (cd /tmp; wget https://github.com/ros-infrastructure/ros_buildfarm/pull/890.diff) 40 | - (cd /tmp/ros_buildfarm; patch -p1 < /tmp/890.diff) 41 | - (cd /tmp/ros_buildfarm; sed -i "/# After all dependencies are installed, update ccache symlinks./a @[if testing]@\nRUN rosdep init\n@[end if]@\n" ros_buildfarm/templates/devel/devel_task.Dockerfile.em) 42 | - (cd /tmp/ros_buildfarm; sed -i "/USER buildfarm/a @[if testing]@\nRUN rosdep update\n@[end if]@\n" ros_buildfarm/templates/devel/devel_task.Dockerfile.em) 43 | # revert https://github.com/ros/ros_comm/pull/1879, whcih create /tmp/rostest_bin_hook/python so did not fail with rostest 44 | - if [ "${ROS_DISTRO_NAME}" == "noetic" ]; then (cd /tmp/ros_buildfarm; sed -i "/^# After all dependencies are installed, update ccache symlinks./a @[if testing]@\nRUN apt-get install -qq -y wget patch\nRUN wget https://patch-diff.githubusercontent.com/raw/ros/ros_comm/pull/1879.diff -O /tmp/1879.diff\nRUN (cd /opt/ros/noetic/bin/; patch -R -p4 < /tmp/1879.diff )\n@[end if]@" ros_buildfarm/templates/devel/devel_task.Dockerfile.em); fi 45 | - (mkdir -p $JOB_PATH; cp -r /tmp/ros_buildfarm $JOB_PATH) # copy to prevent from git clone ros_buildfarm 46 | # 47 | - pip install /tmp/ros_buildfarm 48 | # checkout catkin for catkin_test_results script 49 | - git clone https://github.com/ros/catkin /tmp/catkin 50 | # run devel job for a ROS repository with the same name as this repo 51 | - export REPOSITORY_NAME=`basename $TRAVIS_BUILD_DIR` 52 | # use the code already checked out by Travis 53 | - mkdir -p $JOB_PATH/ws/src 54 | - cp -R $TRAVIS_BUILD_DIR $JOB_PATH/ws/src/ 55 | # generate the script to run a pre-release job for that target and repo 56 | - python /tmp/ros_buildfarm/scripts/prerelease/generate_prerelease_script.py $INDEX_URL $ROS_DISTRO_NAME default $OS_NAME $OS_CODE_NAME $ARCH --output-dir $JOB_PATH --custom-rosdep-update-options=--include-eol-distros 57 | # run the actual job which involves Docker 58 | - cd $JOB_PATH; sh ./prerelease.sh -y 59 | script: 60 | # get summary of test results 61 | - /tmp/catkin/bin/catkin_test_results $JOB_PATH/ws/test_results --all 62 | notifications: 63 | email: false 64 | 65 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 | Changelog for package app_manager 3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 | 5 | 1.3.0 (2021-11-08) 6 | ------------------ 7 | * update setuptools to follow noetic migration guide (`#36 `_) 8 | * app_manager cannot start app after failing app #42 (`#42 `_) 9 | 10 | * set current_app None when start failed 11 | * add test_start_fail.test 12 | * need catch error on _stop_current() 13 | * add test_start_fail.test 14 | test to check `#42 `_, app_manager cannot start app after failing app 15 | 16 | * add test to check if we forget catkin_install_python (`#44 `_) 17 | 18 | * call app_manager/appA with python2/python3 with ROS_PYTHON_VERSION 19 | * use catkin_install_python for noetic 20 | * add test to check if we forget to use catkin_install_python 21 | 22 | * add_rostest(test/test_plugin.test) (`#45 `_) 23 | 24 | * run rosdep install in devel_create_tasks.Dockerfile 25 | * update to format3 and install python-rosdep 26 | * use port 11313 for app_manager in test_plugin.test 27 | * add_rostest(test/test_plugin.test) 28 | 29 | * add more test code (`#41 `_ 30 | 31 | * show more error messages 32 | * default return value of plugin_order must be list 33 | * plugins: 'launch_args', 'plugin_args', 'start_plugin_args', 'stop_plugin_args' must be dict, not list 34 | * test_plugin: add test to check plugins 35 | * use list(self.subs.items()) instead of self.subs.items() 36 | * Error processing request: '>' not supported between instances of 'NoneType' and 'int' 37 | * python3: AttributeError: 'dict' object has no attribute 'iteritems' 38 | * add test for list_apps/stop_app, add test_stop_app.py 39 | * python3: AttributeError: 'dict' object has no attribute 'iterkeys' 40 | * add 2to3 in CHECK_PYTHON3_COMPILE 41 | * add test/test_app.test 42 | * test/resources/example-moin.launch: use arg launch_prefox to select if we use xterm or not 43 | 44 | * add arguments in StartAppRequest (`#27 `_) 45 | 46 | * use req.args for launch args in app_manager.py 47 | * add args in StartApp srv 48 | 49 | * do not run stop_app when _stopping is true (`#38 `_) 50 | * fix travis build (`#39 `_) 51 | 52 | * fix typo in .travis.yml 53 | * run with full path 54 | * add CHECK_PYTHON2_COMPILE and CHECK_PYTHON3_COMPILE tests 55 | 56 | * use plugins as instance / use normal method in app_manager_plugin (`#37 `_) 57 | * set stopped true in app timeout (`#31 `_) 58 | * use system python to check python3 compileall (`#34 `_) 59 | 60 | * Contributors: Kei Okada, Shingo Kitagawa 61 | 62 | 1.2.0 (2021-03-03) 63 | ------------------ 64 | * Merge pull request `#29 `_ from knorth55/add-stopped 65 | * Merge pull request `#28 `_ from knorth55/add-timeout 66 | * Merge pull request `#30 `_ from knorth55/add-all-availables 67 | add available_apps in all platform apps 68 | * add available_apps in all platform apps 69 | * add stopped context in app_manager plugin 70 | * add timeout field in app file 71 | * add enable_app_replacement param in app_manager (`#26 `_) 72 | This allows for both behaviors (replace currently running app or error out) and let's users choose without changing code... 73 | * add noetic test and also checks python 2/3 compatibility (`#24 `_) 74 | * add noetic test and also checks python 2/3 compatibility 75 | * fix to support both python 2/3 76 | * use rospy.log** instead of print 77 | Co-authored-by: Shingo Kitagawa 78 | * Add app manager plugin (`#25 `_) 79 | Enable aspect-oriented modelling, e.g. 80 | - send a mail when someone starts an app 81 | - auto-record rosbags during app run 82 | - auto-upload files on app-close 83 | ... 84 | * start plugin launch in app_manager 85 | * start and stop plugin function 86 | * pass app in plugin functions 87 | * add exit_code in stop_plugin_attr 88 | * stop plugin functions when shutdown is called 89 | * launch when plugin 90 | * load plugin launch when it exists 91 | * use ctx instead of exit_code 92 | * pass exit_code to ctx 93 | * add plugins in AppDefinition 94 | * add _current_plugins and _plugin_context 95 | * pass launch arguments 96 | * add plugin_args for app plugins 97 | * overwrite roslaunch.config.load_config_default for kinetic 98 | * add __stop_current for shutdown and __stop_current 99 | * support "module: null" syntax for app definition 100 | * add app_manager plugin base class 101 | * refactor scripts/app_manager 102 | * add start_plugin_args and stop_plugin_args 103 | * add start_plugin_arg_yaml and stop_plugin_arg_yaml 104 | * add launch_arg_yaml 105 | * add plugin_order to set plugin order 106 | * update readme to add plugin doc 107 | * update readme to add app definitions 108 | * fix readme (`#23 `_) 109 | * add exit_code log in app_manager (`#22 `_) 110 | add exit_code log in app_manager 111 | During successful execution, `dead_list` should always end up empty. 112 | * add all platform for all robots (`#17 `_) 113 | add an additional keyword to explicitly support 'all' platforms 114 | * use rospack to search for app_manager app_dir (`#19 `_) 115 | * use rospack to search for app_manager app_dir 116 | * remove unused imports 117 | * use both --applist and plugin app_dir 118 | * Merge pull request `#20 `_ from knorth55/fix-print-python3 119 | use rospy.logerr to escape print error in python3 120 | * use rospy.logerr to escape print error in python3 121 | * Contributors: Kei Okada, Michael Görner, Shingo Kitagawa 122 | 123 | 1.1.1 (2020-04-13) 124 | ------------------ 125 | * use python3.5 for travis (`#18 `_) 126 | * use app_manager/example-min (`#15 `_) 127 | 128 | * example-min.launch location changed due to https://github.com/PR2/app_manager/pull/15 129 | * use app_manager/example-min, since talker.py was removed from rospy in melodic https://github.com/ros/ros_comm/pull/1847 130 | 131 | * update document (`#14 `_) 132 | 133 | * Add more info for how to start apps, closes https://github.com/PR2/app_manager/issues/13 134 | 135 | * Fix travis (`#16 `_) 136 | 137 | * fix workspace name due to `ros-infrastructure/ros_buildfarm#577 `_ 138 | 139 | 140 | * install launch directory (`#9 `_) 141 | * add run_depend of app_manager in README.md (`#11 `_) 142 | * update travis.yml (`#10 `_) 143 | 144 | * add empty applist0 directory 145 | c.f. https://stackoverflow.com/questions/115983/how-can-i-add-an-empty-directory-to-a-git-repository 146 | * calling self._load() and updating self._file_mtime are never happens, since self.update() is removed in https://github.com/PR2/app_manager/pull/7/files#diff-a8d7b30ba0e424e10aa794dec1928181L98 147 | * revert code from `#7 `_, which wrongly removed invalid_installed_files 148 | * example-min.launch file has been moved to subdir since 2012 149 | https://github.com/ros/ros_comm/commit/964da45c6959bf9c2bde8680c69d1ab36e3770b1#diff-03b2e74d781fea8d7240c1fdd29a41a9 150 | 151 | * Contributors: Kei Okada, Shingo Kitagawa, Takayuki Murooka, Yuki Furuta 152 | 153 | 1.1.0 (2018-08-29) 154 | ------------------ 155 | * Support loading installed apps from export tags (`#7 `_) 156 | * app_manager: add reload_app_list service to dynamically reload apps 157 | * filter apps by robot platform 158 | * add support for loading app directories from plugins 159 | * Cleanup unused files (`#6 `_) 160 | * Contributors: Yuki Furuta 161 | 162 | 1.0.5 (2018-02-14) 163 | ------------------ 164 | * Merge pull request `#5 `_ from k-okada/orp 165 | change maintainer to ROS orphaned package maintainer 166 | * change maintainer to ROS orphaned package maintainer 167 | * Contributors: Kei Okada 168 | 169 | 1.0.3 (2015-02-06) 170 | ------------------ 171 | 172 | 1.0.2 (2014-10-14) 173 | ------------------ 174 | * changelogs 175 | * Fixed installs on app_manager 176 | * Contributors: TheDash 177 | 178 | * Fixed installs on app_manager 179 | * Contributors: TheDash 180 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8.3) 2 | project(app_manager) 3 | find_package(catkin REQUIRED COMPONENTS 4 | message_generation 5 | message_runtime 6 | rosgraph 7 | roslaunch 8 | rospy 9 | rosunit 10 | std_msgs 11 | ) 12 | 13 | catkin_python_setup() 14 | 15 | add_message_files( 16 | FILES 17 | AppList.msg 18 | ClientApp.msg 19 | AppInstallationState.msg 20 | App.msg 21 | KeyValue.msg 22 | AppStatus.msg 23 | ExchangeApp.msg 24 | Icon.msg 25 | StatusCodes.msg 26 | ) 27 | add_service_files( 28 | FILES 29 | GetAppDetails.srv 30 | ListApps.srv 31 | UninstallApp.srv 32 | InstallApp.srv 33 | GetInstallationState.srv 34 | StartApp.srv 35 | StopApp.srv 36 | ) 37 | 38 | 39 | generate_messages( 40 | DEPENDENCIES std_msgs 41 | ) 42 | 43 | catkin_package( 44 | CATKIN_DEPENDS rospy roslaunch rosgraph rosunit 45 | ) 46 | 47 | 48 | catkin_install_python(PROGRAMS bin/rosget bin/appmaster 49 | DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}) 50 | install(DIRECTORY launch test 51 | DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} 52 | USE_SOURCE_PERMISSIONS 53 | PATTERN *.py EXCLUDE) 54 | file(GLOB_RECURSE TEST_FILES 55 | RELATIVE "${PROJECT_SOURCE_DIR}" 56 | "test/*.py") 57 | foreach(TEST_FILE ${TEST_FILES}) 58 | get_filename_component(DIR ${TEST_FILE} DIRECTORY) 59 | catkin_install_python(PROGRAMS ${TEST_FILE} 60 | DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}/${DIR}) 61 | endforeach() 62 | file(GLOB SCRIPTS_FILES 63 | RELATIVE "${PROJECT_SOURCE_DIR}" 64 | "${PROJECT_SOURCE_DIR}/scripts/*") 65 | catkin_install_python(PROGRAMS ${SCRIPTS_FILES} 66 | DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}/scripts) 67 | 68 | if(CATKIN_ENABLE_TESTING) 69 | find_package(rostest) 70 | catkin_add_nosetests(test/test_app.py) 71 | catkin_add_nosetests(test/test_app_list.py) 72 | add_rostest(test/test_app.test) 73 | add_rostest(test/test_plugin.test) 74 | add_rostest(test/test_plugin_timeout.test) 75 | add_rostest(test/test_plugin_success.test) 76 | add_rostest(test/test_plugin_fail.test) 77 | add_rostest(test/test_start_fail.test) 78 | endif() 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | app_manager [![Build Status](https://travis-ci.com/PR2/app_manager.svg?branch=kinetic-devel)](https://travis-ci.org/PR2/app_manager) 2 | ==================================================================================================================================== 3 | 4 | A package for making launch file an application 5 | 6 | ## Installation 7 | 8 | Run `sudo apt-get install ros-$ROS_DISTRO-app-manager` 9 | 10 | ## Usage 11 | 12 | The `app_manager` node loads information of available application from `.installed` files. 13 | `.installed` file is a yaml file that defines installed applications in a package like below: 14 | 15 | ```yaml 16 | # package_root/apps/app.installed 17 | apps: 18 | - app: pkg_name/app_name1 19 | display: sample app 20 | - app: pkg_name/app_name2 21 | display: another sample app 22 | ``` 23 | 24 | Once `.installed` file is defined, you have to notify the location of the files to `app_manager` by either of two ways: 25 | 26 | 1. Give the locations as arguments 27 | 28 | One way to notify the location is to add `--applist` argument with `rosrun`. 29 | 30 | ```bash 31 | rosrun app_manager app_manager --applist `rospack find package_root`/apps 32 | ``` 33 | 34 | This is useful for testing one small `.installed` file or a demonstration. 35 | 36 | 2. Register as export attributes 37 | 38 | Another way to notify the location is to define them in `` tag in `package.xml`. 39 | 40 | ```xml 41 | 42 | 43 | ... 44 | app_manager 45 | ... 46 | 47 | 48 | 49 | 50 | ``` 51 | 52 | And launch `app_manager` without any argument: 53 | 54 | ```bash 55 | rosrun app_manager app_manager 56 | ``` 57 | 58 | `app_manager` node automatically searches all `.installed` files and register as available applications. 59 | 60 | Applications can be filtered by platform defined in each `.app` file. 61 | If you set the parameter `/robot/type` to `pr2`, then apps for platform `pr2` will be available. 62 | 63 | ```bash 64 | rosparam set /robot/type pr2 65 | ``` 66 | 67 | 68 | ## APIs 69 | 70 | All topics/services are advertised under the namespace specified by the parameter `/robot/name`. 71 | 72 | ### Publishing Topics 73 | 74 | - `app_list`: List available/running applications 75 | - `application/app_status`: Current status of app manager 76 | 77 | ### Services 78 | 79 | - `list_apps`: List available/running applications 80 | - `start_app`: Start an available application 81 | - `stop_app`: Stop a runniing application 82 | - `reload_app_list`: Reload installed applications from `*.installed`) file. 83 | 84 | 85 | ## Examples 86 | 87 | Start default roscore 88 | ``` 89 | $ roscore 90 | 91 | ``` 92 | 93 | and start another roscore for app_manager from another Terminal 94 | 95 | ``` 96 | $ roscore -p 11312 97 | ``` 98 | 99 | Start app_manager 100 | ``` 101 | $ rosrun app_manager app_manager --applist `rospack find app_manager`/test/applist1 _interface_master:=http://localhost:11312 102 | ``` 103 | Make sure that it founds the apps 104 | ``` 105 | [INFO] [1575604033.724035]: 1 apps found in /home/user/catkin_ws/src/app_manager/test/applist1/apps1.installed 106 | ``` 107 | 108 | Use service calls to list and start apps. 109 | 110 | ```bash 111 | $ rosservice call robot/list_apps 112 | running_apps: [] 113 | available_apps: 114 | - 115 | name: "app_manager/appA" 116 | display_name: "Android Joystick" 117 | icon: 118 | format: '' 119 | data: [] 120 | client_apps: [] 121 | 122 | $ rosservice call /robot/start_app "name: 'app_manager/appA' 123 | args: 124 | - key: 'foo' 125 | value: 'bar'" 126 | 127 | started: True 128 | error_code: 0 129 | message: "app [app_manager/appA] started" 130 | namespace: "/robot/application" 131 | ``` 132 | 133 | ## Plugins 134 | 135 | You can define `app_manager` plugins as below in app file such as `test.app`. 136 | 137 | ```yaml 138 | # app definitions 139 | display: Test app 140 | platform: all 141 | launch: test_app_manager/test_app.xml 142 | interface: test_app_manager/test_app.interface 143 | # plugin definitions 144 | plugins: 145 | - name: mail_notifier_plugin # name to identify this plugin 146 | type: app_notifier/mail_notifier_plugin # plugin type 147 | launch_args: # arguments for plugin launch file 148 | foo: hello 149 | launch_arg_yaml: /etc/mail_notifier_launch_arg.yaml # argument yaml file for plugin launch file 150 | # in this case, these arguments will be passed. 151 | # {"hoge": 100, "fuga": 30, "bar": 10} will be passed to start plugin 152 | # {"hoge": 50, "fuga": 30} will be passed to stop plugin 153 | plugin_args: # arguments for plugin function 154 | hoge: 10 155 | fuga: 30 156 | start_plugin_args: # arguments for start plugin function 157 | hoge: 100 # arguments for start plugin function arguments (it overwrites plugin_args hoge: 10 -> 100) 158 | bar: 10 159 | stop_plugin_args: # arguments for stop plugin function 160 | hoge: 50 # arguments for stop plugin function arguments (it overwrites plugin_args hoge: 10 -> 50) 161 | plugin_arg_yaml: /etc/mail_notifier_plugin_arg.yaml # argument yaml file for plugin function arguments 162 | - name: rosbag_recorder_plugin # another plugin 163 | type app_recorder/rosbag_recorder_plugin 164 | launch_args: 165 | rosbag_path: /tmp 166 | rosbag_title: test.bag 167 | compress: true 168 | rosbag_topic_names: 169 | - /rosout 170 | - /tf 171 | - /tf_static 172 | plugin_order: # plugin running orders. if you don't set field, plugin will be run in order in plugins field 173 | start_plugin_order: # start plugin running order 174 | - rosbag_recorder_plugin # 1st plugin name 175 | - mail_notifier_plugin #2nd plugin name 176 | stop_plugin_order: # start plugin running order 177 | - rosbag_recorder_plugin 178 | - mail_notifier_plugin 179 | ``` 180 | 181 | Sample plugin repository is [knorth55/app_manager_utils](https://github.com/knorth55/app_manager_utils). 182 | 183 | For more detailed information, please read [#25](https://github.com/PR2/app_manager/pull/25). 184 | 185 | ## Maintainer 186 | 187 | Yuki Furuta <> 188 | -------------------------------------------------------------------------------- /bin/appmaster: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Software License Agreement (BSD License) 3 | # 4 | # Copyright (c) 2008, Willow Garage, Inc. 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions 9 | # are met: 10 | # 11 | # * Redistributions of source code must retain the above copyright 12 | # notice, this list of conditions and the following disclaimer. 13 | # * Redistributions in binary form must reproduce the above 14 | # copyright notice, this list of conditions and the following 15 | # disclaimer in the documentation and/or other materials provided 16 | # with the distribution. 17 | # * Neither the name of Willow Garage, Inc. nor the names of its 18 | # contributors may be used to endorse or promote products derived 19 | # from this software without specific prior written permission. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 24 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 25 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 26 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 27 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 30 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 31 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 32 | # POSSIBILITY OF SUCH DAMAGE. 33 | 34 | import roslib; roslib.load_manifest('rosmaster') 35 | import rosmaster 36 | import sys 37 | rosmaster.rosmaster_main(argv=[a for a in sys.argv if not ':=' in a]) 38 | -------------------------------------------------------------------------------- /bin/rosget: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import subprocess 4 | import sys 5 | 6 | command = "" 7 | try: 8 | command = sys.argv[1] 9 | except: 10 | print ("No command specified, {} help".format(sys.argv[0])) 11 | sys.exit(1) 12 | 13 | cmd_args = [] 14 | 15 | def valid_package(n): 16 | for i in n: 17 | if ((ord(i) >= ord('a') and ord(i) <= ord('z')) 18 | or (ord(i) >= ord('A') and ord(i) <= ord('Z')) 19 | or (ord(i) >= ord('1') and ord(i) <= ord('9')) 20 | or i == '0' or i == '-' or i == '_'): 21 | pass #Character is valid 22 | else: 23 | print ("Bad character in name: {} in {}".format(i, n)) 24 | return False 25 | if (n[0:len("ros-")] != "ros-"): 26 | print ("You are not allowed to modifiy packages not prefixed with \"ros-\", rejecting: {}".format(n)) 27 | return False 28 | return True 29 | 30 | if (command == "update"): 31 | cmd_args = ["apt-get", "update"] 32 | elif (command == "install"): 33 | try: 34 | package = sys.argv[2] 35 | except: 36 | print ("Invalid package, try help as command") 37 | sys.exit(3) 38 | if (not valid_package(package)): 39 | print ("Invalid package name") 40 | sys.exit(4) 41 | else: 42 | cmd_args = ["apt-get", "install", package, "-y"] 43 | elif (command == "remove"): 44 | try: 45 | package= sys.argv[2] 46 | except: 47 | print ("Invalid package, try help as command") 48 | sys.exit(3) 49 | if (not valid_package(package)): 50 | print ("Invalid package name") 51 | sys.exit(4) 52 | else: 53 | cmd_args = ["apt-get", "remove", package, "-y"] 54 | elif (command == "help"): 55 | print ("{} is a wrapper for apt-get that only allows you to install ROS packages.".format(sys.argv[0])) 56 | print ("It is used by app manager and configured to run passwordlessly in sudo. You can use") 57 | print ("it to install and uninstall packages prefixed with \"ros-\". Usage:") 58 | print ("{} help: print this screen.".format(sys.argv[0])) 59 | print ("{} remove : remove a package".format(sys.argv[0])) 60 | print ("{} install : install a package".format(sys.argv[0])) 61 | print ("{} update: do an apt-get update".format(sys.argv[0])) 62 | sys.exit(2) 63 | else: 64 | print ("Invalid command, try: {} help".format(sys.argv[0])) 65 | sys.exit(2) 66 | 67 | 68 | proc = subprocess.Popen(cmd_args) 69 | 70 | 71 | print (proc.communicate()) 72 | 73 | sys.exit(proc.returncode) 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /launch/app_manager.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 28 | 31 | 32 | interface_master: http://$(arg master_address):$(arg master_port) 33 | enable_app_replacement: $(arg enable_app_replacement) 34 | enable_topic_remapping: $(arg enable_topic_remapping) 35 | sigint_timeout: $(arg sigint_timeout) 36 | sigterm_timeout: $(arg sigterm_timeout) 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /msg/App.msg: -------------------------------------------------------------------------------- 1 | # app name 2 | string name 3 | # user-friendly display name of application 4 | string display_name 5 | # icon for showing app 6 | Icon icon 7 | # ordered list (by preference) of client applications to interact with this robot app. 8 | ClientApp[] client_apps 9 | -------------------------------------------------------------------------------- /msg/AppInstallationState.msg: -------------------------------------------------------------------------------- 1 | ExchangeApp[] installed_apps 2 | ExchangeApp[] available_apps 3 | -------------------------------------------------------------------------------- /msg/AppList.msg: -------------------------------------------------------------------------------- 1 | App[] running_apps 2 | App[] available_apps 3 | -------------------------------------------------------------------------------- /msg/AppStatus.msg: -------------------------------------------------------------------------------- 1 | int32 INFO=0 2 | int32 WARN=1 3 | int32 ERROR=2 4 | # Status type. One of INFO, WARN, ERROR. 5 | int32 type 6 | # Status message. 7 | string status 8 | -------------------------------------------------------------------------------- /msg/ClientApp.msg: -------------------------------------------------------------------------------- 1 | # like "android" or "web" or "linux" 2 | string client_type 3 | 4 | # like "intent = ros.android.teleop" and "accelerometer = true", used to choose which ClientApp to use 5 | KeyValue[] manager_data 6 | 7 | # parameters which just get passed through to the client app. 8 | KeyValue[] app_data 9 | -------------------------------------------------------------------------------- /msg/ExchangeApp.msg: -------------------------------------------------------------------------------- 1 | # app name 2 | string name 3 | # user-friendly display name of application 4 | string display_name 5 | # the version of the package currently installed 6 | string version 7 | # latest version of the package avaliable 8 | string latest_version 9 | # the detailed description of the app 10 | string description 11 | # icon for showing app 12 | Icon icon 13 | # hidden apps are not show - used for cases where multiple apps are in a deb 14 | bool hidden -------------------------------------------------------------------------------- /msg/Icon.msg: -------------------------------------------------------------------------------- 1 | # Image data format. "jpeg" or "png" 2 | string format 3 | 4 | # Image data. 5 | uint8[] data 6 | -------------------------------------------------------------------------------- /msg/KeyValue.msg: -------------------------------------------------------------------------------- 1 | string key 2 | string value 3 | -------------------------------------------------------------------------------- /msg/StatusCodes.msg: -------------------------------------------------------------------------------- 1 | # Common error codes used with App Manager. 2 | int32 SUCCESS = 0 3 | # Request was invalid. 4 | int32 BAD_REQUEST = 400 5 | # App is not installed. 6 | int32 NOT_FOUND = 404 7 | # App is not running. 8 | int32 NOT_RUNNING = 430 9 | # Unknown internal error on the server. 10 | int32 INTERNAL_ERROR = 500 11 | # App is installed but failed validation. 12 | int32 APP_INVALID = 510 13 | # App manager does not support launching multiple apps simultaneously. Running app must first be stopped. 14 | int32 MULTIAPP_NOT_SUPPORTED = 511 15 | -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | app_manager 4 | 1.3.0 5 | app_manager 6 | ROS Orphaned Package Maintainers 7 | 8 | BSD 9 | 10 | http://ros.org/wiki/app_manager 11 | 12 | 13 | Jeremy Leibs 14 | Ken Conley 15 | Yuki Furuta 16 | 17 | catkin 18 | python-setuptools 19 | python3-setuptools 20 | 21 | rospy 22 | roslaunch 23 | rosgraph 24 | rosunit 25 | message_generation 26 | 27 | 28 | rospack 29 | rospy 30 | roslaunch 31 | rosgraph 32 | rosunit 33 | std_srvs 34 | message_runtime 35 | 36 | 37 | python-rosdep 38 | python3-rosdep 39 | rostest 40 | rosservice 41 | rospy_tutorials 42 | 43 | 44 | -------------------------------------------------------------------------------- /scripts/app_manager: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | """ 4 | usage: %prog [args] 5 | """ 6 | 7 | import rospkg 8 | 9 | from argparse import ArgumentParser 10 | import os 11 | import sys 12 | import yaml 13 | 14 | import app_manager 15 | import rospy 16 | 17 | 18 | def main(): 19 | rospy.init_node('app_manager') 20 | 21 | argv = rospy.myargv() 22 | parser = ArgumentParser() 23 | 24 | parser.add_argument("--applist", default=None, nargs="*", 25 | help="path to applist directories", metavar="PATH") 26 | args = parser.parse_args(argv[1:]) 27 | 28 | applist = [] 29 | plugin_yamls = [] 30 | rospack = rospkg.RosPack() 31 | depend_pkgs = rospack.get_depends_on('app_manager', implicit=False) 32 | 33 | rospy.loginfo("Loading from plugin definitions") 34 | for depend_pkg in depend_pkgs: 35 | manifest = rospack.get_manifest(depend_pkg) 36 | app_dirs = manifest.get_export('app_manager', 'app_dir') 37 | plugin_yaml = manifest.get_export('app_manager', 'plugin') 38 | if len(app_dirs) != 0: 39 | applist += app_dirs 40 | for app_dir in app_dirs: 41 | rospy.logdebug('Loading app in {}'.format(app_dir)) 42 | if len(plugin_yaml) != 0: 43 | plugin_yamls += plugin_yaml 44 | for plugin_y in plugin_yaml: 45 | rospy.logdebug('Loading plugin in {}'.format(plugin_y)) 46 | 47 | if args.applist is not None: 48 | rospy.loginfo("Loading applist from --applist option") 49 | for path in args.applist: 50 | if not os.path.exists(path): 51 | rospy.logerr( 52 | "applist directory {} does not exist." 53 | "Use --applist to set the correct location".format(path)) 54 | applist += args.applist 55 | 56 | if len(applist) == 0: 57 | rospy.logwarn('No applist directory found.') 58 | 59 | plugins = [] 60 | for plugin_yaml in plugin_yamls: 61 | with open(plugin_yaml) as f: 62 | plugin = yaml.safe_load(f) 63 | plugins += plugin 64 | 65 | robot_name = rospy.get_param('/robot/name', 'robot') 66 | robot_type = rospy.get_param("/robot/type", None) 67 | sigint_timeout = rospy.get_param("~sigint_timeout", 15.0) 68 | sigterm_timeout = rospy.get_param("~sigterm_timeout", 2.0) 69 | if robot_type is None: 70 | rospy.loginfo("The param '/robot/type' is undefined. Using apps for any platforms") 71 | else: 72 | rospy.loginfo("Using apps for platform '%s'" % robot_type) 73 | 74 | interface_master = rospy.get_param('~interface_master', 'http://localhost:11312') 75 | 76 | try: 77 | app_list = app_manager.AppList(applist, platform=robot_type) 78 | except app_manager.AppException as e: 79 | rospy.logerr("Failed to load app list: {}".format(e)) 80 | sys.exit(1) 81 | 82 | exchange = None 83 | 84 | exchange_url = rospy.get_param('/robot/exchange_url', '') 85 | if (exchange_url != ""): 86 | try: 87 | app_path = os.path.join(rospkg.get_ros_home(), "exchange") 88 | if (not os.path.exists(app_path)): 89 | os.makedirs(app_path) 90 | exchange = app_manager.Exchange(exchange_url, app_path, lambda x: rospy.logerr(x)) 91 | app_list.add_directory(os.path.join(app_path, "installed")) 92 | except app_manager.AppException as e: 93 | rospy.logerr("Failed to load exchange: {}".format(e)) 94 | sys.exit(1) 95 | 96 | enable_app_replacement = rospy.get_param('~enable_app_replacement', True) 97 | enable_topic_remapping = rospy.get_param('~enable_topic_remapping', True) 98 | 99 | am = app_manager.AppManager( 100 | robot_name, interface_master, app_list, exchange, plugins, 101 | enable_app_replacement=enable_app_replacement, 102 | enable_topic_remapping=enable_topic_remapping, 103 | sigint_timeout=sigint_timeout, sigterm_timeout=sigterm_timeout) 104 | 105 | rospy.on_shutdown(am.shutdown) 106 | 107 | rospy.spin() 108 | 109 | 110 | 111 | if __name__ == "__main__": 112 | main() 113 | -------------------------------------------------------------------------------- /scripts/test_app.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | """ 4 | usage: %prog [args] 5 | """ 6 | 7 | import roslib 8 | roslib.load_manifest('app_manager') 9 | import rospy 10 | 11 | import os, sys, string 12 | from optparse import OptionParser 13 | 14 | import app_manager 15 | 16 | def main(argv, stdout, environ): 17 | 18 | parser = OptionParser(__doc__.strip()) 19 | (options, args) = parser.parse_args() 20 | 21 | try: 22 | a = app_manager.App(args[0]) 23 | rospy.logwarn(a.yaml) 24 | except app_manager.TaskException as e: 25 | rospy.logerr(e) 26 | 27 | if __name__ == "__main__": 28 | main(sys.argv, sys.stdout, os.environ) 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | ## ! DO NOT MANUALLY INVOKE THIS setup.py, USE CATKIN INSTEAD 2 | 3 | from catkin_pkg.python_setup import generate_distutils_setup 4 | from setuptools import setup 5 | 6 | 7 | # fetch values from package.xml 8 | setup_args = generate_distutils_setup( 9 | packages=['app_manager'], 10 | package_dir={'': 'src'}) 11 | 12 | setup(**setup_args) 13 | -------------------------------------------------------------------------------- /src/app_manager/__init__.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2011, Willow Garage, Inc. 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions 8 | # are met: 9 | # 10 | # * Redistributions of source code must retain the above copyright 11 | # notice, this list of conditions and the following disclaimer. 12 | # * Redistributions in binary form must reproduce the above 13 | # copyright notice, this list of conditions and the following 14 | # disclaimer in the documentation and/or other materials provided 15 | # with the distribution. 16 | # * Neither the name of Willow Garage, Inc. nor the names of its 17 | # contributors may be used to endorse or promote products derived 18 | # from this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 23 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 30 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 | # POSSIBILITY OF SUCH DAMAGE. 32 | # 33 | # Revision $Id: __init__.py 14948 2011-09-07 19:25:54Z pratkanis $ 34 | 35 | from .app import AppDefinition 36 | from .app_manager import AppManager 37 | from .app_list import AppList, get_default_applist_directory 38 | from .app_manager_plugin import AppManagerPlugin 39 | from .exchange import Exchange 40 | from .exceptions import AppException, NotFoundException, \ 41 | InvalidAppException, LaunchException, InternalAppException 42 | -------------------------------------------------------------------------------- /src/app_manager/app.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2011, Willow Garage, Inc. 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions 8 | # are met: 9 | # 10 | # * Redistributions of source code must retain the above copyright 11 | # notice, this list of conditions and the following disclaimer. 12 | # * Redistributions in binary form must reproduce the above 13 | # copyright notice, this list of conditions and the following 14 | # disclaimer in the documentation and/or other materials provided 15 | # with the distribution. 16 | # * Neither the name of Willow Garage, Inc. nor the names of its 17 | # contributors may be used to endorse or promote products derived 18 | # from this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 23 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 30 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 | # POSSIBILITY OF SUCH DAMAGE. 32 | # 33 | # Revision $Id: app.py 14667 2011-08-12 23:55:04Z pratkanis $ 34 | 35 | # author: leibs, kwc 36 | 37 | import os 38 | import errno 39 | import yaml 40 | 41 | import roslaunch 42 | import roslib.names 43 | import rospkg 44 | from rospkg import ResourceNotFound 45 | from .exceptions import AppException, InvalidAppException, NotFoundException, InternalAppException 46 | 47 | class Interface(object): 48 | def __init__(self, subscribed_topics, published_topics): 49 | self.subscribed_topics = subscribed_topics 50 | self.published_topics = published_topics 51 | 52 | def __eq__(self, other): 53 | if not isinstance(other, Interface): 54 | return False 55 | return self.subscribed_topics == other.subscribed_topics and \ 56 | self.published_topics == other.published_topics 57 | 58 | class Client(object): 59 | __slots__ = ['client_type', 'manager_data', 'app_data'] 60 | def __init__(self, client_type, manager_data, app_data): 61 | self.client_type = client_type 62 | self.manager_data = manager_data 63 | self.app_data = app_data 64 | 65 | def as_dict(self): 66 | return {'client_type': self.client_type, 'manager_data': self.manager_data, 'app_data': self.app_data} 67 | 68 | def __eq__(self, other): 69 | if not isinstance(other, Client): 70 | return False 71 | return self.client_type == other.client_type and \ 72 | self.manager_data == other.manager_data and \ 73 | self.app_data == other.app_data 74 | 75 | def __repr__(self): 76 | return yaml.dump(self.as_dict()) 77 | 78 | class AppDefinition(object): 79 | __slots__ = ['name', 'display_name', 'description', 'platform', 80 | 'launch', 'run', 'interface', 'clients', 'icon', 'plugins', 'plugin_order', 81 | 'timeout', 'allow_parallel'] 82 | def __init__(self, name, display_name, description, platform, 83 | interface, clients, launch=None, run=None, icon=None, plugins=None, plugin_order=None, 84 | timeout=None, allow_parallel=True): 85 | self.name = name 86 | self.display_name = display_name 87 | self.description = description 88 | self.platform=platform 89 | self.interface = interface 90 | self.clients = clients 91 | self.launch = launch 92 | self.run = run 93 | self.icon = icon 94 | self.plugins = plugins 95 | self.plugin_order = plugin_order 96 | self.timeout = timeout 97 | self.allow_parallel = allow_parallel 98 | 99 | def __repr__(self): 100 | d = {} 101 | for s in self.__slots__: 102 | if s == 'clients': 103 | d[s] = [c.as_dict() for c in self.clients] 104 | else: 105 | d[s] = getattr(self, s) 106 | return yaml.dump(d) 107 | # return "name: %s\ndisplay: %s\ndescription: %s\nplatform: %s\nlaunch: %s\ninterface: %s\nclients: %s"%(self.name, self.display_name, self.description, self.platform, self.launch, self.interface, self.clients) 108 | 109 | def __eq__(self, other): 110 | if not isinstance(other, AppDefinition): 111 | return False 112 | return self.name == other.name and \ 113 | self.display_name == other.display_name and \ 114 | self.description == other.description and \ 115 | self.platform == other.platform and \ 116 | self.interface == other.interface and \ 117 | self.clients == other.clients and \ 118 | self.launch == other.launch and \ 119 | self.run == other.run and \ 120 | self.icon == other.icon 121 | 122 | def find_resource(resource, rospack=None): 123 | """ 124 | @return: filepath of resource. Does not validate if filepath actually exists. 125 | 126 | @raise ValueError: if resource is not a valid resource name. 127 | @raise rospkg.ResourceNotFound: if package referred 128 | to in resource name cannot be found. 129 | @raise NotFoundException: if resource does not exist. 130 | """ 131 | p, a = roslib.names.package_resource_name(resource) 132 | if not p: 133 | raise ValueError("Resource is missing package name: %s"%(resource)) 134 | 135 | if rospack is None: 136 | rospack = rospkg.RosPack() 137 | matches = roslib.packages.find_resource(p, a, rospack=rospack) 138 | 139 | # TODO: convert ValueError to better type for better error messages 140 | if len(matches) == 1: 141 | return matches[0] 142 | elif not matches: 143 | raise NotFoundException("No resource [%s]"%(resource)) 144 | else: 145 | raise ValueError("Multiple resources named [%s]"%(resource)) 146 | 147 | def load_Interface_from_file(filename): 148 | """ 149 | @raise IOError: I/O error reading file (e.g. does not exist) 150 | @raise InvalidAppException: if app file is invalid 151 | """ 152 | with open(filename,'r') as f: 153 | y = yaml.load(f.read()) 154 | y = y or {} #coerce to dict 155 | try: 156 | subscribed_topics = y.get('subscribed_topics', {}) 157 | published_topics = y.get('published_topics', {}) 158 | except KeyError: 159 | raise InvalidAppException("Malformed interface, missing keys") 160 | return Interface(published_topics=published_topics, subscribed_topics=subscribed_topics) 161 | 162 | def _AppDefinition_load_icon_entry(app_data, appfile="UNKNOWN", rospack=None): 163 | """ 164 | @raise InvalidAppExcetion: if app definition is invalid. 165 | """ 166 | 167 | if rospack is None: 168 | rospack = rospkg.RosPack() 169 | # load/validate launch entry 170 | try: 171 | icon_resource = app_data.get('icon', '') 172 | if icon_resource == '': 173 | return None 174 | icon_filename = find_resource(icon_resource, rospack=rospack) 175 | if not icon_filename or not os.path.exists(icon_filename): 176 | return None 177 | return icon_filename 178 | except ValueError as e: 179 | raise InvalidAppException("Malformed appfile [%s]: bad icon entry: %s"%(appfile, e)) 180 | except NotFoundException: 181 | # TODO: make this a soft fail? 182 | raise InvalidAppException("App file [%s] refers to icon that cannot be found"%(appfile)) 183 | except ResourceNotFound as e: 184 | raise InvalidAppException("App file [%s] refers to package that is not installed: %s"%(appfile, str(e))) 185 | 186 | def _AppDefinition_load_launch_entry(app_data, appfile="UNKNOWN", rospack=None): 187 | """ 188 | @raise InvalidAppExcetion: if app definition is invalid. 189 | """ 190 | # load/validate launch entry 191 | if rospack is None: 192 | rospack = rospkg.RosPack() 193 | try: 194 | launch_resource = app_data.get('launch', '') 195 | if launch_resource == '': 196 | return None 197 | launch = find_resource(launch_resource, rospack=rospack) 198 | if not os.path.exists(launch): 199 | raise InvalidAppException("Malformed appfile [%s]: refers to launch that does not exist."%(appfile)) 200 | return launch 201 | except ValueError as e: 202 | raise InvalidAppException("Malformed appfile [%s]: bad launch entry: %s"%(appfile, e)) 203 | except NotFoundException: 204 | raise InvalidAppException("App file [%s] refers to launch that is not installed"%(appfile)) 205 | except ResourceNotFound as e: 206 | raise InvalidAppException("App file [%s] refers to package that is not installed: %s"%(appfile, str(e))) 207 | 208 | def _AppDefinition_load_run_args_entry(app_data, appfile="UNKNOWN"): 209 | """ 210 | @raise InvalidAppException: if app definition is invalid. 211 | """ 212 | # load/validate launch entry 213 | try: 214 | run_args = app_data.get('run_args', '') 215 | if run_args == '': 216 | return None 217 | return run_args 218 | except ValueError as e: 219 | raise InvalidAppException("Malformed appfile [%s]: bad run_args entry: %s"%(appfile, e)) 220 | 221 | def _AppDefinition_load_run_entry(app_data, appfile="UNKNOWN", rospack=None): 222 | """ 223 | @raise InvalidAppExcetion: if app definition is invalid. 224 | """ 225 | # load/validate run entry 226 | if rospack is None: 227 | rospack = rospkg.RosPack() 228 | try: 229 | run_resource = app_data.get('run', '') 230 | if run_resource == '': 231 | return None 232 | 233 | # check if file exists 234 | run = find_resource(run_resource, rospack=rospack) 235 | if not os.path.exists(run): 236 | raise InvalidAppException("Malformed appfile [%s]: refers to run that does not exist."%(appfile)) 237 | # create node 238 | p, a = roslib.names.package_resource_name(run_resource) 239 | args = _AppDefinition_load_run_args_entry(app_data, appfile) 240 | node = roslaunch.core.Node(p, a, args=args, output='screen') 241 | return node 242 | except ValueError as e: 243 | raise InvalidAppException("Malformed appfile [%s]: bad run entry: %s"%(appfile, e)) 244 | except NotFoundException: 245 | raise InvalidAppException("App file [%s] refers to run that is not installed"%(appfile)) 246 | except ResourceNotFound as e: 247 | raise InvalidAppException("App file [%s] refers to package that is not installed: %s"%(appfile, str(e))) 248 | 249 | def _AppDefinition_load_interface_entry(app_data, appfile="UNKNOWN", rospack=None): 250 | """ 251 | @raise InvalidAppExcetion: if app definition is invalid. 252 | """ 253 | # load/validate interface entry 254 | if rospack is None: 255 | rospack = rospkg.RosPack() 256 | try: 257 | return load_Interface_from_file( 258 | find_resource(app_data['interface'], rospack=rospack)) 259 | except IOError as e: 260 | if e.errno == errno.ENOENT: 261 | raise InvalidAppException("Malformed appfile [%s]: refers to interface file that does not exist"%(appfile)) 262 | else: 263 | raise InvalidAppException("Error with appfile [%s]: cannot read interface file"%(appfile)) 264 | except ValueError: 265 | raise InvalidAppException("Malformed appfile [%s]: bad interface entry"%(appfile)) 266 | except ResourceNotFound as e: 267 | raise InvalidAppException("App file [%s] refers to package that is not installed: %s"%(appfile, str(e))) 268 | 269 | def _AppDefinition_load_clients_entry(app_data, appfile="UNKNOWN"): 270 | """ 271 | @raise InvalidAppExcetion: if app definition is invalid. 272 | """ 273 | clients_data = app_data.get('clients', []) 274 | clients = [] 275 | for c in clients_data: 276 | for reqd in ['type', 'manager']: 277 | if not reqd in c: 278 | raise InvalidAppException("Malformed appfile [%s], missing required key [%s]"%(appfile, reqd)) 279 | client_type = c['type'] 280 | manager_data = c['manager'] 281 | if not type(manager_data) == dict: 282 | raise InvalidAppException("Malformed appfile [%s]: manager data must be a map"%(appfile)) 283 | 284 | app_data = c.get('app', {}) 285 | if not type(app_data) == dict: 286 | raise InvalidAppException("Malformed appfile [%s]: app data must be a map"%(appfile)) 287 | 288 | clients.append(Client(client_type, manager_data, app_data)) 289 | return clients 290 | 291 | 292 | def _AppDefinition_load_plugins_entry(app_data, appfile="UNKNOWN"): 293 | """ 294 | @raise InvalidAppException: if app definition is invalid. 295 | """ 296 | # load/validate launch entry 297 | try: 298 | plugins = app_data.get('plugins', '') 299 | if plugins == '': 300 | return None 301 | for plugin in plugins: 302 | for key in ['launch_args', 'plugin_args', 'start_plugin_args', 'stop_plugin_args']: 303 | if key in plugin and not type(plugin[key]) == dict: 304 | raise InvalidAppException("Malformed appfile [%s]: plugin data(%s) must be a map"%(appfile, key)) 305 | return plugins 306 | except ValueError as e: 307 | raise InvalidAppException("Malformed appfile [%s]: bad plugins entry: %s"%(appfile, e)) 308 | 309 | 310 | def _AppDefinition_load_plugin_order_entry(app_data, appfile="UNKNOWN"): 311 | """ 312 | @raise InvalidAppException: if app definition is invalid. 313 | """ 314 | # load/validate launch entry 315 | try: 316 | plugin_order = app_data.get('plugin_order', '') 317 | if plugin_order == '': 318 | return [] 319 | return plugin_order 320 | except ValueError as e: 321 | raise InvalidAppException("Malformed appfile [%s]: bad plugin_order entry: %s"%(appfile, e)) 322 | 323 | 324 | def _AppDefinition_load_timeout_entry(app_data, appfile="UNKNOWN"): 325 | """ 326 | @raise InvalidAppException: if app definition is invalid. 327 | """ 328 | # load/validate launch entry 329 | try: 330 | timeout = app_data.get('timeout', '') 331 | if timeout == '': 332 | return None 333 | return timeout 334 | except ValueError as e: 335 | raise InvalidAppException("Malformed appfile [%s]: bad timeout entry: %s"%(appfile, e)) 336 | 337 | 338 | def _AppDefinition_load_allow_parallel_entry(app_data, appfile="UNKNOWN"): 339 | """ 340 | @raise InvalidAppException: if app definition is invalid. 341 | """ 342 | # load/validate launch entry 343 | try: 344 | allow_parallel = app_data.get('allow_parallel', '') 345 | if allow_parallel == '': 346 | return True 347 | return allow_parallel 348 | except ValueError as e: 349 | raise InvalidAppException("Malformed appfile [%s]: bad allow_parallel entry: %s"%(appfile, e)) 350 | 351 | 352 | def load_AppDefinition_from_file(appfile, appname, rospack=None): 353 | """ 354 | @raise InvalidAppExcetion: if app definition is invalid. 355 | @raise IOError: I/O error reading appfile (e.g. file does not exist). 356 | """ 357 | with open(appfile,'r') as f: 358 | app_data = yaml.load(f.read()) 359 | for reqd in ['interface', 'platform']: 360 | if not reqd in app_data: 361 | raise InvalidAppException("Malformed appfile [%s], missing required key [%s]"%(appfile, reqd)) 362 | if not 'launch' in app_data and not 'run' in app_data: 363 | raise InvalidAppException("Malformed appfile [%s], must have a [launch] or a [run] key"%(appfile)) 364 | if 'launch' in app_data and 'run' in app_data: 365 | raise InvalidAppException("Malformed appfile [%s], cannot have both [launch] and [run] keys"%(appfile)) 366 | 367 | display_name = app_data.get('display', appname) 368 | description = app_data.get('description', '') 369 | platform = app_data['platform'] 370 | 371 | if rospack is None: 372 | rospack = rospkg.RosPack() 373 | launch = _AppDefinition_load_launch_entry( 374 | app_data, appfile, rospack=rospack) 375 | run = _AppDefinition_load_run_entry( 376 | app_data, appfile, rospack=rospack) 377 | interface = _AppDefinition_load_interface_entry( 378 | app_data, appfile, rospack=rospack) 379 | clients = _AppDefinition_load_clients_entry(app_data, appfile) 380 | icon = _AppDefinition_load_icon_entry( 381 | app_data, appfile, rospack=rospack) 382 | plugins = _AppDefinition_load_plugins_entry(app_data, appfile) 383 | plugin_order = _AppDefinition_load_plugin_order_entry(app_data, appfile) 384 | timeout = _AppDefinition_load_timeout_entry(app_data, appfile) 385 | allow_parallel = _AppDefinition_load_allow_parallel_entry( 386 | app_data, appfile) 387 | 388 | return AppDefinition(appname, display_name, description, platform, 389 | interface, clients, launch, run, icon, 390 | plugins, plugin_order, timeout, allow_parallel) 391 | 392 | def load_AppDefinition_by_name(appname, rospack=None): 393 | """ 394 | @raise InvalidAppExcetion: if app definition is invalid. 395 | @raise NotFoundExcetion: if app definition is not installed. 396 | @raise ValueError: if appname is invalid. 397 | """ 398 | 399 | if not appname: 400 | raise ValueError("app name is empty") 401 | 402 | if rospack is None: 403 | rospack = rospkg.RosPack() 404 | try: 405 | appfile = find_resource(appname + '.app', rospack=rospack) 406 | except ResourceNotFound as e: 407 | raise NotFoundException("Cannot locate app file for %s: package is not installed."%(appname)) 408 | 409 | try: 410 | return load_AppDefinition_from_file(appfile, appname, rospack=rospack) 411 | except IOError as e: 412 | if e.errno == errno.ENOENT: 413 | raise NotFoundException("Cannot locate app file for %s."%(appname)) 414 | else: 415 | raise InternalAppException("I/O error loading AppDefinition file: %s."%(e.errno)) 416 | -------------------------------------------------------------------------------- /src/app_manager/app_list.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2011, Willow Garage, Inc. 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions 8 | # are met: 9 | # 10 | # * Redistributions of source code must retain the above copyright 11 | # notice, this list of conditions and the following disclaimer. 12 | # * Redistributions in binary form must reproduce the above 13 | # copyright notice, this list of conditions and the following 14 | # disclaimer in the documentation and/or other materials provided 15 | # with the distribution. 16 | # * Neither the name of Willow Garage, Inc. nor the names of its 17 | # contributors may be used to endorse or promote products derived 18 | # from this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 23 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 30 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 | # POSSIBILITY OF SUCH DAMAGE. 32 | # 33 | # Revision $Id: topics.py 11753 2010-10-25 06:23:19Z kwc $ 34 | 35 | # author: kwc 36 | 37 | """ 38 | Implements applist part of app_manager, which handles listing of 39 | currently installed applications. 40 | """ 41 | 42 | import os 43 | import rospy 44 | import sys 45 | import yaml 46 | 47 | import rospkg 48 | 49 | from .app import load_AppDefinition_by_name 50 | from .msg import App, ClientApp, KeyValue, Icon 51 | from .exceptions import AppException, InvalidAppException, NotFoundException 52 | 53 | def get_default_applist_directory(): 54 | """ 55 | Default directory where applist configuration is stored. 56 | """ 57 | return "/etc/robot/apps" 58 | 59 | def dict_to_KeyValue(d): 60 | l = [] 61 | for k, v in d.items(): 62 | l.append(KeyValue(k, str(v))) 63 | return l 64 | 65 | def read_Icon_file(filename): 66 | icon = Icon() 67 | if filename == None or filename == "": 68 | return icon 69 | basename, extension = os.path.splitext(filename) 70 | if extension.lower() == ".jpg" or extension.lower() == ".jpeg": 71 | icon.format = "jpeg" 72 | elif extension.lower() == ".png": 73 | icon.format = "png" 74 | else: 75 | icon.format = "" 76 | return icon 77 | icon.data = open(filename, "rb").read() 78 | return icon 79 | 80 | def AppDefinition_to_App(app_definition): 81 | a = App(name=app_definition.name, display_name=app_definition.display_name, icon=read_Icon_file(app_definition.icon)) 82 | a.client_apps = [] 83 | for c in app_definition.clients: 84 | a.client_apps.append(ClientApp(c.client_type, 85 | dict_to_KeyValue(c.manager_data), 86 | dict_to_KeyValue(c.app_data))) 87 | return a 88 | 89 | class InstalledFile(object): 90 | """ 91 | Models data stored in a .installed file. These files are used to 92 | track installation of apps. 93 | """ 94 | 95 | def __init__(self, filename): 96 | self.filename = filename 97 | # list of App 98 | self.available_apps = [] 99 | 100 | self._file_mtime = None 101 | self.update() 102 | 103 | def _load(self): 104 | available_apps = [] 105 | rospack = rospkg.RosPack() 106 | with open(self.filename) as f: 107 | installed_data = yaml.load(f) 108 | for reqd in ['apps']: 109 | if not reqd in installed_data: 110 | raise InvalidAppException("installed file [%s] is missing required key [%s]"%(self.filename, reqd)) 111 | for app in installed_data['apps']: 112 | for areqd in ['app']: 113 | if not areqd in app: 114 | raise InvalidAppException("installed file [%s] app definition is missing required key [%s]"%(self.filename, areqd)) 115 | try: 116 | available_apps.append( 117 | load_AppDefinition_by_name(app['app'], rospack=rospack)) 118 | except NotFoundException as e: 119 | rospy.logerr(e) 120 | continue 121 | except Exception as e: 122 | raise e 123 | 124 | self.available_apps = available_apps 125 | 126 | def update(self): 127 | """ 128 | Update app list 129 | """ 130 | s = os.stat(self.filename) 131 | if s.st_mtime != self._file_mtime: 132 | self._load() 133 | self._file_mtime = s.st_mtime 134 | 135 | def get_available_apps(self, platform=None): 136 | if platform is not None: 137 | available_apps = filter( 138 | lambda app: app.platform in [platform, 'all'], 139 | self.available_apps) 140 | return available_apps 141 | else: 142 | return self.available_apps 143 | 144 | def __eq__(self, other): 145 | return self.filename == other.filename 146 | 147 | def __neq__(self, other): 148 | return not self.__eq__(other) 149 | 150 | 151 | class AppList(object): 152 | def __init__(self, applist_directories, platform=None): 153 | self.applist_directories = applist_directories 154 | self.installed_files = {} 155 | self.invalid_installed_files = [] 156 | self.app_list = [] 157 | self.platform = platform 158 | 159 | self._applist_directory_mtime = None 160 | self.need_update = True 161 | 162 | def _find_installed_files(self): 163 | installed_files = [] 164 | for d in self.applist_directories: 165 | for f in os.listdir(d): 166 | if f.endswith(".installed"): 167 | full_path = os.path.abspath(os.path.join(d, f)) 168 | installed_files.append(full_path) 169 | return installed_files 170 | 171 | def _load(self, files): 172 | if files: 173 | installed_files = files 174 | else: 175 | installed_files = self.installed_files.keys() 176 | invalid_installed_files = [] 177 | app_list = [] 178 | for f in installed_files: 179 | try: 180 | if f in self.installed_files: 181 | # update InstalledFile object 182 | installed_file = self.installed_files[f] 183 | else: 184 | # new installed file 185 | installed_file = InstalledFile(f) 186 | self.installed_files[f] = installed_file 187 | installed_file.update() 188 | app_list.extend(installed_file.get_available_apps(platform=self.platform)) 189 | rospy.loginfo("%d apps found in %s" % (len(installed_file.available_apps), installed_file.filename)) 190 | except AppException as e: 191 | rospy.logerr("ERROR: %s" % (str(e))) 192 | invalid_installed_files.append((f, e)) 193 | except Exception as e: 194 | rospy.logerr("ERROR: %s" % (str(e))) 195 | invalid_installed_files.append((f, e)) 196 | 197 | self.app_list = app_list 198 | self.invalid_installed_files = invalid_installed_files 199 | 200 | def get_app_list(self): 201 | if not self.app_list: 202 | self.update() 203 | return [AppDefinition_to_App(ad) for ad in self.app_list] 204 | 205 | def get_app(self, name): 206 | for app in self.app_list: 207 | if app.name == name: 208 | return app 209 | return None 210 | 211 | def add_directory(self, directory): 212 | if not os.path.exists(directory): 213 | raise IOError("applist directory %s does not exist." % directory) 214 | if directory in self.applist_directory: 215 | raise RuntimeError("applist directory %s already exists" % directory) 216 | self.applist_directories.append(directory) 217 | self.need_update = True 218 | 219 | def remove_directory(self, directory): 220 | if directory not in self.applist_directory: 221 | raise RuntimeError("applist directory %s does not in list" % directory) 222 | self.applist_directory.remove(directory) 223 | self.need_update = True 224 | 225 | def update(self): 226 | """ 227 | Update app list 228 | """ 229 | files = None 230 | if self.need_update: 231 | files = self._find_installed_files() 232 | self.need_update = False 233 | self._load(files) 234 | -------------------------------------------------------------------------------- /src/app_manager/app_manager.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2011, Willow Garage, Inc. 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions 8 | # are met: 9 | # 10 | # * Redistributions of source code must retain the above copyright 11 | # notice, this list of conditions and the following disclaimer. 12 | # * Redistributions in binary form must reproduce the above 13 | # copyright notice, this list of conditions and the following 14 | # disclaimer in the documentation and/or other materials provided 15 | # with the distribution. 16 | # * Neither the name of Willow Garage, Inc. nor the names of its 17 | # contributors may be used to endorse or promote products derived 18 | # from this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 23 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 30 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 | # POSSIBILITY OF SUCH DAMAGE. 32 | # 33 | # Revision $Id: app_manager.py 14948 2011-09-07 19:25:54Z pratkanis $ 34 | 35 | # author: leibs 36 | import sys 37 | import os 38 | 39 | if sys.version_info[0] == 3: 40 | import _thread as thread # python3 renamed from thread to _thread 41 | else: 42 | import thread 43 | 44 | import time 45 | import yaml 46 | 47 | import traceback 48 | 49 | import rosgraph.names 50 | import rospy 51 | import roslib 52 | 53 | import roslaunch.config 54 | import roslaunch.core 55 | import roslaunch.parent 56 | import roslaunch.pmon 57 | import roslaunch.xmlloader 58 | 59 | import roslaunch.loader 60 | from std_srvs.srv import Empty, EmptyResponse 61 | 62 | from .app import AppDefinition, find_resource, load_AppDefinition_by_name 63 | from .exceptions import LaunchException, AppException, InvalidAppException, NotFoundException 64 | from .master_sync import MasterSync 65 | from .msg import App, AppList, StatusCodes, AppStatus, AppInstallationState, ExchangeApp 66 | from .srv import StartApp, StopApp, ListApps, ListAppsResponse, StartAppResponse, StopAppResponse, InstallApp, UninstallApp, GetInstallationState, UninstallAppResponse, InstallAppResponse, GetInstallationStateResponse, GetAppDetails, GetAppDetailsResponse 67 | 68 | # for profiling 69 | # import cProfile, pstats 70 | # from io import BytesIO as StringIO 71 | 72 | def _load_config_default( 73 | roslaunch_files, port, roslaunch_strs=None, loader=None, verbose=False, 74 | assign_machines=True, ignore_unset_args=False 75 | ): 76 | config = roslaunch.config.ROSLaunchConfig() 77 | if port: 78 | config.master.uri = rosgraph.network.create_local_xmlrpc_uri(port) 79 | 80 | loader = loader or roslaunch.xmlloader.XmlLoader() 81 | loader.ignore_unset_args = ignore_unset_args 82 | 83 | # load the roscore file first. we currently have 84 | # last-declaration wins rules. roscore is just a 85 | # roslaunch file with special load semantics 86 | roslaunch.config.load_roscore(loader, config, verbose=verbose) 87 | 88 | # load the roslaunch_files into the config 89 | for f in roslaunch_files: 90 | if isinstance(f, tuple): 91 | f, args = f 92 | else: 93 | args = None 94 | try: 95 | rospy.loginfo('loading config file %s' % f) 96 | loader.load(f, config, argv=args, verbose=verbose) 97 | except roslaunch.xmlloader.XmlParseException as e: 98 | raise roslaunch.core.RLException(e) 99 | except roslaunch.loader.LoadException as e: 100 | raise roslaunch.core.RLException(e) 101 | # we need this for the hardware test systems, which builds up 102 | # roslaunch launch files in memory 103 | if roslaunch_strs: 104 | for launch_str in roslaunch_strs: 105 | try: 106 | rospy.loginfo('loading config file from string') 107 | loader.load_string(launch_str, config) 108 | except roslaunch.xmlloader.XmlParseException as e: 109 | raise roslaunch.core.RLException( 110 | 'Launch string: %s\nException: %s' % (launch_str, e)) 111 | except roslaunch.loader.LoadException as e: 112 | raise roslaunch.core.RLException( 113 | 'Launch string: %s\nException: %s' % (launch_str, e)) 114 | # choose machines for the nodes 115 | if assign_machines: 116 | config.assign_machines() 117 | return config 118 | 119 | 120 | # overwrite load_config_default function for kinetic 121 | # see: https://github.com/ros/ros_comm/pull/1115 122 | roslaunch.config.load_config_default = _load_config_default 123 | 124 | 125 | class AppManager(object): 126 | 127 | def __init__( 128 | self, robot_name, interface_master, app_list, 129 | exchange, plugins=None, enable_app_replacement=True, 130 | enable_topic_remapping=True, 131 | sigint_timeout=15.0, sigterm_timeout=2.0, 132 | ): 133 | self._robot_name = robot_name 134 | self._interface_master = interface_master 135 | self._app_list = app_list 136 | self._current_app = self._current_app_definition = None 137 | self._exchange = exchange 138 | self._plugins = plugins 139 | self._enable_app_replacement = enable_app_replacement 140 | self._enable_topic_remapping = enable_topic_remapping 141 | self._sigint_timeout = sigint_timeout 142 | self._sigterm_timeout = sigterm_timeout 143 | 144 | rospy.loginfo("Starting app manager for %s"%self._robot_name) 145 | 146 | self._app_interface = self.scoped_name('application') 147 | 148 | # note: we publish into the application namespace 149 | self._status_pub = rospy.Publisher( 150 | self.scoped_name('application/app_status'), AppStatus, 151 | latch=True, queue_size=1) 152 | self._list_apps_pub = rospy.Publisher( 153 | self.scoped_name('app_list'), AppList, 154 | latch=True, queue_size=1) 155 | 156 | self._list_apps_srv = rospy.Service(self.scoped_name('list_apps'), ListApps, self.handle_list_apps) 157 | self._start_app_srv = rospy.Service(self.scoped_name('start_app'), StartApp, self.handle_start_app) 158 | self._stop_app_srv = rospy.Service(self.scoped_name('stop_app'), StopApp, self.handle_stop_app) 159 | self._reload_app_list_srv = rospy.Service(self.scoped_name('reload_app_list'), Empty, self.handle_reload_app_list) 160 | if (self._exchange): 161 | self._exchange_list_apps_pub = rospy.Publisher(self.scoped_name('exchange_app_list'), AppInstallationState, latch=True) 162 | self._list_exchange_apps_srv = rospy.Service(self.scoped_name('list_exchange_apps'), GetInstallationState, self.handle_list_exchange_apps) 163 | self._get_app_details_srv = rospy.Service(self.scoped_name('get_app_details'), GetAppDetails, self.handle_get_app_details) 164 | self._install_app_srv = rospy.Service(self.scoped_name('install_app'), InstallApp, self.handle_install_app) 165 | self._uninstall_app_srv = rospy.Service(self.scoped_name('uninstall_app'), UninstallApp, self.handle_uninstall_app) 166 | 167 | pub_names = [x.resolved_name for x in [self._list_apps_pub, self._status_pub, self._exchange_list_apps_pub]] 168 | service_names = [x.resolved_name for x in [self._list_apps_srv, self._start_app_srv, self._stop_app_srv, self._get_app_details_srv, self._list_exchange_apps_srv, self._install_app_srv, self._uninstall_app_srv]] 169 | else: 170 | pub_names = [x.resolved_name for x in [self._list_apps_pub, self._status_pub]] 171 | service_names = [x.resolved_name for x in [self._list_apps_srv, self._start_app_srv, self._stop_app_srv]] 172 | 173 | self._api_sync = MasterSync(self._interface_master, 174 | local_service_names=service_names, 175 | local_pub_names=pub_names) 176 | 177 | self._launch = None 178 | self._plugin_launch = None 179 | self._interface_sync = None 180 | self._exit_code = None 181 | self._stopped = None 182 | self._stopping = None 183 | self._current_process = None 184 | self._timeout = None 185 | self._current_plugins = None 186 | self._current_plugin_processes = None 187 | self._plugin_context = None 188 | self._plugin_insts = None 189 | self._start_time = None 190 | 191 | roslaunch.pmon._init_signal_handlers() 192 | 193 | if (self._exchange): 194 | self._exchange.update_local() 195 | 196 | # for time profiling 197 | # time profiling is commented out because it will slow down. 198 | # comment in when you debug it 199 | # start_time = time.time() 200 | # pr = cProfile.Profile() 201 | # pr.enable() 202 | self._app_list.update() 203 | self.publish_exchange_list_apps() 204 | self.publish_list_apps() 205 | # pr.disable() 206 | # s = StringIO() 207 | # sortby = 'cumulative' 208 | # ps = pstats.Stats(pr, stream=s).sort_stats(sortby) 209 | # ps.print_stats() 210 | # print(s.getvalue()) 211 | # end_time = time.time() 212 | # rospy.logerr('total time: {}'.format(end_time - start_time)) 213 | 214 | # show_summary keyword is added in melodic 215 | # removing for kinetic compability 216 | rospy.loginfo("Initializing default launcher") 217 | try: 218 | self._default_launch = roslaunch.parent.ROSLaunchParent( 219 | rospy.get_param("/run_id"), [], is_core=False, 220 | sigint_timeout=self._sigint_timeout, 221 | sigterm_timeout=self._sigterm_timeout) 222 | except TypeError: 223 | # ROSLaunchParent() does not have sigint/sigterm_timeout argument 224 | # if roslaunch < 1.14.13 or < 1.15.5 225 | self._default_launch = roslaunch.parent.ROSLaunchParent( 226 | rospy.get_param("/run_id"), [], is_core=False) 227 | self._default_launch.start(auto_terminate=False) 228 | 229 | def shutdown(self): 230 | if self._api_sync: 231 | self._api_sync.stop() 232 | if self._interface_sync: 233 | self._interface_sync.stop() 234 | self._stopped = True 235 | self.__stop_current() 236 | 237 | def _get_current_app(self): 238 | return self._current_app 239 | 240 | def _set_current_app(self, app, app_definition): 241 | self._current_app = app 242 | self._current_app_definition = app_definition 243 | 244 | if self._list_apps_pub: 245 | if app is not None: 246 | self._list_apps_pub.publish([app], self._app_list.get_app_list()) 247 | else: 248 | self._list_apps_pub.publish([], self._app_list.get_app_list()) 249 | 250 | def scoped_name(self, name): 251 | return rosgraph.names.canonicalize_name('/%s/%s'%(self._robot_name, rospy.remap_name(name))) 252 | 253 | def handle_get_app_details(self, req): 254 | return GetAppDetailsResponse(app=self._exchange.get_app_details(req.name)) 255 | 256 | def handle_list_exchange_apps(self, req): 257 | if (self._exchange == None): 258 | return None 259 | if (req.remote_update): 260 | rospy.loginfo("UPDATE") 261 | if (not self._exchange.update()): 262 | return None 263 | i_apps = self._exchange.get_installed_apps() 264 | a_apps = self._exchange.get_available_apps() 265 | return GetInstallationStateResponse(installed_apps=i_apps, available_apps=a_apps) 266 | 267 | def publish_list_apps(self): 268 | if self._current_app: 269 | self._list_apps_pub.publish([self._current_app], self._app_list.get_app_list()) 270 | else: 271 | self._list_apps_pub.publish([], self._app_list.get_app_list()) 272 | 273 | def publish_exchange_list_apps(self): 274 | if (self._exchange == None): 275 | return 276 | i_apps = self._exchange.get_installed_apps() 277 | a_apps = self._exchange.get_available_apps() 278 | self._exchange_list_apps_pub.publish(i_apps, a_apps) 279 | 280 | def handle_install_app(self, req): 281 | appname = req.name 282 | if (self._exchange.install_app(appname)): 283 | self._app_list.update() 284 | self.publish_list_apps() 285 | self.publish_exchange_list_apps() 286 | return InstallAppResponse(installed=True, message="app [%s] installed"%(appname)) 287 | else: 288 | return InstallAppResponse(installed=False, message="app [%s] could not be installed"%(appname)) 289 | 290 | def handle_uninstall_app(self, req): 291 | appname = req.name 292 | if (self._exchange.uninstall_app(appname)): 293 | self._app_list.update() 294 | self.publish_list_apps() 295 | self.publish_exchange_list_apps() 296 | return UninstallAppResponse(uninstalled=True, message="app [%s] uninstalled"%(appname)) 297 | else: 298 | return UninstallAppResponse(uninstalled=False, message="app [%s] could not be uninstalled"%(appname)) 299 | 300 | def handle_list_apps(self, req): 301 | rospy.loginfo("Listing apps") 302 | current = self._current_app 303 | if current: 304 | running_apps = [current] 305 | else: 306 | running_apps = [] 307 | self._app_list.update() 308 | rospy.loginfo("done listing apps") 309 | return ListAppsResponse(running_apps=running_apps, available_apps=self._app_list.get_app_list()) 310 | 311 | def handle_start_app(self, req): 312 | rospy.loginfo("start_app: %s"%(req.name)) 313 | 314 | appname = req.name 315 | rospy.loginfo("Loading app: %s"%(appname)) 316 | try: 317 | if self._app_list and self._app_list.get_app(appname): 318 | app = self._app_list.get_app(appname) 319 | else: 320 | app = load_AppDefinition_by_name(appname) 321 | except ValueError as e: 322 | return StartAppResponse(started=False, message=str(e), error_code=StatusCodes.BAD_REQUEST) 323 | except InvalidAppException as e: 324 | return StartAppResponse(started=False, message=str(e), error_code=StatusCodes.INTERNAL_ERROR) 325 | except NotFoundException as e: 326 | return StartAppResponse(started=False, message=str(e), error_code=StatusCodes.NOT_FOUND) 327 | 328 | # Only support run apps with no plugins to run in parallel 329 | # TODO: use multiplexers to enable safe resource sharing 330 | rospy.loginfo('Current App: {}'.format(self._current_app)) 331 | if (self._current_app and 332 | (app.launch or app.plugins 333 | or not self._current_app_definition.allow_parallel 334 | or not app.allow_parallel)): 335 | if self._current_app_definition.name == req.name: 336 | return StartAppResponse(started=True, message="app [%s] already started"%(req.name), namespace=self._app_interface) 337 | elif not self._enable_app_replacement: 338 | return StartAppResponse( 339 | started=False, 340 | message="app [%s] is denied because app [%s] is already running." 341 | % (req.name, self._current_app_definition.name), 342 | namespace=self._app_interface, 343 | error_code=StatusCodes.MULTIAPP_NOT_SUPPORTED) 344 | else: 345 | self.stop_app(self._current_app_definition.name) 346 | 347 | try: 348 | is_main_app = self._current_app is None 349 | has_plugin = not not app.plugins 350 | 351 | rospy.loginfo('App: {} main: {} plugins: {}'.format(appname, is_main_app, has_plugin)) 352 | if is_main_app: 353 | self._set_current_app(App(name=appname), app) 354 | 355 | self._status_pub.publish(AppStatus(AppStatus.INFO, 'launching %s'%(app.display_name))) 356 | 357 | launch_files = [] 358 | if app.launch: 359 | if len(req.args) == 0: 360 | launch_files = [app.launch] 361 | rospy.loginfo("Launching: {}".format(app.launch)) 362 | else: 363 | app_launch_args = [] 364 | for arg in req.args: 365 | app_launch_args.append("{}:={}".format(arg.key, arg.value)) 366 | launch_files = [(app.launch, app_launch_args)] 367 | rospy.loginfo("Launching: {} {}".format(app.launch, app_launch_args)) 368 | 369 | plugin_launch_files = [] 370 | if app.plugins: 371 | self._current_plugins = [] 372 | if 'start_plugin_order' in app.plugin_order: 373 | plugin_names = [p['name'] for p in app.plugins] 374 | plugin_order = app.plugin_order['start_plugin_order'] 375 | if len(set(plugin_names) - set(plugin_order)) > 0: 376 | rospy.logwarn( 377 | "Some plugins are defined in plugins but not written in start_plugin_order: {}" 378 | .format(set(plugin_names) - set(plugin_order))) 379 | app_plugins = [] 380 | for plugin_name in plugin_order: 381 | if plugin_name not in plugin_names: 382 | rospy.logerr("app plugin '{}' not found in app file.".format(plugin_name)) 383 | continue 384 | app_plugins.append( 385 | app.plugins[plugin_names.index(plugin_name)]) 386 | else: 387 | app_plugins = app.plugins 388 | for app_plugin in app_plugins: 389 | app_plugin_type = app_plugin['type'] 390 | try: 391 | plugin = next( 392 | p for p in self._plugins if p['name'] == app_plugin_type) 393 | self._current_plugins.append((app_plugin, plugin)) 394 | if 'launch' in plugin and plugin['launch']: 395 | plugin_launch_file = find_resource(plugin['launch']) 396 | launch_args = {} 397 | if 'launch_args' in app_plugin: 398 | launch_args.update(app_plugin['launch_args']) 399 | if 'launch_arg_yaml' in app_plugin: 400 | with open(app_plugin['launch_arg_yaml']) as yaml_f: 401 | yaml_launch_args = yaml.load(yaml_f) 402 | for k, v in yaml_launch_args.items(): 403 | if k in launch_args: 404 | rospy.logwarn("'{}' is set both in launch_args and launch_arg_yaml".format(k)) 405 | rospy.logwarn("'{}' is overwritten: {} -> {}".format(k, launch_args[k], v)) 406 | launch_args[k] = v 407 | plugin_launch_args = [] 408 | for k, v in launch_args.items(): 409 | if isinstance(v, list): 410 | v = " ".join(map(str, v)) 411 | plugin_launch_args.append("{}:={}".format(k, v)) 412 | rospy.loginfo( 413 | "Launching plugin: {} {}".format( 414 | plugin_launch_file, plugin_launch_args)) 415 | plugin_launch_files.append( 416 | (plugin_launch_file, plugin_launch_args)) 417 | except StopIteration: 418 | rospy.logerr( 419 | 'There is no available app_manager plugin: {}' 420 | .format(app_plugin_type)) 421 | 422 | #TODO:XXX This is a roslaunch-caller-like abomination. Should leverage a true roslaunch API when it exists. 423 | if app.launch: 424 | try: 425 | self._launch = roslaunch.parent.ROSLaunchParent( 426 | rospy.get_param("/run_id"), launch_files, 427 | is_core=False, process_listeners=(), 428 | sigint_timeout=self._sigint_timeout, 429 | sigterm_timeout=self._sigterm_timeout) 430 | except TypeError: 431 | # ROSLaunchParent() does not have sigint/sigterm_timeout argument 432 | # if roslaunch < 1.14.13 or < 1.15.5 433 | self._launch = roslaunch.parent.ROSLaunchParent( 434 | rospy.get_param("/run_id"), launch_files, 435 | is_core=False, process_listeners=()) 436 | self._launch._load_config() 437 | if has_plugin: 438 | try: 439 | self._plugin_launch = roslaunch.parent.ROSLaunchParent( 440 | rospy.get_param("/run_id"), plugin_launch_files, 441 | is_core=False, process_listeners=(), 442 | sigint_timeout=self._sigint_timeout, 443 | sigterm_timeout=self._sigterm_timeout) 444 | except TypeError: 445 | # ROSLaunchParent() does not have sigint/sigterm_timeout argument 446 | # if roslaunch < 1.14.13 or < 1.15.5 447 | self._plugin_launch = roslaunch.parent.ROSLaunchParent( 448 | rospy.get_param("/run_id"), plugin_launch_files, 449 | is_core=False, process_listeners=()) 450 | self._plugin_launch._load_config() 451 | 452 | #TODO: convert to method 453 | nodes = [] 454 | if app.launch: 455 | nodes.extend(self._launch.config.nodes) 456 | if app.run: 457 | nodes.append(app.run) 458 | if self._enable_topic_remapping: 459 | for N in nodes: 460 | for t in app.interface.published_topics.keys(): 461 | N.remap_args.append((t, self._app_interface + '/' + t)) 462 | for t in app.interface.subscribed_topics.keys(): 463 | N.remap_args.append((t, self._app_interface + '/' + t)) 464 | 465 | # run plugin modules first 466 | if is_main_app: 467 | self._current_plugin_processes = [] 468 | if has_plugin and self._current_plugins: 469 | self._plugin_context = {} 470 | self._plugin_insts = {} 471 | for app_plugin, plugin in self._current_plugins: 472 | if 'module' in plugin and plugin['module']: 473 | plugin_args = {} 474 | start_plugin_args = {} 475 | if 'plugin_args' in app_plugin: 476 | plugin_args.update(app_plugin['plugin_args']) 477 | if 'plugin_arg_yaml' in app_plugin: 478 | with open(app_plugin['plugin_arg_yaml']) as yaml_f: 479 | yaml_plugin_args = yaml.load(yaml_f) 480 | for k, v in yaml_plugin_args.items(): 481 | if k in plugin_args: 482 | rospy.logwarn("'{}' is set both in plugin_args and plugin_arg_yaml".format(k)) 483 | rospy.logwarn("'{}' is overwritten: {} -> {}".format(k, plugin_args[k], v)) 484 | plugin_args[k] = v 485 | if 'start_plugin_args' in app_plugin: 486 | start_plugin_args.update(app_plugin['start_plugin_args']) 487 | if 'start_plugin_arg_yaml' in app_plugin: 488 | with open(app_plugin['start_plugin_arg_yaml']) as yaml_f: 489 | yaml_plugin_args = yaml.load(yaml_f) 490 | for k, v in yaml_plugin_args.items(): 491 | if k in start_plugin_args: 492 | rospy.logwarn("'{}' is set both in start_plugin_args and start_plugin_arg_yaml".format(k)) 493 | rospy.logwarn("'{}' is overwritten: {} -> {}".format(k, start_plugin_args[k], v)) 494 | start_plugin_args[k] = v 495 | plugin_args.update(start_plugin_args) 496 | mod = __import__(plugin['module'].split('.')[0]) 497 | for sub_mod in plugin['module'].split('.')[1:]: 498 | mod = getattr(mod, sub_mod) 499 | plugin_inst = mod() 500 | plugin_inst.app_manager_start_plugin( 501 | app, self._plugin_context, plugin_args) 502 | self._plugin_insts[plugin['module']] = plugin_inst 503 | if 'run' in plugin and plugin['run']: 504 | p, a = roslib.names.package_resource_name(plugin['run']) 505 | args = plugin.get('run_args', None) 506 | node = roslaunch.core.Node(p, a, args=args, output='screen', 507 | required=False) 508 | proc, success = self._default_launch.runner.launch_node(node) 509 | if not success: 510 | raise roslaunch.core.RLException( 511 | "failed to launch plugin %s/%s"%(node.package, node.type)) 512 | self._current_plugin_processes.append(proc) 513 | 514 | # then, start plugin launches 515 | if has_plugin: 516 | self._plugin_launch.start() 517 | 518 | # finally launch main launch 519 | if app.launch: 520 | self._launch.start() 521 | if app.run: 522 | node = app.run 523 | proc, success = self._default_launch.runner.launch_node(node) 524 | if not success: 525 | raise roslaunch.core.RLException( 526 | "failed to launch %s/%s"%(node.package, node.type)) 527 | if is_main_app: 528 | self._current_process = proc 529 | 530 | if is_main_app and app.timeout is not None: 531 | self._start_time = rospy.Time.now() 532 | 533 | fp = [x for x in app.interface.subscribed_topics.keys()] 534 | lp = [x for x in app.interface.published_topics.keys()] 535 | if self._enable_topic_remapping: 536 | fp = [self._app_interface + '/' + x for x in fp] 537 | lp = [self._app_interface + '/' + x for x in lp] 538 | 539 | self._interface_sync = MasterSync(self._interface_master, foreign_pub_names=fp, local_pub_names=lp) 540 | if is_main_app: 541 | thread.start_new_thread(self.app_monitor, (app.launch,)) 542 | 543 | return StartAppResponse(started=True, message="app [%s] started"%(appname), namespace=self._app_interface) 544 | 545 | except Exception as e: 546 | rospy.logerr(traceback.format_exc()) 547 | exc_type, exc_obj, exc_tb = sys.exc_info() 548 | fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 549 | try: 550 | # attempt to kill any launched resources 551 | self._stop_current() 552 | except: 553 | pass 554 | finally: 555 | self._set_current_app(None, None) 556 | self._status_pub.publish(AppStatus(AppStatus.INFO, 'app start failed')) 557 | rospy.logerr( 558 | "app start failed [{}, line {}: {}]".format(fname, exc_tb.tb_lineno, str(e))) 559 | return StartAppResponse(started=False, message="internal error [%s, line %d: %s]"%(fname, exc_tb.tb_lineno, str(e)), error_code=StatusCodes.INTERNAL_ERROR) 560 | 561 | def _stop_current(self): 562 | try: 563 | self._stopping = True 564 | self.__stop_current() 565 | finally: 566 | self._launch = None 567 | self._plugin_launch = None 568 | self._exit_code = None 569 | self._stopped = None 570 | self._stopping = None 571 | self._current_process = None 572 | self._timeout = None 573 | self._current_plugins = None 574 | self._current_plugin_processes = None 575 | self._plugin_context = None 576 | self._plugin_insts = None 577 | self._start_time = None 578 | try: 579 | self._interface_sync.stop() 580 | finally: 581 | self._interface_sync = None 582 | 583 | def __stop_current(self): 584 | if self._api_sync: 585 | self._api_sync.stop() 586 | if self._launch: 587 | self._launch.shutdown() 588 | if (self._exit_code is None 589 | and self._launch.pm 590 | and len(self._launch.pm.dead_list) > 0): 591 | exit_codes = [p.exit_code for p in self._launch.pm.dead_list] 592 | self._exit_code = max(exit_codes) 593 | if self._current_process: 594 | self._current_process.stop() 595 | if (self._exit_code is None 596 | and self._default_launch.pm 597 | and len(self._default_launch.pm.dead_list) > 0): 598 | self._exit_code = self._default_launch.pm.dead_list[0].exit_code 599 | if not self._exit_code is None and self._exit_code > 0: 600 | rospy.logerr( 601 | "App stopped with exit code: {}".format(self._exit_code)) 602 | if self._plugin_launch: 603 | self._plugin_launch.shutdown() 604 | if self._current_plugin_processes: 605 | for p in self._current_plugin_processes: 606 | p.stop() 607 | if self._current_plugins: 608 | self._plugin_context['exit_code'] = self._exit_code 609 | self._plugin_context['stopped'] = self._stopped 610 | self._plugin_context['timeout'] = self._timeout 611 | if 'stop_plugin_order' in self._current_app_definition.plugin_order: 612 | plugin_names = [p['name'] for p in self._current_app_definition.plugins] 613 | plugin_order = self._current_app_definition.plugin_order['stop_plugin_order'] 614 | if len(set(plugin_names) - set(plugin_order)) > 0: 615 | rospy.logwarn( 616 | "Some plugins are defined in plugins but not written in stop_plugin_order: {}" 617 | .format(set(plugin_names) - set(plugin_order))) 618 | current_plugins = [] 619 | for plugin_name in plugin_order: 620 | if plugin_name not in plugin_names: 621 | rospy.logerr("app plugin '{}' not found in app file.".format(plugin_name)) 622 | continue 623 | current_plugin_names = [p['name'] for p, _ in self._current_plugins] 624 | if plugin_name not in current_plugin_names: 625 | rospy.logwarn("app plugin '{}' is not running, so skip stopping".format(plugin_name)) 626 | continue 627 | current_plugins.append( 628 | self._current_plugins[current_plugin_names.index(plugin_name)]) 629 | else: 630 | current_plugins = self._current_plugins 631 | for app_plugin, plugin in current_plugins: 632 | if 'module' in plugin and plugin['module']: 633 | plugin_args = {} 634 | stop_plugin_args = {} 635 | if 'plugin_args' in app_plugin: 636 | plugin_args.update(app_plugin['plugin_args']) 637 | if 'plugin_arg_yaml' in app_plugin: 638 | with open(app_plugin['plugin_arg_yaml']) as yaml_f: 639 | yaml_plugin_args = yaml.load(yaml_f) 640 | for k, v in yaml_plugin_args.items(): 641 | if k in plugin_args: 642 | rospy.logwarn("'{}' is set both in plugin_args and plugin_arg_yaml".format(k)) 643 | rospy.logwarn("'{}' is overwritten: {} -> {}".format(k, plugin_args[k], v)) 644 | plugin_args[k] = v 645 | if 'stop_plugin_args' in app_plugin: 646 | stop_plugin_args.update(app_plugin['stop_plugin_args']) 647 | if 'stop_plugin_arg_yaml' in app_plugin: 648 | with open(app_plugin['stop_plugin_arg_yaml']) as yaml_f: 649 | yaml_plugin_args = yaml.load(yaml_f) 650 | for k, v in yaml_plugin_args.items(): 651 | if k in stop_plugin_args: 652 | rospy.logwarn("'{}' is set both in stop_plugin_args and stop_plugin_arg_yaml".format(k)) 653 | rospy.logwarn("'{}' is overwritten: {} -> {}".format(k, stop_plugin_args[k], v)) 654 | stop_plugin_args[k] = v 655 | plugin_args.update(stop_plugin_args) 656 | if plugin['module'] in self._plugin_insts: 657 | plugin_inst = self._plugin_insts[plugin['module']] 658 | else: 659 | mod = __import__(plugin['module'].split('.')[0]) 660 | for sub_mod in plugin['module'].split('.')[1:]: 661 | mod = getattr(mod, sub_mod) 662 | plugin_inst = mod() 663 | plugin_inst.app_manager_stop_plugin( 664 | self._current_app_definition, 665 | self._plugin_context, plugin_args) 666 | 667 | def handle_stop_app(self, req): 668 | rospy.loginfo("handle stop app: %s"%(req.name)) 669 | self._stopped = True 670 | return self.stop_app(req.name) 671 | 672 | def handle_reload_app_list(self, req=None): 673 | try: 674 | self._app_list.update() 675 | self.publish_list_apps() 676 | self.publish_exchange_list_apps() 677 | rospy.loginfo("app list is reloaded") 678 | except Exception as e: 679 | rospy.logerr("Failed to reload app list: %s" % e) 680 | return EmptyResponse() 681 | 682 | def app_monitor(self, is_launch): 683 | def get_target(): 684 | if is_launch: 685 | return self._launch 686 | return self._current_process 687 | def is_done(target): 688 | if is_launch: 689 | return (not target.pm or target.pm.done) 690 | return target.stopped 691 | def check_required(target): 692 | # required nodes are not registered to the dead_list when finished 693 | # so we need to constantly check its return value 694 | if is_launch and target.pm: 695 | # run nodes are never registered as required 696 | procs = target.pm.procs[:] 697 | exit_codes = [p.exit_code for p in procs if p.required] 698 | if exit_codes: 699 | self._exit_code = max(exit_codes) 700 | 701 | while get_target(): 702 | time.sleep(0.1) 703 | target = get_target() 704 | timeout = self._current_app_definition.timeout 705 | appname = self._current_app_definition.name 706 | now = rospy.Time.now() 707 | if target: 708 | check_required(target) 709 | if is_done(target): 710 | time.sleep(1.0) 711 | if not self._stopping: 712 | self.stop_app(appname) 713 | break 714 | if (timeout is not None and 715 | self._start_time is not None and 716 | (now - self._start_time).to_sec() > timeout): 717 | self._stopped = True 718 | self._timeout = True 719 | self.stop_app(appname) 720 | rospy.logerr( 721 | 'app {} is stopped because of timeout: {}s'.format( 722 | appname, timeout)) 723 | break 724 | 725 | 726 | 727 | def stop_app(self, appname): 728 | resp = StopAppResponse(stopped=False) 729 | try: 730 | app = self._current_app_definition 731 | 732 | # request to stop all apps. 733 | if app is not None and appname == '*': 734 | appname = app.name 735 | 736 | if app is None or app.name != appname: 737 | rospy.loginfo("handle stop app: app [%s] is not running [x]"%(appname)) 738 | resp.error_code = StatusCodes.NOT_RUNNING 739 | resp.message = "app %s is not running"%(appname) 740 | else: 741 | try: 742 | app_status_message = None 743 | if self._launch or self._current_process: 744 | rosinfo_message = "handle stop app: stopping app [%s]"%(appname) 745 | app_status_message = 'stopping %s'%(app.display_name) 746 | self._stop_current() 747 | resp.stopped = True 748 | resp.message = "%s stopped"%(appname) 749 | if self._timeout: 750 | resp.timeout = self._timeout 751 | rosinfo_message += "by timeout" 752 | app_status_message += "by timeout" 753 | resp.message += " by timeout" 754 | rospy.loginfo(rosinfo_message) 755 | else: 756 | rospy.loginfo("handle stop app: app [%s] is not running"%(appname)) 757 | resp.message = "app [%s] is not running"%(appname) 758 | resp.error_code = StatusCodes.NOT_RUNNING 759 | finally: 760 | if app_status_message is not None: 761 | self._status_pub.publish( 762 | AppStatus(AppStatus.INFO, app_status_message)) 763 | self._launch = None 764 | self._current_process = None 765 | self._set_current_app(None, None) 766 | 767 | except Exception as e: 768 | rospy.logerr(traceback.format_exc()) 769 | exc_type, exc_obj, exc_tb = sys.exc_info() 770 | fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 771 | rospy.logerr("handle stop app: internal error [%s, line %d: %s]"%(fname, exc_tb.tb_lineno, str(e))) 772 | resp.error_code = StatusCodes.INTERNAL_ERROR 773 | resp.message = "internal error: %s"%(str(e)) 774 | 775 | return resp 776 | -------------------------------------------------------------------------------- /src/app_manager/app_manager_plugin.py: -------------------------------------------------------------------------------- 1 | class AppManagerPlugin(object): 2 | 3 | """Base class for app_manager plugin 4 | 5 | This is a base class for app_manager plugin. 6 | app_manager plugin have two class methods; 7 | app_manager_start_plugin and app_manager_stop_plugin. 8 | app_manager_start_plugin runs before app starts, 9 | and app_manager_stop_plugin runs after app stops. 10 | 11 | app_manager plugin is defined in yaml format as below; 12 | - name: app_recorder/rosbag_recorder_plugin # plugin name 13 | launch: app_recorder/rosbag_recorder.launch # plugin launch name 14 | module: app_recorder.rosbag_recorder_plugin.RosbagRecorderPlugin 15 | # plugin module name 16 | 17 | Also, app_manager plugin yaml file is exported in package.xml as below; 18 | 19 | 20 | 21 | 22 | In app file, you can add plugin to your app as below; 23 | - plugins 24 | - name: mail_notifier_plugin # name to identify this plugin 25 | type: app_notifier/mail_notifier_plugin # plugin type 26 | launch_args: # arguments for plugin launch file 27 | - foo: bar 28 | plugin_args: # arguments for plugin function arguments 29 | - hoge: fuga 30 | 31 | Both class methods have 3 arguments; 32 | App definition, app context and app plugin arguments. 33 | App definition is the definition of app. 34 | App context is the shared information about app (app context) 35 | between plugins, such as app results and plugin results. 36 | App plugin arguments are the arguments for the module defined in app file 37 | and written as below; 38 | """ 39 | 40 | def __init__(self): 41 | pass 42 | 43 | def app_manager_start_plugin(self, app, ctx, plugin_args): 44 | """Start plugin for app_manager 45 | 46 | Args: 47 | app (app_manager.AppDefinition): app definition 48 | ctx (dict): app context shared between plugins 49 | plugin_args (dict): arguments for plugin defined in app file 50 | """ 51 | 52 | return ctx 53 | 54 | def app_manager_stop_plugin(self, app, ctx, plugin_args): 55 | """Stop plugin for app_manager 56 | 57 | Args: 58 | app (app_manager.AppDefinition): app definition 59 | ctx (dict): app context shared between plugins 60 | plugin_args (dict): arguments for plugin defined in app file 61 | """ 62 | 63 | return ctx 64 | -------------------------------------------------------------------------------- /src/app_manager/exceptions.py: -------------------------------------------------------------------------------- 1 | class AppException(Exception): 2 | """ 3 | Base exception class for App exceptions 4 | """ 5 | pass 6 | 7 | class InvalidAppException(AppException): 8 | """ 9 | App specification is invalid. 10 | """ 11 | pass 12 | 13 | class NotFoundException(AppException): 14 | """ 15 | Resource is not installed. 16 | """ 17 | pass 18 | 19 | class LaunchException(AppException): 20 | """ 21 | Exception thrown related to launching an App 22 | """ 23 | pass 24 | 25 | class InternalAppException(Exception): 26 | """ 27 | Base exception class for App exceptions 28 | """ 29 | pass 30 | -------------------------------------------------------------------------------- /src/app_manager/exchange.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | # Software License Agreement (BSD License) 3 | # 4 | # Copyright (c) 2011, Willow Garage, Inc. 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions 9 | # are met: 10 | # 11 | # * Redistributions of source code must retain the above copyright 12 | # notice, this list of conditions and the following disclaimer. 13 | # * Redistributions in binary form must reproduce the above 14 | # copyright notice, this list of conditions and the following 15 | # disclaimer in the documentation and/or other materials provided 16 | # with the distribution. 17 | # * Neither the name of Willow Garage, Inc. nor the names of its 18 | # contributors may be used to endorse or promote products derived 19 | # from this software without specific prior written permission. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 24 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 25 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 26 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 27 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 30 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 31 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 32 | # POSSIBILITY OF SUCH DAMAGE. 33 | # 34 | # Revision $Id: topics.py 11753 2010-10-25 06:23:19Z kwc $ 35 | 36 | # author: pratkanis 37 | 38 | """ 39 | Implements exchange part of app_manager, which handles listing of 40 | avialable and removable applications. 41 | """ 42 | 43 | import subprocess 44 | import os 45 | import sys 46 | import yaml 47 | import rospy 48 | from std_msgs.msg import String 49 | from .msg import ExchangeApp, Icon 50 | 51 | class Exchange(): 52 | def __init__(self, url, directory, on_error = lambda x: None): 53 | self._url = url 54 | self._directory = directory 55 | self._on_error = on_error 56 | self._installed_apps = [] 57 | self._available_apps = [] 58 | self._debs = {} 59 | 60 | self._exchange_local = os.path.join(self._directory, "exchange.yaml") 61 | d = os.path.join(self._directory, "installed") 62 | if (not os.path.exists(d)): 63 | os.mkdir(d) 64 | self._exchange_file = os.path.join(d, "app_exchange.installed") 65 | rospy.loginfo("Directory: {}".format(self._directory)) 66 | rospy.loginfo("Local path: {}".format(self._exchange_local)) 67 | rospy.loginfo("Local file: {}".format(self._exchange_file)) 68 | rospy.loginfo(subprocess.Popen(["whoami"], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()) 69 | 70 | def get_installed_version(self, deb): 71 | data = subprocess.Popen(["dpkg", "-l", deb], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() 72 | val = (data[0] or '').strip() 73 | for i in val.split('\n'): 74 | if (i.find(deb) > 0 and i.find("ii") == 0): 75 | return [s for s in i.strip().split(" ") if s][2] 76 | self._on_error("Failed to get installed version: " + str(data)) 77 | return "FAILED" 78 | 79 | def get_available_version(self, deb): 80 | data = subprocess.Popen(["apt-cache", "showpkg", deb], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() 81 | val = (data[0] or '').strip() 82 | nearing = False 83 | for i in val.split('\n'): 84 | if (nearing): 85 | return i.strip().split(" ")[0].strip() 86 | if (i.strip() == "Versions:"): 87 | nearing = True 88 | self._on_error("Failed to get available version: " + str(data)) 89 | return "FAILED" 90 | 91 | def is_installed(self, deb): 92 | data = subprocess.Popen(["dpkg", "-l", deb], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() 93 | val = (data[0] or '').strip() 94 | for i in val.split("\n"): 95 | if (i.find(deb) > 0): 96 | return (i.find("ii") == 0) 97 | self._on_error("Error getting installed packages: " + str(data)) 98 | return False 99 | 100 | def get_installed_apps(self): 101 | return self._installed_apps 102 | 103 | def get_available_apps(self): 104 | return self._available_apps 105 | 106 | def get_app_details(self, name): 107 | local_path = os.path.join(self._directory, name) 108 | if (not os.path.exists(local_path)): 109 | os.makedirs(local_path) 110 | data = subprocess.Popen(["wget", "-O", os.path.join(local_path, "app.yaml"), (self._url.strip('/') + "/" + name + ".yaml")], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() 111 | val = (data[0] or '').strip() 112 | rospy.logwarn(val) 113 | try: 114 | data = yaml.load(open(os.path.join(local_path, "app.yaml"))) 115 | icon_url = data["icon_url"] 116 | icon_format = data["icon_format"] 117 | val = (subprocess.Popen(["wget", "-O", os.path.join(local_path, "icon" + icon_format), icon_url], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()[0] or '').strip() 118 | rospy.logwarn(val) 119 | except: 120 | rospy.logerr("No icon") 121 | self.update_local() 122 | for i in self._available_apps: 123 | if (i.name == name): 124 | return i 125 | for i in self._installed_apps: 126 | if (i.name == name): 127 | return i 128 | self._on_error("Problem getting app details: " + str(data)) 129 | return None 130 | 131 | def install_app(self, app): 132 | deb = False 133 | for i in self._available_apps: 134 | if (i.name == app): 135 | if (deb): 136 | return False #Somehow a dupe 137 | deb = self._debs[i.name] 138 | for i in self._installed_apps: 139 | if (i.name == app): 140 | if (deb): 141 | return False #Somehow a dupe 142 | deb = self._debs[i.name] 143 | if (deb == False): 144 | self._on_error("No debian found for install") 145 | return False 146 | rospy.loginfo("install app") 147 | p = subprocess.Popen(["sudo", "rosget", "install", deb], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 148 | #data = p.communicate() 149 | #data = "test string" 150 | pub = rospy.Publisher('install_status', String) 151 | l1 = [] 152 | for line in iter(p.stdout.readline, ''): 153 | if line.rstrip() != '': 154 | pub.publish(line.rstrip()) 155 | l1.append(line) 156 | else: 157 | break 158 | l2 = [] 159 | for line in iter(p.stderr.readline, ''): 160 | if line.rstrip() != '': 161 | pub.publish(line.rstrip()) 162 | l2.append(line) 163 | else: 164 | break 165 | 166 | data = (''.join(l1), ''.join(l2)) 167 | val = (data[0] or '').strip() 168 | rospy.logwarn(val) 169 | self.update_local() 170 | for i in self._installed_apps: 171 | if (i.name == app): 172 | return True 173 | self._on_error("Invalid return for install: " + str(data)) 174 | return False 175 | 176 | def uninstall_app(self, app): 177 | deb = False 178 | for i in self._installed_apps: 179 | if (i.name == app): 180 | if (deb): 181 | return False #Somehow a dupe 182 | deb = self._debs[i.name] 183 | if (deb == False): 184 | self._on_error("No debian found for uninstall") 185 | return False 186 | rospy.loginfo("uninstall app") 187 | data = subprocess.Popen(["sudo", "rosget", "remove", deb], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() 188 | val = (data[0] or '').strip() 189 | self.update_local() 190 | for i in self._available_apps: 191 | if (i.name == app): 192 | return True 193 | self._on_error("Invalid return for uninstall: " + str(data)) 194 | return False 195 | 196 | def update(self): 197 | #Call server 198 | val = (subprocess.Popen(["wget", "-O", self._exchange_local, self._url + "/applications.yaml"], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()[0] or '').strip() 199 | if (val != "" or not os.path.exists(self._exchange_local)): 200 | rospy.logerr("Wget failed: {}".format(val)) 201 | return False 202 | 203 | p = subprocess.Popen(["sudo", "rosget", "update"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 204 | data = p.communicate() 205 | val = (data[0] or '').strip() 206 | if (p.returncode != 0): 207 | self._on_error("Invalid return of update: " + str(data)) 208 | self.update_local() 209 | if (p.returncode != 0): 210 | return False 211 | return True 212 | 213 | def update_local(self): 214 | installed_apps = [] 215 | file_apps = [] 216 | available_apps = [] 217 | try: 218 | exchange_data = yaml.load(open(self._exchange_local)) 219 | except: 220 | return 221 | if (not exchange_data): 222 | return 223 | for app in exchange_data['apps']: 224 | appc = ExchangeApp() 225 | appc.name = app['app'] 226 | appc.display_name = app['display'] 227 | deb = app['debian'] 228 | self._debs[app['app']] = deb 229 | appc.latest_version = self.get_available_version(deb) 230 | appc.hidden = False 231 | try: 232 | if(app['hidden']): 233 | appc.hidden = True 234 | except: 235 | pass 236 | 237 | local_path = os.path.join(self._directory, app['app']) 238 | if (os.path.exists(local_path)): 239 | format = "" 240 | if (os.path.exists(os.path.join(local_path, "app.yaml"))): 241 | rospy.logwarn(local_path) 242 | data = yaml.load(open(os.path.join(local_path, "app.yaml"))) 243 | try: 244 | appc.description = data['description'] 245 | except: 246 | if (appc.hidden): 247 | appc.description = "Descriptionless hidden app" 248 | else: 249 | appc.description = "No description set, likely an error in the yaml file" 250 | try: 251 | format = data['icon_format'] 252 | except: 253 | pass 254 | if (os.path.exists(os.path.join(local_path, "icon" + format)) and format != ""): 255 | icon = Icon() 256 | icon.format = format.strip(".") 257 | if (icon.format == "jpg"): icon.format = "jpeg" 258 | icon.data = open(os.path.join(local_path, "icon" + format), "rb").read() 259 | appc.icon = icon 260 | if (self.is_installed(deb)): 261 | appc.version = self.get_installed_version(deb) 262 | installed_apps.append(appc) 263 | file_apps.append(app) #Should remove debian tag? 264 | else: 265 | available_apps.append(appc) 266 | 267 | f = open(self._exchange_file, "w") 268 | yaml.dump({"apps": file_apps}, f) 269 | f.close() 270 | self._installed_apps = installed_apps 271 | self._available_apps = available_apps 272 | 273 | 274 | -------------------------------------------------------------------------------- /src/app_manager/master_sync.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | if sys.version_info[0] == 3: 4 | from urllib.parse import urlparse # python3 move urlparse to urllib.parse 5 | else: 6 | import urlparse 7 | 8 | 9 | import threading 10 | import time 11 | 12 | import rosgraph 13 | import rosgraph.names 14 | import rosgraph.network 15 | 16 | import rospy 17 | 18 | from rospy.core import global_name, is_topic 19 | from rospy.impl.validators import non_empty, ParameterInvalid 20 | 21 | from rospy.impl.masterslave import apivalidate 22 | 23 | from rosgraph.xmlrpc import XmlRpcNode, XmlRpcHandler 24 | 25 | def is_publishers_list(paramName): 26 | return ('is_publishers_list', paramName) 27 | 28 | 29 | class TopicPubListenerHandler(XmlRpcHandler): 30 | 31 | def __init__(self, cb): 32 | super(TopicPubListenerHandler, self).__init__() 33 | self.uri = None 34 | self.cb = cb 35 | 36 | def _ready(self, uri): 37 | self.uri = uri 38 | 39 | def _custom_validate(self, validation, param_name, param_value, caller_id): 40 | if validation == 'is_publishers_list': 41 | if not type(param_value) == list: 42 | raise ParameterInvalid("ERROR: param [%s] must be a list"%param_name) 43 | for v in param_value: 44 | if not isinstance(v, basestring): 45 | raise ParameterInvalid("ERROR: param [%s] must be a list of strings"%param_name) 46 | parsed = urlparse.urlparse(v) 47 | if not parsed[0] or not parsed[1]: #protocol and host 48 | raise ParameterInvalid("ERROR: param [%s] does not contain valid URLs [%s]"%(param_name, v)) 49 | return param_value 50 | else: 51 | raise ParameterInvalid("ERROR: param [%s] has an unknown validation type [%s]"%(param_name, validation)) 52 | 53 | @apivalidate([]) 54 | def getBusStats(self, caller_id): 55 | # not supported 56 | return 1, '', [[], [], []] 57 | 58 | @apivalidate([]) 59 | def getBusInfo(self, caller_id): 60 | # not supported 61 | return 1, '', [[], [], []] 62 | 63 | @apivalidate('') 64 | def getMasterUri(self, caller_id): 65 | # not supported 66 | return 1, '', '' 67 | 68 | @apivalidate(0, (None, )) 69 | def shutdown(self, caller_id, msg=''): 70 | return -1, "not authorized", 0 71 | 72 | @apivalidate(-1) 73 | def getPid(self, caller_id): 74 | return -1, "not authorized", 0 75 | 76 | ############################################################################### 77 | # PUB/SUB APIS 78 | 79 | @apivalidate([]) 80 | def getSubscriptions(self, caller_id): 81 | return 1, "subscriptions", [[], []] 82 | 83 | @apivalidate([]) 84 | def getPublications(self, caller_id): 85 | return 1, "publications", [[], []] 86 | 87 | @apivalidate(-1, (global_name('parameter_key'), None)) 88 | def paramUpdate(self, caller_id, parameter_key, parameter_value): 89 | # not supported 90 | return -1, 'not authorized', 0 91 | 92 | @apivalidate(-1, (is_topic('topic'), is_publishers_list('publishers'))) 93 | def publisherUpdate(self, caller_id, topic, publishers): 94 | self.cb(topic, publishers) 95 | 96 | @apivalidate([], (is_topic('topic'), non_empty('protocols'))) 97 | def requestTopic(self, caller_id, topic, protocols): 98 | return 0, "no supported protocol implementations", [] 99 | 100 | 101 | class RemoteManager(object): 102 | def __init__(self, master_uri, cb): 103 | self.master_uri = master_uri 104 | 105 | ns = rosgraph.names.get_ros_namespace() 106 | anon_name = rosgraph.names.anonymous_name('master_sync') 107 | 108 | self.master = rosgraph.Master(rosgraph.names.ns_join(ns, anon_name), master_uri=self.master_uri) 109 | 110 | self.cb = cb 111 | 112 | self.type_cache = {} 113 | 114 | self.subs = {} 115 | self.pubs = {} 116 | self.srvs = {} 117 | 118 | rpc_handler = TopicPubListenerHandler(self.new_topics) 119 | self.external_node = XmlRpcNode(rpc_handler=rpc_handler) 120 | self.external_node.start() 121 | 122 | timeout_t = time.time() + 5. 123 | while time.time() < timeout_t and self.external_node.uri is None: 124 | time.sleep(0.01) 125 | 126 | 127 | def get_topic_type(self, query_topic): 128 | query_topic = self.resolve(query_topic) 129 | 130 | if query_topic in self.type_cache: 131 | return self.type_cache[query_topic] 132 | else: 133 | for topic, topic_type in self.master.getTopicTypes(): 134 | self.type_cache[topic] = topic_type 135 | if query_topic in self.type_cache: 136 | return self.type_cache[query_topic] 137 | else: 138 | return "*" 139 | 140 | def subscribe(self, topic): 141 | topic = self.resolve(topic) 142 | publishers = self.master.registerSubscriber(topic, '*', self.external_node.uri) 143 | self.subs[(topic, self.external_node.uri)] = self.master 144 | self.new_topics(topic, publishers) 145 | 146 | def advertise(self, topic, topic_type, uri): 147 | topic = self.resolve(topic) 148 | 149 | # Prevent double-advertisements 150 | if (topic, uri) in self.pubs: 151 | return 152 | 153 | # These registrations need to be anonymous so the master doesn't kill us if there is a duplicate name 154 | anon_name = rosgraph.names.anonymous_name('master_sync') 155 | master = rosgraph.Master(anon_name, master_uri=self.master_uri) 156 | 157 | rospy.loginfo("Registering (%s,%s) on master %s"%(topic,uri,master.master_uri)) 158 | 159 | master.registerPublisher(topic, topic_type, uri) 160 | self.pubs[(topic, uri)] = master 161 | 162 | 163 | def unadvertise(self, topic, uri): 164 | if (topic, uri) in self.pubs: 165 | m = self.pubs[(topic,uri)] 166 | rospy.loginfo("Unregistering (%s,%s) from master %s"%(topic,uri,m.master_uri)) 167 | m.unregisterPublisher(topic,uri) 168 | del self.pubs[(topic,uri)] 169 | 170 | 171 | def advertise_list(self, topic, topic_type, uris): 172 | topic = self.resolve(topic) 173 | 174 | unadv = set((t,u) for (t,u) in self.pubs.keys() if t == topic) - set([(topic, u) for u in uris]) 175 | for (t,u) in self.pubs.keys(): 176 | if (t,u) in unadv: 177 | self.unadvertise(t,u) 178 | 179 | for u in uris: 180 | self.advertise(topic, topic_type, u) 181 | 182 | def lookup_service(self, service_name): 183 | service_name = self.resolve(service_name) 184 | try: 185 | return self.master.lookupService(service_name) 186 | except rosgraph.MasterError: 187 | return None 188 | 189 | def advertise_service(self, service_name, uri): 190 | 191 | # These registrations need to be anonymous so the master doesn't kill us if there is a duplicate name 192 | anon_name = rosgraph.names.anonymous_name('master_sync') 193 | master = rosgraph.Master(anon_name, master_uri=self.master_uri) 194 | 195 | if (service_name) in self.srvs: 196 | if self.srvs[service_name][0] == uri: 197 | return 198 | else: 199 | self.unadvertise_service(service_name) 200 | 201 | fake_api = 'http://%s:0'%rosgraph.network.get_host_name() 202 | rospy.loginfo("Registering service (%s,%s) on master %s"%(service_name, uri, master.master_uri)) 203 | master.registerService(service_name, uri, fake_api) 204 | 205 | self.srvs[service_name] = (uri, master) 206 | 207 | def unadvertise_service(self, service_name): 208 | if service_name in self.srvs: 209 | uri,m = self.srvs[service_name] 210 | rospy.loginfo("Unregistering service (%s,%s) from master %s"%(service_name, uri, m.master_uri)) 211 | m.unregisterService(service_name, uri) 212 | del self.srvs[service_name] 213 | 214 | 215 | def resolve(self, topic): 216 | ns = rosgraph.names.namespace(self.master.caller_id) 217 | return rosgraph.names.ns_join(ns, topic) 218 | 219 | def unsubscribe_all(self): 220 | for (t,u),m in list(self.subs.items()): 221 | m.unregisterSubscriber(t,u) 222 | for t,u in list(self.pubs.keys()): 223 | self.unadvertise(t,u) 224 | for s in list(self.srvs.keys()): 225 | self.unadvertise_service(s) 226 | 227 | def new_topics(self, topic, publishers): 228 | self.cb(topic, [p for p in publishers if (topic,p) not in self.pubs]) 229 | 230 | 231 | def check_master(m): 232 | try: 233 | m.getUri() 234 | return True 235 | except Exception: 236 | return False 237 | 238 | class MasterSync(object): 239 | def __init__(self, foreign_master, local_service_names = [], local_pub_names = [], foreign_service_names = [], foreign_pub_names = []): 240 | 241 | self.local_service_names = local_service_names 242 | self.local_pub_names = local_pub_names 243 | self.foreign_service_names = foreign_service_names 244 | self.foreign_pub_names = foreign_pub_names 245 | 246 | self.local_manager = None 247 | self.foreign_manager = None 248 | self.stopping = False 249 | self.thread = None 250 | 251 | # Get master URIs 252 | local_master = rosgraph.get_master_uri() 253 | 254 | m = rosgraph.Master(rospy.get_name(), master_uri=foreign_master) 255 | r = rospy.Rate(1) 256 | rospy.loginfo("Waiting for foreign master [%s] to come up..."%(foreign_master)) 257 | while not check_master(m) and not rospy.is_shutdown(): 258 | r.sleep() 259 | 260 | if not rospy.is_shutdown(): 261 | rospy.loginfo("Foreign master is available") 262 | 263 | self.local_manager = RemoteManager(local_master, self.new_local_topics) 264 | self.foreign_manager = RemoteManager(foreign_master, self.new_foreign_topics) 265 | 266 | for t in self.local_pub_names: 267 | self.local_manager.subscribe(t) 268 | 269 | for t in self.foreign_pub_names: 270 | self.foreign_manager.subscribe(t) 271 | 272 | self.thread = threading.Thread(target=self.spin) 273 | self.thread.start() 274 | 275 | else: 276 | rospy.loginfo("shutdown flag raised, aborting...") 277 | 278 | 279 | def new_local_topics(self, topic, publishers): 280 | topic_type = self.local_manager.get_topic_type(topic) 281 | self.foreign_manager.advertise_list(topic, topic_type, publishers) 282 | 283 | 284 | def new_foreign_topics(self, topic, publishers): 285 | topic_type = self.foreign_manager.get_topic_type(topic) 286 | self.local_manager.advertise_list(topic, topic_type, publishers) 287 | 288 | 289 | def stop(self): 290 | self.stopping = True 291 | self.thread.join() 292 | 293 | if self.local_manager: 294 | self.local_manager.unsubscribe_all() 295 | if self.foreign_manager: 296 | self.foreign_manager.unsubscribe_all() 297 | 298 | 299 | # Spin is necessary to synchronize SERVICES. Topics work entirely on a callback-driven basis 300 | def spin(self): 301 | r = rospy.Rate(1.0) 302 | while not rospy.is_shutdown() and not self.stopping: 303 | for s in self.local_service_names: 304 | srv_uri = self.local_manager.lookup_service(s) 305 | if srv_uri is not None: 306 | self.foreign_manager.advertise_service(s, srv_uri) 307 | else: 308 | self.foreign_manager.unadvertise_service(s) 309 | for s in self.foreign_service_names: 310 | srv_uri = self.foreign_manager.lookup_service(s) 311 | if srv_uri is not None: 312 | self.local_manager.advertise_service(s, srv_uri) 313 | else: 314 | self.local_manager.unadvertise_service(s) 315 | r.sleep() 316 | -------------------------------------------------------------------------------- /srv/GetAppDetails.srv: -------------------------------------------------------------------------------- 1 | # Name of the app to get details of 2 | string name 3 | --- 4 | ExchangeApp app 5 | 6 | -------------------------------------------------------------------------------- /srv/GetInstallationState.srv: -------------------------------------------------------------------------------- 1 | bool remote_update 2 | --- 3 | ExchangeApp[] installed_apps 4 | ExchangeApp[] available_apps -------------------------------------------------------------------------------- /srv/InstallApp.srv: -------------------------------------------------------------------------------- 1 | # Name of the app to install or upgrade 2 | string name 3 | --- 4 | # true if app started, false otherwise 5 | bool installed 6 | # response message for debugging 7 | string message 8 | 9 | -------------------------------------------------------------------------------- /srv/ListApps.srv: -------------------------------------------------------------------------------- 1 | --- 2 | App[] running_apps 3 | App[] available_apps -------------------------------------------------------------------------------- /srv/StartApp.srv: -------------------------------------------------------------------------------- 1 | # Name of the app to launch 2 | string name 3 | KeyValue[] args 4 | --- 5 | # true if app started, false otherwise 6 | bool started 7 | # if app did not start, error code for classifying start failure. See 8 | # StatusCodes.msg for common codes. 9 | int32 error_code 10 | # response message for debugging 11 | string message 12 | # Namespace where the app interface can be found 13 | string namespace 14 | 15 | -------------------------------------------------------------------------------- /srv/StopApp.srv: -------------------------------------------------------------------------------- 1 | # Name of app to stop. Sending "*" stops all apps. 2 | string name 3 | --- 4 | # true if app stopped, false otherwise 5 | bool stopped 6 | # true if app stopped by timeout, false otherwise 7 | bool timeout 8 | # if app did not stop, error code for classifying stop failure. See 9 | # StatusCodes.msg for common codes. 10 | int32 error_code 11 | string message 12 | -------------------------------------------------------------------------------- /srv/UninstallApp.srv: -------------------------------------------------------------------------------- 1 | # Name of app to uninstall 2 | string name 3 | --- 4 | # true if app stopped, false otherwise 5 | bool uninstalled 6 | string message 7 | -------------------------------------------------------------------------------- /test/appA.app: -------------------------------------------------------------------------------- 1 | display: Android Joystick 2 | description: Control the TurtleBot with an Android device 3 | platform: turtlebot 4 | launch: app_manager/example-min.launch 5 | interface: app_manager/empty.interface 6 | -------------------------------------------------------------------------------- /test/applist0/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | 6 | -------------------------------------------------------------------------------- /test/applist1/apps1.installed: -------------------------------------------------------------------------------- 1 | apps: 2 | - display: Android Joystick 3 | app: app_manager/appA 4 | -------------------------------------------------------------------------------- /test/applistbad/bad.installed: -------------------------------------------------------------------------------- 1 | bad: foo -------------------------------------------------------------------------------- /test/applistbad/bad2.installed: -------------------------------------------------------------------------------- 1 | apps: 2 | - bad: {} -------------------------------------------------------------------------------- /test/empty.interface: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PR2/app_manager/c23f10a5bcbb092b17fda8370d62d6d4d9747f11/test/empty.interface -------------------------------------------------------------------------------- /test/plugin/__init__.py: -------------------------------------------------------------------------------- 1 | from .test_plugin import TestPlugin 2 | -------------------------------------------------------------------------------- /test/plugin/package.xml: -------------------------------------------------------------------------------- 1 | 2 | app_manager_test_plugin 3 | 0.0.0 4 | test_plugin of app_manager 5 | ROS Orphaned Package Maintainers 6 | BSD 7 | 8 | app_manager 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/plugin/plugin.installed: -------------------------------------------------------------------------------- 1 | apps: 2 | - app: app_manager/test_plugin # look for plugin.app 3 | display: Test app plugin 4 | - app: app_manager/test_plugin_timeout 5 | display: Test app plugin timeout 6 | -------------------------------------------------------------------------------- /test/plugin/plugin.yaml: -------------------------------------------------------------------------------- 1 | # this file provide information of launch program and start/stop plugin code 2 | - name: test_plugin/test_plugin 3 | launch: app_manager/sample_node.xml 4 | module: test_plugin.TestPlugin 5 | -------------------------------------------------------------------------------- /test/plugin/sample_node.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | 5 | import rospy 6 | from std_msgs.msg import String 7 | 8 | 9 | def sample_node(): 10 | rospy.init_node('sample_node', anonymous=True) 11 | pub = rospy.Publisher('/test_plugin', String, queue_size=10) 12 | param1 = rospy.get_param('~param1') 13 | param2 = rospy.get_param('~param2') 14 | success = rospy.get_param('~success', False) 15 | fail = rospy.get_param('~fail', False) 16 | rospy.loginfo('~A started.'.format(rospy.get_name())) 17 | rospy.loginfo('param1: {}'.format(param1)) 18 | rospy.loginfo('param2: {}'.format(param2)) 19 | rate = rospy.Rate(10) # 10hz 20 | while not rospy.is_shutdown(): 21 | pub.publish("{{'param1': '{}', 'param2': '{}'}}".format(param1, param2)) 22 | if success: 23 | sys.exit(0) 24 | if fail: 25 | sys.exit(1) 26 | rate.sleep() 27 | 28 | if __name__ == '__main__': 29 | try: 30 | sample_node() 31 | except rospy.ROSInterruptException: 32 | pass 33 | -------------------------------------------------------------------------------- /test/plugin/sample_node.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | param1: $(arg param1) 10 | param2: $(arg param2) 11 | success: $(arg success) 12 | fail: $(arg fail) 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/plugin/test_plugin.app: -------------------------------------------------------------------------------- 1 | # this node shows app information 2 | display: Test app plugin 3 | platform: all 4 | launch: app_manager/sample_node.xml 5 | interface: app_manager/test_plugin.interface 6 | plugins: 7 | - name: test_plugin # name to identify this plugin 8 | type: test_plugin/test_plugin # plugin type, defiend in plugin.yml: name 9 | launch_args: # arguments for plugin launch file, defined in plugin.yml: launch 10 | param1: hello 11 | param2: world 12 | plugin_args: # default arguments for plugin function 13 | hoge: 10 14 | start_plugin_args: # arguments for start plugin function 15 | hoge: 100 # arguments for start plugin function arguments (it overwrites plugin_args hoge: 10 -> 100) 16 | fuga: 300 # arguments for start plugin function arguments (it overwrites plugin_args hoge: 10 -> 100) 17 | stop_plugin_args: # arguments for stop plugin function 18 | hoge: 1000 # arguments for start plugin function arguments (it overwrites plugin_args fuga: 300 -> 1000) 19 | fuga: 3000 # arguments for start plugin function arguments (it overwrites plugin_args fuga: 300 -> 1000) 20 | -------------------------------------------------------------------------------- /test/plugin/test_plugin.interface: -------------------------------------------------------------------------------- 1 | published_topics: {} 2 | subscribed_topics: {} 3 | -------------------------------------------------------------------------------- /test/plugin/test_plugin.py: -------------------------------------------------------------------------------- 1 | from app_manager import AppManagerPlugin 2 | from std_msgs.msg import String 3 | import rospy 4 | 5 | class TestPlugin(AppManagerPlugin): 6 | def __init__(self): 7 | super(TestPlugin, self).__init__() 8 | self.pub = rospy.Publisher('/test_plugin', String) 9 | #wait for listener 10 | while self.pub.get_num_connections() == 0: 11 | rospy.logwarn('wait for subscriber...') 12 | 13 | def app_manager_start_plugin(self, app, ctx, plugin_args): 14 | self.start_time = rospy.Time.now() 15 | self.pub.publish("{{'start_plugin': {}}}".format(plugin_args)) 16 | 17 | def app_manager_stop_plugin(self, app, ctx, plugin_args): 18 | self.pub.publish( 19 | "{{'stop_plugin': {}," 20 | "'exit_code': {}," 21 | "'stopped': {}," 22 | "'timeout': {}," 23 | "}}".format(plugin_args, 24 | ctx['exit_code'], 25 | ctx['stopped'], 26 | ctx['timeout'])) 27 | ctx['test_app_exit_code'] = 0 28 | return ctx 29 | -------------------------------------------------------------------------------- /test/plugin/test_plugin_timeout.app: -------------------------------------------------------------------------------- 1 | # this node shows app information 2 | display: Test app plugin 3 | platform: all 4 | launch: app_manager/sample_node.xml 5 | interface: app_manager/test_plugin.interface 6 | timeout: 0.5 7 | plugins: 8 | - name: test_plugin # name to identify this plugin 9 | type: test_plugin/test_plugin # plugin type, defiend in plugin.yml: name 10 | launch_args: # arguments for plugin launch file, defined in plugin.yml: launch 11 | param1: hello 12 | param2: world 13 | plugin_args: # default arguments for plugin function 14 | hoge: 10 15 | start_plugin_args: # arguments for start plugin function 16 | hoge: 100 # arguments for start plugin function arguments (it overwrites plugin_args hoge: 10 -> 100) 17 | fuga: 300 # arguments for start plugin function arguments (it overwrites plugin_args hoge: 10 -> 100) 18 | stop_plugin_args: # arguments for stop plugin function 19 | hoge: 1000 # arguments for start plugin function arguments (it overwrites plugin_args fuga: 300 -> 1000) 20 | fuga: 3000 # arguments for start plugin function arguments (it overwrites plugin_args fuga: 300 -> 1000) 21 | -------------------------------------------------------------------------------- /test/resources/example-min.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /test/test1.interface: -------------------------------------------------------------------------------- 1 | published_topics: 2 | /camera/rgb/image_color/compressed: sensor_msgs/CompressedImage 3 | subscribed_topics: 4 | /turtlebot_node/cmd_vel: geometry_msgs/Twist 5 | -------------------------------------------------------------------------------- /test/test_app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Software License Agreement (BSD License) 3 | # 4 | # Copyright (c) 2011, Willow Garage, Inc. 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions 9 | # are met: 10 | # 11 | # * Redistributions of source code must retain the above copyright 12 | # notice, this list of conditions and the following disclaimer. 13 | # * Redistributions in binary form must reproduce the above 14 | # copyright notice, this list of conditions and the following 15 | # disclaimer in the documentation and/or other materials provided 16 | # with the distribution. 17 | # * Neither the name of Willow Garage, Inc. nor the names of its 18 | # contributors may be used to endorse or promote products derived 19 | # from this software without specific prior written permission. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 24 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 25 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 26 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 27 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 30 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 31 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 32 | # POSSIBILITY OF SUCH DAMAGE. 33 | PKG = 'app_manager' 34 | import roslib; roslib.load_manifest(PKG) 35 | 36 | import os 37 | import sys 38 | import unittest 39 | 40 | import rospkg 41 | import rosunit 42 | 43 | class AppTest(unittest.TestCase): 44 | 45 | 46 | def test_Interface(self): 47 | from app_manager.app import Interface 48 | s1 = {'/chatter': 'std_msgs/String'} 49 | p1 = {'/chatter': 'std_msgs/String'} 50 | i1 = Interface(s1, p1) 51 | self.assertEquals(i1, i1) 52 | self.assertEquals(s1, i1.subscribed_topics) 53 | self.assertEquals(s1, i1.published_topics) 54 | 55 | i2 = Interface({'/chatter': 'std_msgs/String'}, {'/chatter': 'std_msgs/String'}) 56 | self.assertEquals(i1, i2) 57 | i3 = Interface({'/chatter2': 'std_msgs/String'}, {'/chatter': 'std_msgs/String'}) 58 | self.assertNotEquals(i1, i3) 59 | i4 = Interface({'/chatter': 'std_msgs/String'}, {'/chatter2': 'std_msgs/String'}) 60 | self.assertNotEquals(i1, i4) 61 | 62 | def test_Client(self): 63 | from app_manager.app import Client 64 | c1 = Client(client_type="android", 65 | manager_data={"manager1": "dataA"}, 66 | app_data={"app1": "dataB"}) 67 | self.assertEquals("android", c1.client_type) 68 | self.assertEquals({"manager1": "dataA"}, c1.manager_data) 69 | self.assertEquals({"app1": "dataB"}, c1.app_data) 70 | self.assertEquals(c1, c1) 71 | c2 = Client(client_type="android", 72 | manager_data={"manager1": "dataA"}, 73 | app_data={"app1": "dataB"}) 74 | self.assertEquals(c1, c2) 75 | noteq = [ 76 | Client("androidB", {"manager1": "dataA"}, 77 | {"app1": "dataB"}), 78 | Client("android", {"manager2": "dataA"}, 79 | {"app1": "dataB"}), 80 | Client("android", {"manager1": "dataA"}, 81 | {"app2": "dataB"}), 82 | ] 83 | for c3 in noteq: 84 | self.assertNotEquals(c1, c3) 85 | 86 | def test_AppDefinition(self): 87 | # name, display_name, description, platform, launch, interface, clients 88 | from app_manager.app import AppDefinition, Client 89 | ad1 = AppDefinition('foo', 'Foo', 'Is Foo', 'android', 'foo.launch', 'foo.interface', [Client("android", {}, {})]) 90 | self.assertEquals(ad1, ad1) 91 | ad2 = AppDefinition('foo', 'Foo', 'Is Foo', 'android', 'foo.launch', 'foo.interface', [Client("android", {}, {})]) 92 | self.assertEquals(ad1, ad2) 93 | ad3 = AppDefinition('foo', 'Foo', 'Is Foo', 'android', 'foo.launch', 'foo.interface', [Client("android", {}, {})], None) 94 | self.assertEquals(ad1, ad3) 95 | 96 | ad1b = AppDefinition('foo', 'Foo', 'Is Foo', 'android', 'foo.launch', 'foo.interface', [Client("android", {}, {})], 'app_manager/icon.png') 97 | self.assertEquals(ad1, ad1) 98 | ad2b = AppDefinition('foo', 'Foo', 'Is Foo', 'android', 'foo.launch', 'foo.interface', [Client("android", {}, {})], 'app_manager/icon.png') 99 | self.assertEquals(ad1b, ad2b) 100 | nad2b = AppDefinition('foo', 'Foo', 'Is Foo', 'android', 'foo.launch', 'foo.interface', [Client("android", {}, {})], 'app_manager/icon2.png') 101 | self.assertNotEquals(ad1, nad2b) 102 | 103 | noteq = [ 104 | AppDefinition('bar', 'Foo', 'Is Foo', 'android', 'foo.launch', 'foo.interface', [Client("android", {}, {})]), 105 | AppDefinition('foo', 'Bar', 'Is Foo', 'android', 'foo.launch', 'foo.interface', [Client("android", {}, {})]), 106 | AppDefinition('foo', 'Foo', 'Is Bar', 'android', 'foo.launch', 'foo.interface', [Client("android", {}, {})]), 107 | AppDefinition('foo', 'Foo', 'Is Foo', 'ios', 'foo.launch', 'foo.interface', [Client("android", {}, {})]), 108 | AppDefinition('foo', 'Foo', 'Is Foo', 'android', 'bar.launch', 'foo.interface', [Client("android", {}, {})]), 109 | AppDefinition('foo', 'Foo', 'Is Foo', 'android', 'foo.launch', 'bar.interface', [Client("android", {}, {})]), 110 | AppDefinition('foo', 'Foo', 'Is Foo', 'android', 'foo.launch', 'foo.interface', [Client("ios", {}, {})]), 111 | ] 112 | for nad in noteq: 113 | self.assertNotEquals(ad1, nad) 114 | 115 | def test_find_resource(self): 116 | from app_manager.app import find_resource 117 | rospack = rospkg.RosPack() 118 | path = rospack.get_path(PKG) 119 | test_dir = os.path.join(path, 'test') 120 | 121 | e = os.path.join(test_dir, 'empty.interface') 122 | self.assertEquals(e, find_resource('%s/empty.interface'%(PKG))) 123 | 124 | e = os.path.join(test_dir, 'applist1', 'apps1.installed') 125 | self.assertEquals(e, find_resource('%s/apps1.installed'%(PKG))) 126 | 127 | try: 128 | find_resource('empty.interface') 129 | self.fail("should have thrown ValueError: no package name") 130 | except ValueError: 131 | pass 132 | try: 133 | find_resource('app_manager') 134 | self.fail("should have thrown ValueError: no resource name") 135 | except ValueError: 136 | pass 137 | 138 | def test_load_AppDefinition_by_name(self): 139 | rospack = rospkg.RosPack() 140 | rl_dir = rospack.get_path('roslaunch') 141 | from app_manager import NotFoundException, InternalAppException 142 | from app_manager.app import load_AppDefinition_by_name, Interface 143 | try: 144 | load_AppDefinition_by_name(None) 145 | self.fail("should fail") 146 | except ValueError: pass 147 | try: 148 | load_AppDefinition_by_name("fake_pkg/appA") 149 | self.fail("should fail") 150 | except NotFoundException: pass 151 | 152 | ad = load_AppDefinition_by_name('app_manager/appA') 153 | self.assertEquals("app_manager/appA", ad.name) 154 | self.assertEquals("Android Joystick", ad.display_name) 155 | self.assertEquals("Control the TurtleBot with an Android device", ad.description) 156 | self.assertEquals("turtlebot", ad.platform) 157 | #self.assertEquals(os.path.join(rl_dir, 'resources', 'example-min.launch'), ad.launch) # example-min.launch location changed due to https://github.com/PR2/app_manager/pull/15 158 | self.assertEquals(os.path.join(rospack.get_path(PKG), 'test', 'resources', 'example-min.launch'), ad.launch) 159 | self.assert_(isinstance(ad.interface, Interface)) 160 | self.assertEquals({}, ad.interface.subscribed_topics) 161 | self.assertEquals({}, ad.interface.published_topics) 162 | self.assertEquals([], ad.clients) 163 | 164 | #monkey patch in for coverage 165 | import errno 166 | def fake_load_enoent(*args, **kwargs): 167 | raise IOError(errno.ENOENT, "fnf") 168 | def fake_load_gen(*args, **kwargs): 169 | raise IOError() 170 | import app_manager.app 171 | load_actual = app_manager.app.load_AppDefinition_from_file 172 | try: 173 | app_manager.app.load_AppDefinition_from_file = fake_load_enoent 174 | try: 175 | load_AppDefinition_by_name('app_manager/appA') 176 | self.fail("should have raised") 177 | except NotFoundException: pass 178 | 179 | app_manager.app.load_AppDefinition_from_file = fake_load_gen 180 | try: 181 | load_AppDefinition_by_name('app_manager/appA') 182 | self.fail("should have raised") 183 | except InternalAppException: pass 184 | finally: 185 | app_manager.app.load_AppDefinition_from_file = load_actual 186 | 187 | def test_load_AppDefinition_from_file(self): 188 | rospack = rospkg.RosPack() 189 | rl_dir = rospack.get_path('roslaunch') 190 | path = rospack.get_path(PKG) 191 | test_dir = os.path.join(path, 'test') 192 | 193 | from app_manager.app import load_AppDefinition_from_file, Interface 194 | ad = load_AppDefinition_from_file(os.path.join(test_dir, 'appA.app'), 'foo/AppA') 195 | self.assertEquals("foo/AppA", ad.name) 196 | self.assertEquals("Android Joystick", ad.display_name) 197 | self.assertEquals("Control the TurtleBot with an Android device", ad.description) 198 | self.assertEquals("turtlebot", ad.platform) 199 | #self.assertEquals(os.path.join(rl_dir, 'resources', 'example-min.launch'), ad.launch) # example-min.launch location changed due to https://github.com/PR2/app_manager/pull/15 200 | self.assertEquals(os.path.join(test_dir, 'resources', 'example-min.launch'), ad.launch) 201 | self.assert_(isinstance(ad.interface, Interface)) 202 | self.assertEquals({}, ad.interface.subscribed_topics) 203 | self.assertEquals({}, ad.interface.published_topics) 204 | self.assertEquals([], ad.clients) 205 | #self.assertEquals('app_manager/empty.interface', ad.interface) 206 | 207 | def test_load_Interface_from_file(self): 208 | from app_manager.app import load_Interface_from_file 209 | rospack = rospkg.RosPack() 210 | path = rospack.get_path(PKG) 211 | test_dir = os.path.join(path, 'test') 212 | 213 | empty = load_Interface_from_file(os.path.join(test_dir, 'empty.interface')) 214 | self.assertEquals({}, empty.subscribed_topics) 215 | self.assertEquals({}, empty.published_topics) 216 | 217 | test1 = load_Interface_from_file(os.path.join(test_dir, 'test1.interface')) 218 | self.assertEquals({'/camera/rgb/image_color/compressed': 'sensor_msgs/CompressedImage'}, test1.published_topics) 219 | self.assertEquals({'/turtlebot_node/cmd_vel': 'geometry_msgs/Twist'}, test1.subscribed_topics) 220 | 221 | 222 | if __name__ == '__main__': 223 | rosunit.unitrun(PKG, 'test_app', AppTest, coverage_packages=['app_manager.app']) 224 | 225 | -------------------------------------------------------------------------------- /test/test_app.test: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/test_app_list.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Software License Agreement (BSD License) 3 | # 4 | # Copyright (c) 2011, Willow Garage, Inc. 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions 9 | # are met: 10 | # 11 | # * Redistributions of source code must retain the above copyright 12 | # notice, this list of conditions and the following disclaimer. 13 | # * Redistributions in binary form must reproduce the above 14 | # copyright notice, this list of conditions and the following 15 | # disclaimer in the documentation and/or other materials provided 16 | # with the distribution. 17 | # * Neither the name of Willow Garage, Inc. nor the names of its 18 | # contributors may be used to endorse or promote products derived 19 | # from this software without specific prior written permission. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 24 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 25 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 26 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 27 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 30 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 31 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 32 | # POSSIBILITY OF SUCH DAMAGE. 33 | PKG = 'app_manager' 34 | import roslib; roslib.load_manifest(PKG) 35 | 36 | import os 37 | import sys 38 | import unittest 39 | 40 | import rospkg 41 | import rosunit 42 | 43 | def touch(filename): 44 | os.utime(filename, None) 45 | 46 | class AppListTest(unittest.TestCase): 47 | 48 | def test_AppList(self): 49 | import app_manager 50 | rospack = rospkg.RosPack() 51 | path = rospack.get_path(PKG) 52 | test_dir = os.path.join(path, 'test') 53 | 54 | app_list = app_manager.AppList([os.path.join(test_dir, 'applist0')]) 55 | self.assertEquals([], app_list.get_app_list()) 56 | 57 | filename = os.path.join(test_dir, 'applist1') 58 | app_list = app_manager.AppList([filename]) 59 | al = app_list.get_app_list() 60 | self.assertEquals([], app_list.invalid_installed_files) 61 | #self.assertEquals(1, len(al), al.invalid_installed_files) 62 | self.assertEquals('Android Joystick', al[0].display_name ) 63 | 64 | #Had to be commented out, see app_list.py 65 | #mtime = app_list._applist_directory_mtime 66 | #app_list.update() 67 | #self.assertEquals(mtime, app_list._applist_directory_mtime) 68 | #touch(filename) 69 | #app_list.update() 70 | #self.assertNotEquals(mtime, app_list._applist_directory_mtime) 71 | 72 | filename = os.path.join(test_dir, 'applistbad') 73 | app_list = app_manager.AppList([filename]) 74 | al = app_list.get_app_list() 75 | self.assertEquals([], al) 76 | self.assertEquals(2, len(app_list.invalid_installed_files)) 77 | 78 | def test_get_default_applist_directory(self): 79 | import app_manager.app_list 80 | self.assertEquals('/etc/robot/apps', app_manager.app_list.get_default_applist_directory()) 81 | 82 | def test_InstalledFile(self): 83 | from app_manager import InvalidAppException 84 | from app_manager.app import find_resource 85 | from app_manager.app_list import InstalledFile 86 | 87 | filename = find_resource('app_manager/apps1.installed') 88 | inf = InstalledFile(filename) 89 | self.failIf(inf._file_mtime is None) 90 | self.assertEquals(filename, inf.filename) 91 | self.assertEquals(1, len(inf.available_apps)) 92 | self.assertEquals('Android Joystick', inf.available_apps[0].display_name) 93 | 94 | #Had to be commented out, see app_list.py 95 | #mtime = inf._file_mtime 96 | #inf.update() 97 | #self.assertEquals(mtime, inf._file_mtime) 98 | #touch(filename) 99 | #inf.update() 100 | #self.assertNotEquals(mtime, inf._file_mtime) 101 | 102 | for bad in ['app_manager/bad.installed', 'app_manager/bad2.installed']: 103 | filename = find_resource(bad) 104 | try: 105 | inf = InstalledFile(filename) 106 | self.fail("should have thrown") 107 | except InvalidAppException: pass 108 | 109 | def test_dict_to_KeyValue(self): 110 | from app_manager.msg import KeyValue 111 | from app_manager.app_list import dict_to_KeyValue 112 | 113 | v = dict_to_KeyValue({}) 114 | self.assertEquals([], v) 115 | 116 | v = dict_to_KeyValue({'a': 'b'}) 117 | self.assertEquals([KeyValue('a', 'b')], v) 118 | 119 | v = dict_to_KeyValue({'a': 'b', 'c': 'd'}) 120 | for ve in [KeyValue('a', 'b'), KeyValue('c', 'd')]: 121 | self.assert_(ve in v) 122 | 123 | # make sure that types convert 124 | v = dict_to_KeyValue({'a': 1}) 125 | self.assertEquals([KeyValue('a', '1')], v) 126 | 127 | def test_AppDefinition_to_App(self): 128 | from app_manager.msg import App, ClientApp, KeyValue 129 | from app_manager.app import AppDefinition, Client 130 | from app_manager.app_list import AppDefinition_to_App, dict_to_KeyValue 131 | 132 | ad = AppDefinition(name="appname", display_name="An App", 133 | description="Does something", platform="fakebot", 134 | launch="file.launch", interface="file.interface", clients=[]) 135 | a = AppDefinition_to_App(ad) 136 | self.assertEquals(a.name, 'appname') 137 | self.assertEquals(a.display_name, 'An App') 138 | self.assertEquals([], a.client_apps) 139 | 140 | client1 = Client('android', 141 | {'manager1': 'data1'}, 142 | {'app1': 'data1'}) 143 | ca = ClientApp('android', [KeyValue('manager1', 'data1')], [KeyValue('app1', 'data1')]) 144 | ad = AppDefinition(name="appname", display_name="An App", 145 | description="Does something", platform="fakebot", 146 | launch="file.launch", interface="file.interface", clients=[client1]) 147 | a = AppDefinition_to_App(ad) 148 | self.assertEquals([ca], a.client_apps) 149 | 150 | client1 = Client('android', 151 | {'manager1': 'data1', 'manager2': 'data2'}, 152 | {'app1': 'data1', 'app2': 'data2'}) 153 | ca = ClientApp('android', dict_to_KeyValue(client1.manager_data), dict_to_KeyValue(client1.app_data)) 154 | ad = AppDefinition(name="appname", display_name="An App", 155 | description="Does something", platform="fakebot", 156 | launch="file.launch", interface="file.interface", clients=[client1]) 157 | a = AppDefinition_to_App(ad) 158 | self.assertEquals([ca], a.client_apps) 159 | 160 | client2 = Client('web', {}, 161 | {'app2': 'data2', 'app2b': 'data2b'}) 162 | ca2 = ClientApp('web', [], dict_to_KeyValue(client2.app_data)) 163 | ad = AppDefinition(name="appname", display_name="An App", 164 | description="Does something", platform="fakebot", 165 | launch="file.launch", interface="file.interface", clients=[client1, client2]) 166 | a = AppDefinition_to_App(ad) 167 | self.assertEquals([ca, ca2], a.client_apps) 168 | 169 | 170 | if __name__ == '__main__': 171 | rosunit.unitrun(PKG, 'test_app_list', AppListTest, coverage_packages=['app_manager.app_list']) 172 | 173 | -------------------------------------------------------------------------------- /test/test_plugin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Software License Agreement (BSD License) 3 | # 4 | # Copyright (c) 2011, Willow Garage, Inc. 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions 9 | # are met: 10 | # 11 | # * Redistributions of source code must retain the above copyright 12 | # notice, this list of conditions and the following disclaimer. 13 | # * Redistributions in binary form must reproduce the above 14 | # copyright notice, this list of conditions and the following 15 | # disclaimer in the documentation and/or other materials provided 16 | # with the distribution. 17 | # * Neither the name of Willow Garage, Inc. nor the names of its 18 | # contributors may be used to endorse or promote products derived 19 | # from this software without specific prior written permission. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 24 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 25 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 26 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 27 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 30 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 31 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 32 | # POSSIBILITY OF SUCH DAMAGE. 33 | 34 | PKG = 'app_manager' 35 | 36 | import os 37 | import sys 38 | import time 39 | import unittest 40 | 41 | import rospy 42 | import rostest 43 | import rosunit 44 | 45 | from app_manager.msg import KeyValue 46 | from app_manager.srv import * 47 | from std_msgs.msg import String 48 | 49 | class StopAppTest(unittest.TestCase): 50 | 51 | def __init__(self, *args): 52 | super(StopAppTest, self).__init__(*args) 53 | rospy.init_node('stop_app_test') 54 | 55 | def cb(self, msg): 56 | rospy.logwarn("{} received".format(msg)) 57 | self.msg = msg 58 | message = eval(msg.data) 59 | if ('start_plugin' in message 60 | and message['start_plugin']['fuga'] == 300 61 | and message['start_plugin']['hoge'] == 100): 62 | self.msg_started = True 63 | if ('stop_plugin' in message 64 | and 'exit_code' in message 65 | and 'stopped' in message 66 | and 'timeout' in message 67 | and message['stop_plugin']['fuga'] == 3000 68 | and message['stop_plugin']['hoge'] == 1000): 69 | self.msg_ctx_exit_code = message['exit_code'] 70 | self.msg_ctx_stopped = message['stopped'] 71 | self.msg_ctx_timeout = message['timeout'] 72 | self.msg_stopped = True 73 | if ('param1' in message 74 | and 'param2' in message 75 | and message['param1'] == 'hello' 76 | and message['param2'] == 'world'): 77 | self.msg_plugin_started = True 78 | if ('param1' in message 79 | and 'param2' in message 80 | and message['param1'] == 'param1' 81 | and message['param2'] == 'param2'): 82 | self.msg_app_started = True 83 | self.msg_received = self.msg_received + 1 84 | 85 | def setUp(self): 86 | self.msg = None 87 | self.msg_received = 0 88 | self.msg_started = False 89 | self.msg_stopped = False 90 | self.msg_plugin_started = False 91 | self.msg_app_started = False 92 | self.msg_ctx_exit_code = None 93 | self.msg_ctx_stopped = None 94 | self.msg_ctx_timeout = None 95 | rospy.Subscriber('/test_plugin', String, self.cb) 96 | rospy.wait_for_service('/robot/list_apps') 97 | rospy.wait_for_service('/robot/start_app') 98 | rospy.wait_for_service('/robot/stop_app') 99 | self.list = rospy.ServiceProxy('/robot/list_apps', ListApps) 100 | self.start = rospy.ServiceProxy('/robot/start_app', StartApp) 101 | self.stop = rospy.ServiceProxy('/robot/stop_app', StopApp) 102 | 103 | def test_start_stop_app(self): 104 | # wait for plugins 105 | list_req = ListAppsRequest() 106 | list_res = ListAppsResponse() 107 | while not 'app_manager/test_plugin' in list(map(lambda x: x.name, list_res.available_apps)): 108 | list_res = self.list.call(list_req) 109 | # rospy.logwarn("received 'list_apps' {}".format(list_res)) 110 | time.sleep(1) 111 | # start plugin 112 | start_req = StartAppRequest(name='app_manager/test_plugin') 113 | start_res = self.start.call(start_req) 114 | rospy.logwarn('start app {}'.format(start_res)) 115 | self.assertEqual(start_res.error_code, 0) 116 | while (not rospy.is_shutdown() 117 | and not self.msg_started): 118 | rospy.logwarn('Wait for start message received..') 119 | rospy.sleep(1) 120 | 121 | # check app and plugin both started 122 | while (not rospy.is_shutdown() 123 | and not self.msg_app_started 124 | and not self.msg_plugin_started): 125 | rospy.logwarn('Wait for app/plugin message received..') 126 | rospy.sleep(1) 127 | 128 | # stop plugin 129 | stop_req = StopAppRequest(name='app_manager/test_plugin') 130 | stop_res = self.stop.call(stop_req) 131 | rospy.logwarn('stop app {}'.format(stop_res)) 132 | self.assertEqual(stop_res.error_code, 0) 133 | 134 | while (not rospy.is_shutdown() 135 | and not self.msg_stopped): 136 | rospy.logwarn('Wait for stop message received..') 137 | rospy.sleep(1) 138 | 139 | self.assertEqual(self.msg_ctx_exit_code, None) 140 | self.assertEqual(self.msg_ctx_stopped, True) 141 | self.assertEqual(self.msg_ctx_timeout, None) 142 | 143 | def test_start_stop_app_timeout(self): 144 | # wait for plugins 145 | list_req = ListAppsRequest() 146 | list_res = ListAppsResponse() 147 | while not 'app_manager/test_plugin' in list(map(lambda x: x.name, list_res.available_apps)): 148 | list_res = self.list.call(list_req) 149 | # rospy.logwarn("received 'list_apps' {}".format(list_res)) 150 | time.sleep(1) 151 | # start plugin 152 | start_req = StartAppRequest(name='app_manager/test_plugin_timeout') 153 | start_res = self.start.call(start_req) 154 | rospy.logwarn('start app {}'.format(start_res)) 155 | self.assertEqual(start_res.error_code, 0) 156 | while (not rospy.is_shutdown() 157 | and not self.msg_started): 158 | rospy.logwarn('Wait for start message received..') 159 | rospy.sleep(1) 160 | 161 | # check app and plugin both started 162 | while (not rospy.is_shutdown() 163 | and not self.msg_app_started 164 | and not self.msg_plugin_started): 165 | rospy.logwarn('Wait for app/plugin message received..') 166 | rospy.sleep(1) 167 | 168 | while (not rospy.is_shutdown() 169 | and not self.msg_stopped): 170 | rospy.logwarn('Wait for stop message received..') 171 | rospy.sleep(1) 172 | 173 | self.assertEqual(self.msg_ctx_exit_code, None) 174 | self.assertEqual(self.msg_ctx_stopped, True) 175 | self.assertEqual(self.msg_ctx_timeout, True) 176 | 177 | def test_start_stop_app_fail(self): 178 | # wait for plugins 179 | list_req = ListAppsRequest() 180 | list_res = ListAppsResponse() 181 | while not 'app_manager/test_plugin' in list(map(lambda x: x.name, list_res.available_apps)): 182 | list_res = self.list.call(list_req) 183 | # rospy.logwarn("received 'list_apps' {}".format(list_res)) 184 | time.sleep(1) 185 | # start plugin 186 | start_req = StartAppRequest( 187 | name='app_manager/test_plugin', 188 | args=[KeyValue(key='fail', value='true')]) 189 | start_res = self.start.call(start_req) 190 | rospy.logwarn('start app {}'.format(start_res)) 191 | self.assertEqual(start_res.error_code, 0) 192 | while (not rospy.is_shutdown() 193 | and not self.msg_started): 194 | rospy.logwarn('Wait for start message received..') 195 | rospy.sleep(1) 196 | 197 | # check app and plugin both started 198 | while (not rospy.is_shutdown() 199 | and not self.msg_app_started 200 | and not self.msg_plugin_started): 201 | rospy.logwarn('Wait for app/plugin message received..') 202 | rospy.sleep(1) 203 | 204 | while (not rospy.is_shutdown() 205 | and not self.msg_stopped): 206 | rospy.logwarn('Wait for stop message received..') 207 | rospy.sleep(1) 208 | 209 | self.assertEqual(self.msg_ctx_exit_code, 1) 210 | self.assertEqual(self.msg_ctx_stopped, None) 211 | self.assertEqual(self.msg_ctx_timeout, None) 212 | 213 | def test_start_stop_app_success(self): 214 | # wait for plugins 215 | list_req = ListAppsRequest() 216 | list_res = ListAppsResponse() 217 | while not 'app_manager/test_plugin' in list(map(lambda x: x.name, list_res.available_apps)): 218 | list_res = self.list.call(list_req) 219 | # rospy.logwarn("received 'list_apps' {}".format(list_res)) 220 | time.sleep(1) 221 | # start plugin 222 | start_req = StartAppRequest( 223 | name='app_manager/test_plugin', 224 | args=[KeyValue(key='success', value='true')]) 225 | start_res = self.start.call(start_req) 226 | rospy.logwarn('start app {}'.format(start_res)) 227 | self.assertEqual(start_res.error_code, 0) 228 | while (not rospy.is_shutdown() 229 | and not self.msg_started): 230 | rospy.logwarn('Wait for start message received..') 231 | rospy.sleep(1) 232 | 233 | # check app and plugin both started 234 | while (not rospy.is_shutdown() 235 | and not self.msg_app_started 236 | and not self.msg_plugin_started): 237 | rospy.logwarn('Wait for app/plugin message received..') 238 | rospy.sleep(1) 239 | 240 | while (not rospy.is_shutdown() 241 | and not self.msg_stopped): 242 | rospy.logwarn('Wait for stop message received..') 243 | rospy.sleep(1) 244 | 245 | self.assertEqual(self.msg_ctx_exit_code, 0) 246 | self.assertEqual(self.msg_ctx_stopped, None) 247 | self.assertEqual(self.msg_ctx_timeout, None) 248 | 249 | 250 | if __name__ == '__main__': 251 | try: 252 | rostest.run('stop_app_test', PKG, StopAppTest, sys.argv) 253 | except KeyboardInterrupt: 254 | pass 255 | print("{} exiting".format(PKG)) 256 | -------------------------------------------------------------------------------- /test/test_plugin.test: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/test_plugin_fail.test: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/test_plugin_success.test: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/test_plugin_timeout.test: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/test_start_fail.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Software License Agreement (BSD License) 3 | # 4 | # Copyright (c) 2011, Willow Garage, Inc. 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions 9 | # are met: 10 | # 11 | # * Redistributions of source code must retain the above copyright 12 | # notice, this list of conditions and the following disclaimer. 13 | # * Redistributions in binary form must reproduce the above 14 | # copyright notice, this list of conditions and the following 15 | # disclaimer in the documentation and/or other materials provided 16 | # with the distribution. 17 | # * Neither the name of Willow Garage, Inc. nor the names of its 18 | # contributors may be used to endorse or promote products derived 19 | # from this software without specific prior written permission. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 24 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 25 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 26 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 27 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 30 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 31 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 32 | # POSSIBILITY OF SUCH DAMAGE. 33 | 34 | PKG = 'app_manager' 35 | 36 | import os 37 | import sys 38 | import time 39 | import unittest 40 | 41 | import rospy 42 | import rostest 43 | import rosunit 44 | 45 | from app_manager.msg import * 46 | from app_manager.srv import * 47 | 48 | from std_msgs.msg import String 49 | 50 | class StartFailTest(unittest.TestCase): 51 | 52 | def __init__(self, *args): 53 | super(StartFailTest, self).__init__(*args) 54 | rospy.init_node('start_fail_test') 55 | 56 | def cb(self, msg): 57 | rospy.logwarn("{} received".format(msg)) 58 | self.msg = msg 59 | self.msg_received = self.msg_received + 1 60 | 61 | def setUp(self): 62 | rospy.wait_for_service('/robot/list_apps') 63 | rospy.wait_for_service('/robot/stop_app') 64 | rospy.wait_for_service('/robot/start_app') 65 | self.list = rospy.ServiceProxy('/robot/list_apps', ListApps) 66 | self.stop = rospy.ServiceProxy('/robot/stop_app', StopApp) 67 | self.start = rospy.ServiceProxy('/robot/start_app', StartApp) 68 | self.msg = None 69 | self.msg_received = 0 70 | rospy.Subscriber('/chatter', String, self.cb) 71 | 72 | def test_start_fail_app(self): 73 | # wait for application 74 | rospy.logwarn("Wait for application") 75 | list_req = ListAppsRequest() 76 | list_res = ListAppsResponse() 77 | while not 'app_manager/appA' in list(map(lambda x: x.name, list_res.available_apps)): 78 | list_res = self.list.call(list_req) 79 | # rospy.logwarn("received 'list_apps' {}".format(list_res)) 80 | time.sleep(1) 81 | 82 | # intentionally failed to start 83 | rospy.logwarn("Start application with wrong arguments") 84 | start_req = StartAppRequest(name='app_manager/appA', args=[KeyValue('launch_prefix', 'no_command')]) 85 | start_res = self.start.call(start_req) 86 | rospy.logwarn("start 'started_app' {}".format(start_res)) 87 | self.assertEqual(start_res.started, False) 88 | 89 | # confirm if application is failed to start 90 | rospy.logwarn("Check running application") 91 | list_req = ListAppsRequest() 92 | list_res = ListAppsResponse(running_apps=[App(name='app_manager/appA')]) 93 | while 'app_manager/appA' in list(map(lambda x: x.name, list_res.running_apps)): 94 | list_res = self.list.call(list_req) 95 | # rospy.logwarn("received 'list_apps' {}".format(list_res)) 96 | time.sleep(1) 97 | break 98 | 99 | # start app and check if actually started 100 | rospy.logwarn("Start application") 101 | start_req = StartAppRequest(name='app_manager/appA', args=[KeyValue('launch_prefix', 'python{}'.format(os.environ['ROS_PYTHON_VERSION']))]) 102 | start_res = self.start.call(start_req) 103 | rospy.logwarn("received 'started_app' {}".format(start_res)) 104 | self.assertEqual(start_res.started, True) 105 | 106 | # check if msg received 107 | while (not rospy.is_shutdown()) and self.msg == None: 108 | rospy.logwarn('Wait for /chatter message received..') 109 | rospy.sleep(1) 110 | 111 | # stop plugin 112 | stop_req = StopAppRequest(name='app_manager/appA') 113 | stop_res = self.stop.call(stop_req) 114 | rospy.logwarn('stop app {}'.format(stop_res)) 115 | self.assertEqual(stop_res.stopped, True) 116 | 117 | if __name__ == '__main__': 118 | try: 119 | rostest.run('start_fail_test', PKG, StartFailTest, sys.argv) 120 | except KeyboardInterrupt: 121 | pass 122 | print("{} exiting".format(PKG)) 123 | -------------------------------------------------------------------------------- /test/test_start_fail.test: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/test_stop_app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Software License Agreement (BSD License) 3 | # 4 | # Copyright (c) 2011, Willow Garage, Inc. 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions 9 | # are met: 10 | # 11 | # * Redistributions of source code must retain the above copyright 12 | # notice, this list of conditions and the following disclaimer. 13 | # * Redistributions in binary form must reproduce the above 14 | # copyright notice, this list of conditions and the following 15 | # disclaimer in the documentation and/or other materials provided 16 | # with the distribution. 17 | # * Neither the name of Willow Garage, Inc. nor the names of its 18 | # contributors may be used to endorse or promote products derived 19 | # from this software without specific prior written permission. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 24 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 25 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 26 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 27 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 30 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 31 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 32 | # POSSIBILITY OF SUCH DAMAGE. 33 | 34 | PKG = 'app_manager' 35 | 36 | import os 37 | import sys 38 | import time 39 | import unittest 40 | 41 | import rospy 42 | import rostest 43 | import rosunit 44 | 45 | from app_manager.srv import * 46 | 47 | class StopAppTest(unittest.TestCase): 48 | 49 | def __init__(self, *args): 50 | super(StopAppTest, self).__init__(*args) 51 | rospy.init_node('stop_app_test') 52 | 53 | def setUp(self): 54 | rospy.wait_for_service('/robot/list_apps') 55 | rospy.wait_for_service('/robot/stop_app') 56 | self.list = rospy.ServiceProxy('/robot/list_apps', ListApps) 57 | self.stop = rospy.ServiceProxy('/robot/stop_app', StopApp) 58 | 59 | def test_stop_app(self): 60 | list_req = ListAppsRequest() 61 | list_res = ListAppsResponse() 62 | while not 'app_manager/appA' in list(map(lambda x: x.name, list_res.running_apps)): 63 | list_res = self.list.call(list_req) 64 | # rospy.logwarn("received 'list_apps' {}".format(list_res)) 65 | time.sleep(1) 66 | 67 | stop_req = StopAppRequest(name='app_manager/appA') 68 | stop_res = self.stop.call(stop_req) 69 | rospy.logwarn("received 'stop_app' {}".format(stop_res)) 70 | self.assertEqual(stop_res.stopped, True) 71 | 72 | if __name__ == '__main__': 73 | try: 74 | rostest.run('stop_app_test', PKG, StopAppTest, sys.argv) 75 | except KeyboardInterrupt: 76 | pass 77 | print("{} exiting".format(PKG)) 78 | --------------------------------------------------------------------------------