├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.rst ├── LICENSE ├── README.md ├── doc ├── conf.py ├── index.rst ├── install.rst ├── jobs.rst ├── providers.rst ├── sphinxarg │ ├── README.md │ ├── __init__.py │ ├── ext.py │ └── parser.py └── uninstall.rst ├── package.xml ├── resource └── robot_upstart ├── robot_upstart ├── __init__.py ├── install_script.py ├── job.py ├── providers.py └── uninstall_script.py ├── rosdoc.yaml ├── scripts ├── getifip ├── install ├── mklaunch ├── mkxacro ├── mutate_files └── uninstall ├── setup.cfg ├── setup.py ├── templates ├── job-start.em ├── job-stop.em ├── job.conf.em └── systemd_job.conf.em └── test ├── launch ├── a.launch └── b.launch └── test_basics.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: robot_upstart_ci 2 | 3 | on: 4 | push: 5 | branches: [foxy-devel] 6 | pull_request: 7 | branches: [foxy-devel] 8 | 9 | jobs: 10 | jazzy_ci: 11 | name: Jazzy 12 | runs-on: ubuntu-24.04 13 | steps: 14 | - uses: actions/checkout@v2.3.4 15 | - uses: ros-tooling/setup-ros@v0.7 16 | with: 17 | required-ros-distributions: jazzy 18 | - uses: ros-tooling/action-ros-ci@v0.3 19 | id: action_ros_ci_step 20 | with: 21 | target-ros2-distro: jazzy 22 | import-token: ${{ secrets.GITHUB_TOKEN }} 23 | skip-tests: false 24 | package-name: 25 | robot_upstart 26 | 27 | humble_ci: 28 | name: Humble 29 | runs-on: ubuntu-22.04 30 | steps: 31 | - uses: actions/checkout@v2.3.4 32 | - uses: ros-tooling/setup-ros@v0.7 33 | with: 34 | required-ros-distributions: humble 35 | - uses: ros-tooling/action-ros-ci@v0.3 36 | id: action_ros_ci_step 37 | with: 38 | target-ros2-distro: humble 39 | import-token: ${{ secrets.GITHUB_TOKEN }} 40 | skip-tests: false 41 | package-name: 42 | robot_upstart -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.py[oc] 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: generic 2 | services: 3 | - docker 4 | 5 | env: 6 | matrix: 7 | - ROS_DISTRO="noetic" 8 | 9 | install: 10 | - git clone --quiet --depth 1 https://github.com/ros-industrial/industrial_ci.git .industrial_ci -b master 11 | script: 12 | - .industrial_ci/travis.sh 13 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 | Changelog for package robot_upstart 3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 | 5 | 1.0.4 (2024-09-09) 6 | ------------------ 7 | * Make tests pass on ROS 2 (`#124 `_) 8 | * Add Github CI 9 | * Contributors: Chris Iverach-Brereton 10 | 11 | 1.0.3 (2023-07-14) 12 | ------------------ 13 | * Source workspace after exporting domain ID and RMW 14 | * Contributors: Roni Kreinin 15 | 16 | 1.0.2 (2022-04-21) 17 | ------------------ 18 | * Removed whitespace 19 | * Use a single rmw_config arg for both fastrtps and cyclonedds 20 | * Added ROS_DOMAIN_ID arg 21 | * Removed master_uri, added rmw, cyclonedds_config, and fastrtps_config 22 | * Contributors: Roni Kreinin 23 | 24 | 1.0.1 (2022-04-07) 25 | ------------------ 26 | * Added ament_index_python as run dep. 27 | * Removed un-used import. 28 | * Switched setup.cfg parameters to use underscores. 29 | * Updated setup.py to version 1.0.0. 30 | * Contributors: Tony Baltovski 31 | 32 | 1.0.0 (2022-02-16) 33 | ------------------ 34 | * Fixed package.xml and setup.py information. 35 | * Removed CMakeLists.txt. 36 | * Removed src folder. 37 | * ros2 foxy 38 | * Contributors: Tony Baltovski, zifengqi123 39 | 40 | 0.4.2 (2022-02-16) 41 | ------------------ 42 | * Use setpriv instead of setuidgid 43 | Replicates change from melodic-devel. Setpriv allows use of group permissions, reducing the need to globally apply r/w permissions to devices. 44 | * empty -> EmPy 45 | * Added License. 46 | * Contributors: Chris I-B, Mikael Arguedas, Tony Baltovski 47 | 48 | 0.4.1 (2021-05-12) 49 | ------------------ 50 | * Enable customization of After= in service (`#104 `_) 51 | * Cosmetic 52 | Remove unnecessary parenthesis. 53 | * Correct typo 54 | * FIX: Remove unreachable code 55 | * Enable customizable After= in service 56 | This feature enables the user to define the services after which the 57 | generated service will. This is handy when hardware-related system 58 | services have to start before the ROS software. 59 | * Added util-linux as dependency for setpriv. 60 | * Bumped CMake version to avoid author warning. 61 | * Contributors: Tkostas, Tony Baltovski 62 | 63 | 0.4.0 (2021-03-01) 64 | ------------------ 65 | * Updated maintainers. 66 | * fix remaining roslint error 67 | * fix most roslint failures 68 | * run ci on ROS noetic 69 | * changed unittest file to use env python3 70 | * updated to work with python3 71 | * Update the python scripts to be python3-compatible 72 | * [doc] Add commands when systemd is chosen. (`#78 `_) 73 | When `systemd` is specified as a provider, commands are different. 74 | https://wiki.ubuntu.com/SystemdForUpstartUsers 75 | [doc] Add systemd start/stop commands. 76 | * Melodic compatibility: modify getifip for bionic output (`#88 `_) 77 | * Add net-tools as dependency because ifconfig is used 78 | * Modify getifip to handle bionic ifconfig output 79 | * Break line that the linter doesn't like. (`#87 `_) 80 | * [doc] Clarify the logic for automated job name determination. (`#82 `_) 81 | Problem addressed 82 | ================= 83 | When `--job` option is not passed to `rosrun robot_upstart install`, the job name gets determined automatically but the logic of it is not clear. 84 | Solution to the problem 85 | ======================= 86 | Add an explanation to the document. 87 | * [CI][kinetic-devel] Update to Xenial. (`#79 `_) 88 | * [CI][kinetic-devel] Update to Xenial. 89 | On a PR https://github.com/clearpathrobotics/robot_upstart/pull/78 I saw [CI failure](https://travis-ci.org/clearpathrobotics/robot_upstart/builds/507510733?utm_source=github_status&utm_medium=notification) that seems to be related to platform issue. Using `trusty` for xenial-based job might not work (any more?). 90 | * [CI] Switch to industrial_ci. Add ROS2 dashing. 91 | * [CI] Remove ROS2 dashing for now (see https://github.com/clearpathrobotics/robot_upstart/pull/79#issuecomment-533908848). 92 | * Add support for wait flag in the install script (`#73 `_) 93 | * Contributors: Chris I-B, Frederik Mazur Andersen, Isaac I.Y. Saito, Mateusz Sadowski, Mikael Arguedas, Mike Purvis, Ramon Wijnands, Tony Baltovski 94 | 95 | 0.3.0 (2018-05-23) 96 | ------------------ 97 | * Add a dependency onto network-online.target (`#67 `_) 98 | * Clarify the reason of the error due to wrong pkgpath passed. (`#57 `_) 99 | * Allow ROS_HOME to be set previously by env file. (`#54 `_) 100 | * Contributors: Isaac I.Y. Saito, Thomas Furfaro, mhosmar-cpr 101 | 102 | 0.2.2 (2017-01-23) 103 | ------------------ 104 | * Added a spin wait until ros processes exit. (`#40 `_) 105 | * Moved detect_providers to providers.py (`#46 `_) 106 | * Miscellaneous source code fixups. 107 | * Contributors: Mike Purvis, Tony Baltovski, Zac Witte 108 | 109 | 0.2.1 (2016-12-19) 110 | ------------------ 111 | * Added option to install under systemd rather than upstart (`#41 `_) 112 | * Added option to add launch files as symbolic link (`#43 `_) 113 | * Fix title underline to silence doc job warning. 114 | * Update README.md 115 | Use `latest_available` URL for documentation link. 116 | * Merge pull request `#31 `_ from clearpathrobotics/roslint_fix 117 | Remove unwanted whitespace 118 | * Remove unwanted whitespace 119 | * Merge pull request `#28 `_ from clearpathrobotics/install_multiple_files 120 | Updated install script to allow adding multiple launch files to a job 121 | * Ensure script aborts if one of the provided launch files cannot be found 122 | * Updated install script to allow adding multiple launch files to a job at once 123 | * Fix leftover {user} tokens in template. 124 | * Formatting changes for new pep8. 125 | * Contributors: Jonathan Jekir, Kazumi Malhan, Mike Purvis, Niklas Casaril 126 | 127 | 0.2.0 (2015-03-14) 128 | ------------------ 129 | * Linter fixes. 130 | * Contributors: Mike Purvis 131 | 132 | 0.1.2 (2015-03-13) 133 | ------------------ 134 | * Add some basic install/uninstall tests. 135 | * Add uninstall job method and script. 136 | * Remove out of date README content, now forwards to ROS Wiki and generated documentation. 137 | * Add a documentation section about permissions 138 | * Contributors: Gaël Ecorchard, Mike Purvis 139 | 140 | 0.1.1 (2015-01-20) 141 | ------------------ 142 | * Python Rewrite 143 | * The startup event is too early for ROS to start, use local-filesystems instead. 144 | * Remove bash versions of the install and uninstall utilities. 145 | * Add support for supplying the --wait flag to roslaunch. 146 | * Add Sphinx documentation. 147 | To get the argparse docs required moving most of the install 148 | script to a module, which probably should have been done anyway. 149 | * Add a new-implementation install script, refactor Provider to be a class rather than function. 150 | * Add roslint. 151 | * Initial implementation of Python job generator. 152 | * Port templated files to use empy. 153 | This gets rid of the bespoke templating system that was so bad. Also 154 | notable here is adding a --root flag to install somewhere other than 155 | the actual root. This needs to be further fleshed out, for example 156 | by not reinvoking with sudo when installing to non-root location. 157 | * use LANG=C for ifconfig 158 | * add argument to specify log directory 159 | * Contributors: Eisoku Kuroiwa, Mike Purvis, ipa-mig 160 | 161 | 0.0.6 (2014-02-25) 162 | ------------------ 163 | * Add capability to also generate amalgamated descriptions, similar to launch files. 164 | * Update package.xml 165 | * Contributors: Mike Purvis 166 | 167 | 0.0.5 (2013-09-13) 168 | ------------------ 169 | * Better console outputs. 170 | * Remove debug output from install script. 171 | 172 | 0.0.4 (2013-09-11) 173 | ------------------ 174 | * Provide --augment option, to add files to a job without creating a new one. 175 | * Explicitly depend on daemontools. 176 | 177 | 0.0.3 (2013-09-11) 178 | ------------------ 179 | * Supply ROS_HOME explicitly in start script. 180 | * Remove spurious comment from uninstall script. 181 | 182 | 0.0.2 (2013-09-06) 183 | ------------------ 184 | * Eliminate rosrun from the make process. 185 | 186 | 0.0.1 (2013-09-06) 187 | ------------------ 188 | * Generalized robot upstart scripts based on turtlebot_bringup 189 | * Includes install and uninstall scripts 190 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2013-2021, Clearpath Robotics 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 are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # robot_upstart [![robot_upstart_ci](https://github.com/clearpathrobotics/robot_upstart/actions/workflows/ci.yml/badge.svg?branch=foxy-devel)](https://github.com/clearpathrobotics/robot_upstart/actions/workflows/ci.yml) 2 | 3 | Clearpath Robotics presents a suite of scripts to assist with launching background ROS processes on Ubuntu Linux PCs. Please see the [generated documentation](http://docs.ros.org/latest-available/api/robot_upstart/html/) and [ROS Wiki](http://wiki.ros.org/robot_upstart). 4 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import sys 5 | import xml.etree.ElementTree as etree 6 | 7 | sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) 8 | 9 | extensions = [ 10 | 'sphinx.ext.autodoc', 11 | 'sphinx.ext.doctest', 12 | 'sphinx.ext.viewcode', 13 | 'sphinxarg.ext' 14 | ] 15 | 16 | # This invocation turns on documenting class constructors (off by default) 17 | autoclass_content = 'both' 18 | 19 | source_suffix = '.rst' 20 | master_doc = 'index' 21 | 22 | project = u'robot_upstart' 23 | copyright = u'2015, Mike Purvis' 24 | 25 | # Get version number from package.xml. 26 | tree = etree.parse('../package.xml') 27 | version = tree.find("version").text 28 | release = version 29 | 30 | html_theme = 'nature' 31 | htmlhelp_basename = 'robot_upstartdoc' 32 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | Upstart for ROS Robots 2 | ====================== 3 | 4 | This package aims to assist with creating simple platform-specific jobs 5 | to start your robot's ROS launch files when its PC powers up. 6 | 7 | .. toctree:: 8 | :hidden: 9 | 10 | install 11 | uninstall 12 | jobs 13 | providers 14 | 15 | 16 | Usage 17 | ----- 18 | 19 | The basic or one-off usage is with the ``install`` script, which can be 20 | as simple as: 21 | 22 | .. code-block:: bash 23 | 24 | $ rosrun robot_upstart install myrobot_bringup/launch/base.launch 25 | 26 | This will create a job called ``myrobot`` on your machine, which launches 27 | base.launch. It will start automatically when you next start your machine, 28 | or you can bring it up and down manually (command may differ per providers (``upstart`` by default)): 29 | 30 | .. code-block:: bash 31 | 32 | When upstart is in use 33 | $ sudo service myrobot start 34 | $ sudo service myrobot stop 35 | 36 | When systemd in use 37 | $ sudo systemctl start myrobot 38 | $ sudo systemctl stop myrobot 39 | 40 | If the job is crashing on startup, or you otherwise want to see what is 41 | being output to the terminal on startup, check the upstart log: 42 | 43 | .. code-block:: bash 44 | 45 | upstart 46 | $ sudo tail /var/log/upstart/myrobot.log -n 30 47 | 48 | 49 | systemd 50 | $ sudo journalctl -u myrobot 51 | 52 | For more details, please see :doc:`install` and :doc:`uninstall`. 53 | 54 | 55 | Python API 56 | ---------- 57 | 58 | More advanced users or platform maintainers will prefer to script the 59 | job creation as part of a larger installation script which may do other 60 | tasks, such as pick and choose which launchers to install based on a 61 | recipe, interactive wizard, or hardware introspection scheme. 62 | 63 | These users will want to work with the Python API, which is detailed 64 | in :doc:`jobs`. 65 | 66 | 67 | Extending 68 | --------- 69 | 70 | If you're interesting in adding support for other init schemes to 71 | robot_upstart, please see :doc:`providers`. 72 | 73 | 74 | Index 75 | ----- 76 | 77 | * :ref:`genindex` 78 | * :ref:`search` 79 | -------------------------------------------------------------------------------- /doc/install.rst: -------------------------------------------------------------------------------- 1 | The ``install`` script 2 | ====================== 3 | 4 | .. argparse:: 5 | :module: robot_upstart.install_script 6 | :func: get_argument_parser 7 | :prog: install 8 | 9 | 10 | Permissions 11 | ----------- 12 | 13 | It's important to understand how permissions work robot_upstart: 14 | 15 | 1. The upstart job invokes its `jobname-start` bash script as root. 16 | 17 | 2. The script sets up environment variables, and then uses setuidgid_ to execute roslaunch as an unprivileged user. This is by default the user who ran the install script, but it can also be specified explicitly via a flag. 18 | 19 | 3. The `roslaunch` which executes *does not have its user's group memberships*. This means that it will not have access to serial ports with the `dialout` group, or locations in `/var/log` owned by root, etc. Any filesystem resources needed by your ROS nodes should be chowned to the same unprivileged user which will run ROS, or should set to world readable/writeable, for example using udev. 20 | 21 | .. _setuidgid: http://manpages.ubuntu.com/manpages/trusty/man8/setuidgid.8.html 22 | 23 | Implementation 24 | -------------- 25 | 26 | If you're in the process of transitioning from using the ``install`` script 27 | to the Python API, it may be helpful to inspect exactly how the script uses 28 | the API. You can find its implementation in the 29 | :func:`robot_upstart.install_script.main` function. 30 | 31 | .. automodule:: robot_upstart.install_script 32 | :members: 33 | -------------------------------------------------------------------------------- /doc/jobs.rst: -------------------------------------------------------------------------------- 1 | Jobs 2 | ==== 3 | 4 | .. automodule:: robot_upstart.job 5 | :members: 6 | 7 | Example 8 | ------- 9 | 10 | A minimal example of creating an upstart job using this API: 11 | 12 | .. code-block:: py 13 | 14 | import robot_upstart 15 | 16 | j = robot_upstart.Job() 17 | j.add(package="myrobot_bringup", filename="launch/base.launch") 18 | j.install() 19 | 20 | The :meth:`Job.add` method may be called multiple times, of course, and 21 | you may alternatively specify a glob, if you're like to create a job which 22 | launches all of the launchfiles from a package, eg: 23 | 24 | .. code-block:: py 25 | 26 | import robot_upstart 27 | 28 | j = robot_upstart.Job() 29 | j.add(package="myrobot_bringup", glob="launch/*.launch") 30 | j.install() 31 | 32 | Finally, if the ``package`` parameter is not specified, then the glob or 33 | filename is relative to the current directory, rather than a package. 34 | -------------------------------------------------------------------------------- /doc/providers.rst: -------------------------------------------------------------------------------- 1 | Providers 2 | ========= 3 | 4 | .. automodule:: robot_upstart.providers 5 | :members: 6 | -------------------------------------------------------------------------------- /doc/sphinxarg/README.md: -------------------------------------------------------------------------------- 1 | sphinxarg 2 | ========= 3 | 4 | From: https://github.com/ribozz/sphinx-argparse 5 | -------------------------------------------------------------------------------- /doc/sphinxarg/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clearpathrobotics/robot_upstart/211e84ebb96974dbe829db931bff537bd518814d/doc/sphinxarg/__init__.py -------------------------------------------------------------------------------- /doc/sphinxarg/ext.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | import os 3 | 4 | from docutils import nodes 5 | from docutils.parsers.rst.directives import flag, unchanged 6 | from sphinx.util.compat import Directive 7 | from sphinx.util.nodes import nested_parse_with_titles 8 | 9 | from sphinxarg.parser import parse_parser, parser_navigate 10 | 11 | 12 | def map_nested_definitions(nested_content): 13 | if nested_content is None: 14 | raise Exception('Nested content should be iterable, not null') 15 | # build definition dictionary 16 | definitions = {} 17 | for item in nested_content: 18 | if not isinstance(item, nodes.definition_list): 19 | continue 20 | for subitem in item: 21 | if not isinstance(subitem, nodes.definition_list_item): 22 | continue 23 | if not len(subitem.children) > 0: 24 | continue 25 | classifier = '@after' 26 | idx = subitem.first_child_matching_class(nodes.classifier) 27 | if idx is not None: 28 | ci = subitem[idx] 29 | if len(ci.children) > 0: 30 | classifier = ci.children[0].astext() 31 | if classifier is not None and classifier not in ( 32 | '@replace', '@before', '@after'): 33 | raise Exception('Unknown classifier: %s' % classifier) 34 | idx = subitem.first_child_matching_class(nodes.term) 35 | if idx is not None: 36 | ch = subitem[idx] 37 | if len(ch.children) > 0: 38 | term = ch.children[0].astext() 39 | idx = subitem.first_child_matching_class(nodes.definition) 40 | if idx is not None: 41 | def_node = subitem[idx] 42 | def_node.attributes['classifier'] = classifier 43 | definitions[term] = def_node 44 | return definitions 45 | 46 | 47 | def print_arg_list(data, nested_content): 48 | definitions = map_nested_definitions(nested_content) 49 | items = [] 50 | if 'args' in data: 51 | for arg in data['args']: 52 | my_def = [nodes.paragraph(text=arg['help'])] if arg['help'] else [] 53 | name = arg['name'] 54 | my_def = apply_definition(definitions, my_def, name) 55 | if len(my_def) == 0: 56 | my_def.append(nodes.paragraph(text='Undocumented')) 57 | if 'choices' in arg: 58 | my_def.append(nodes.paragraph( 59 | text=('Possible choices: %s' % ', '.join([str(c) for c in arg['choices']])))) 60 | items.append( 61 | nodes.option_list_item( 62 | '', nodes.option_group('', nodes.option_string(text=name)), 63 | nodes.description('', *my_def))) 64 | return nodes.option_list('', *items) if items else None 65 | 66 | 67 | def print_opt_list(data, nested_content): 68 | definitions = map_nested_definitions(nested_content) 69 | items = [] 70 | if 'options' in data: 71 | for opt in data['options']: 72 | names = [] 73 | my_def = [nodes.paragraph(text=opt['help'])] if opt['help'] else [] 74 | for name in opt['name']: 75 | option_declaration = [nodes.option_string(text=name)] 76 | if opt['default'] is not None \ 77 | and opt['default'] != '==SUPPRESS==': 78 | option_declaration += nodes.option_argument( 79 | '', text='=' + str(opt['default'])) 80 | names.append(nodes.option('', *option_declaration)) 81 | my_def = apply_definition(definitions, my_def, name) 82 | if len(my_def) == 0: 83 | my_def.append(nodes.paragraph(text='Undocumented')) 84 | if 'choices' in opt: 85 | my_def.append(nodes.paragraph( 86 | text=('Possible choices: %s' % ', '.join([str(c) for c in opt['choices']])))) 87 | items.append( 88 | nodes.option_list_item( 89 | '', nodes.option_group('', *names), 90 | nodes.description('', *my_def))) 91 | return nodes.option_list('', *items) if items else None 92 | 93 | 94 | def print_command_args_and_opts(arg_list, opt_list, sub_list=None): 95 | items = [] 96 | if arg_list: 97 | items.append(nodes.definition_list_item( 98 | '', nodes.term(text='Positional arguments:'), 99 | nodes.definition('', arg_list))) 100 | if opt_list: 101 | items.append(nodes.definition_list_item( 102 | '', nodes.term(text='Options:'), 103 | nodes.definition('', opt_list))) 104 | if sub_list and len(sub_list): 105 | items.append(nodes.definition_list_item( 106 | '', nodes.term(text='Sub-commands:'), 107 | nodes.definition('', sub_list))) 108 | return nodes.definition_list('', *items) 109 | 110 | 111 | def apply_definition(definitions, my_def, name): 112 | if name in definitions: 113 | definition = definitions[name] 114 | classifier = definition['classifier'] 115 | if classifier == '@replace': 116 | return definition.children 117 | if classifier == '@after': 118 | return my_def + definition.children 119 | if classifier == '@before': 120 | return definition.children + my_def 121 | raise Exception('Unknown classifier: %s' % classifier) 122 | return my_def 123 | 124 | 125 | def print_subcommand_list(data, nested_content): 126 | definitions = map_nested_definitions(nested_content) 127 | items = [] 128 | if 'children' in data: 129 | for child in data['children']: 130 | my_def = [nodes.paragraph( 131 | text=child['help'])] if child['help'] else [] 132 | name = child['name'] 133 | my_def = apply_definition(definitions, my_def, name) 134 | if len(my_def) == 0: 135 | my_def.append(nodes.paragraph(text='Undocumented')) 136 | my_def.append(nodes.literal_block(text=child['usage'])) 137 | my_def.append(print_command_args_and_opts( 138 | print_arg_list(child, nested_content), 139 | print_opt_list(child, nested_content) 140 | )) 141 | items.append( 142 | nodes.definition_list_item( 143 | '', 144 | nodes.term('', '', nodes.strong(text=name)), 145 | nodes.definition('', *my_def) 146 | ) 147 | ) 148 | return nodes.definition_list('', *items) 149 | 150 | 151 | class ArgParseDirective(Directive): 152 | has_content = True 153 | option_spec = dict(module=unchanged, func=unchanged, ref=unchanged, 154 | prog=unchanged, path=unchanged, nodefault=flag, 155 | manpage=unchanged, nosubcommands=unchanged) 156 | 157 | def _construct_manpage_specific_structure(self, parser_info): 158 | """ 159 | Construct a typical man page consisting of the following elements: 160 | NAME (automatically generated, out of our control) 161 | SYNOPSIS 162 | DESCRIPTION 163 | OPTIONS 164 | FILES 165 | SEE ALSO 166 | BUGS 167 | """ 168 | # SYNOPSIS section 169 | synopsis_section = nodes.section( 170 | '', 171 | nodes.title(text='Synopsis'), 172 | nodes.literal_block(text=parser_info["bare_usage"]), 173 | ids=['synopsis-section']) 174 | # DESCRIPTION section 175 | description_section = nodes.section( 176 | '', 177 | nodes.title(text='Description'), 178 | nodes.paragraph(text=parser_info.get( 179 | 'description', parser_info.get( 180 | 'help', "undocumented").capitalize())), 181 | ids=['description-section']) 182 | nested_parse_with_titles( 183 | self.state, self.content, description_section) 184 | if parser_info.get('epilog'): 185 | # TODO: do whatever sphinx does to understand ReST inside 186 | # docstrings magically imported from other places. The nested 187 | # parse method invoked above seem to be able to do this but 188 | # I haven't found a way to do it for arbitrary text 189 | description_section += nodes.paragraph( 190 | text=parser_info['epilog']) 191 | # OPTIONS section 192 | options_section = nodes.section( 193 | '', 194 | nodes.title(text='Options'), 195 | ids=['options-section']) 196 | if 'args' in parser_info: 197 | options_section += nodes.paragraph() 198 | options_section += nodes.subtitle(text='Positional arguments:') 199 | options_section += self._format_positional_arguments(parser_info) 200 | if 'options' in parser_info: 201 | options_section += nodes.paragraph() 202 | options_section += nodes.subtitle(text='Optional arguments:') 203 | options_section += self._format_optional_arguments(parser_info) 204 | items = [ 205 | # NOTE: we cannot generate NAME ourselves. It is generated by 206 | # docutils.writers.manpage 207 | synopsis_section, 208 | description_section, 209 | ] 210 | if len(options_section.children) > 1: 211 | items.append(options_section) 212 | if 'nosubcommands' not in self.options: 213 | # SUBCOMMANDS section (non-standard) 214 | subcommands_section = nodes.section( 215 | '', 216 | nodes.title(text='Sub-Commands'), 217 | ids=['subcommands-section']) 218 | if 'children' in parser_info: 219 | subcommands_section += self._format_subcommands(parser_info) 220 | if len(subcommands_section) > 1: 221 | items.append(subcommands_section) 222 | if os.getenv("INCLUDE_DEBUG_SECTION"): 223 | import json 224 | # DEBUG section (non-standard) 225 | debug_section = nodes.section( 226 | '', 227 | nodes.title(text="Argparse + Sphinx Debugging"), 228 | nodes.literal_block(text=json.dumps(parser_info, indent=' ')), 229 | ids=['debug-section']) 230 | items.append(debug_section) 231 | return items 232 | 233 | def _format_positional_arguments(self, parser_info): 234 | assert 'args' in parser_info 235 | items = [] 236 | for arg in parser_info['args']: 237 | arg_items = [] 238 | if arg['help']: 239 | arg_items.append(nodes.paragraph(text=arg['help'])) 240 | else: 241 | arg_items.append(nodes.paragraph(text='Undocumented')) 242 | if 'choices' in arg: 243 | arg_items.append( 244 | nodes.paragraph( 245 | text='Possible choices: ' + ', '.join(arg['choices']))) 246 | items.append( 247 | nodes.option_list_item( 248 | '', nodes.option_group( 249 | '', nodes.description(text=arg['metavar'])), 250 | nodes.description('', *arg_items))) 251 | return nodes.option_list('', *items) 252 | 253 | def _format_optional_arguments(self, parser_info): 254 | assert 'options' in parser_info 255 | items = [] 256 | for opt in parser_info['options']: 257 | names = [] 258 | opt_items = [] 259 | for name in opt['name']: 260 | option_declaration = [nodes.option_string(text=name)] 261 | if opt['default'] is not None \ 262 | and opt['default'] != '==SUPPRESS==': 263 | option_declaration += nodes.option_argument( 264 | '', text='=' + str(opt['default'])) 265 | names.append(nodes.option('', *option_declaration)) 266 | if opt['help']: 267 | opt_items.append(nodes.paragraph(text=opt['help'])) 268 | else: 269 | opt_items.append(nodes.paragraph(text='Undocumented')) 270 | if 'choices' in opt: 271 | opt_items.append( 272 | nodes.paragraph( 273 | text='Possible choices: ' + ', '.join(opt['choices']))) 274 | items.append( 275 | nodes.option_list_item( 276 | '', nodes.option_group('', *names), 277 | nodes.description('', *opt_items))) 278 | return nodes.option_list('', *items) 279 | 280 | def _format_subcommands(self, parser_info): 281 | assert 'children' in parser_info 282 | items = [] 283 | for subcmd in parser_info['children']: 284 | subcmd_items = [] 285 | if subcmd['help']: 286 | subcmd_items.append(nodes.paragraph(text=subcmd['help'])) 287 | else: 288 | subcmd_items.append(nodes.paragraph(text='Undocumented')) 289 | items.append( 290 | nodes.definition_list_item( 291 | '', 292 | nodes.term('', '', nodes.strong( 293 | text=subcmd['bare_usage'])), 294 | nodes.definition('', *subcmd_items))) 295 | return nodes.definition_list('', *items) 296 | 297 | def run(self): 298 | if 'module' in self.options and 'func' in self.options: 299 | module_name = self.options['module'] 300 | attr_name = self.options['func'] 301 | elif 'ref' in self.options: 302 | _parts = self.options['ref'].split('.') 303 | module_name = '.'.join(_parts[0:-1]) 304 | attr_name = _parts[-1] 305 | else: 306 | raise self.error( 307 | ':module: and :func: should be specified, or :ref:') 308 | mod = __import__(module_name, globals(), locals(), [attr_name]) 309 | if not hasattr(mod, attr_name): 310 | raise self.error(( 311 | 'Module "%s" has no attribute "%s"\n' 312 | 'Incorrect argparse :module: or :func: values?' 313 | ) % (module_name, attr_name)) 314 | func = getattr(mod, attr_name) 315 | if isinstance(func, ArgumentParser): 316 | parser = func 317 | else: 318 | parser = func() 319 | if 'path' not in self.options: 320 | self.options['path'] = '' 321 | path = str(self.options['path']) 322 | if 'prog' in self.options: 323 | parser.prog = self.options['prog'] 324 | result = parse_parser( 325 | parser, skip_default_values='nodefault' in self.options) 326 | result = parser_navigate(result, path) 327 | if 'manpage' in self.options: 328 | return self._construct_manpage_specific_structure(result) 329 | nested_content = nodes.paragraph() 330 | self.state.nested_parse( 331 | self.content, self.content_offset, nested_content) 332 | nested_content = nested_content.children 333 | items = [] 334 | # add common content between 335 | for item in nested_content: 336 | if not isinstance(item, nodes.definition_list): 337 | items.append(item) 338 | if 'description' in result: 339 | items.append(nodes.paragraph(text=result['description'])) 340 | items.append(nodes.literal_block(text=result['usage'])) 341 | items.append(print_command_args_and_opts( 342 | print_arg_list(result, nested_content), 343 | print_opt_list(result, nested_content), 344 | print_subcommand_list(result, nested_content) 345 | )) 346 | if 'epilog' in result: 347 | items.append(nodes.paragraph(text=result['epilog'])) 348 | return items 349 | 350 | 351 | def setup(app): 352 | app.add_directive('argparse', ArgParseDirective) 353 | -------------------------------------------------------------------------------- /doc/sphinxarg/parser.py: -------------------------------------------------------------------------------- 1 | from argparse import _HelpAction, _SubParsersAction 2 | import re 3 | 4 | 5 | class NavigationException(Exception): 6 | pass 7 | 8 | 9 | def parser_navigate(parser_result, path, current_path=None): 10 | if isinstance(path, str): 11 | if path == '': 12 | return parser_result 13 | path = re.split(r'\s+', path) 14 | current_path = current_path or [] 15 | if len(path) == 0: 16 | return parser_result 17 | if 'children' not in parser_result: 18 | raise NavigationException( 19 | 'Current parser have no children elements. (path: %s)' % 20 | ' '.join(current_path)) 21 | next_hop = path.pop(0) 22 | for child in parser_result['children']: 23 | if child['name'] == next_hop: 24 | current_path.append(next_hop) 25 | return parser_navigate(child, path, current_path) 26 | raise NavigationException( 27 | 'Current parser have no children element with name: %s (path: %s)' % ( 28 | next_hop, ' '.join(current_path))) 29 | 30 | 31 | def _try_add_parser_attribute(data, parser, attribname): 32 | attribval = getattr(parser, attribname, None) 33 | if attribval is None: 34 | return 35 | if not isinstance(attribval, str): 36 | return 37 | if len(attribval) > 0: 38 | data[attribname] = attribval 39 | 40 | 41 | def _format_usage_without_prefix(parser): 42 | """ 43 | Use private argparse APIs to get the usage string without 44 | the 'usage: ' prefix. 45 | """ 46 | fmt = parser._get_formatter() 47 | fmt.add_usage(parser.usage, parser._actions, 48 | parser._mutually_exclusive_groups, prefix='') 49 | return fmt.format_help().strip() 50 | 51 | 52 | def parse_parser(parser, data=None, **kwargs): 53 | if data is None: 54 | data = { 55 | 'name': '', 56 | 'usage': parser.format_usage().strip(), 57 | 'bare_usage': _format_usage_without_prefix(parser), 58 | 'prog': parser.prog, 59 | } 60 | _try_add_parser_attribute(data, parser, 'description') 61 | _try_add_parser_attribute(data, parser, 'epilog') 62 | for action in parser._get_positional_actions(): 63 | if isinstance(action, _HelpAction): 64 | continue 65 | if isinstance(action, _SubParsersAction): 66 | helps = {} 67 | for item in action._choices_actions: 68 | helps[item.dest] = item.help 69 | for name, subaction in action._name_parser_map.items(): 70 | subaction.prog = '%s %s' % (parser.prog, name) 71 | subdata = { 72 | 'name': name, 73 | 'help': helps[name] if name in helps else '', 74 | 'usage': subaction.format_usage().strip(), 75 | 'bare_usage': _format_usage_without_prefix(subaction), 76 | } 77 | parse_parser(subaction, subdata, **kwargs) 78 | if 'children' not in data: 79 | data['children'] = [] 80 | data['children'].append(subdata) 81 | continue 82 | if 'args' not in data: 83 | data['args'] = [] 84 | arg = { 85 | 'name': action.dest, 86 | 'help': action.help or '', 87 | 'metavar': action.metavar 88 | } 89 | if action.choices: 90 | arg['choices'] = action.choices 91 | data['args'].append(arg) 92 | show_defaults = ( 93 | ('skip_default_values' not in kwargs) or 94 | (kwargs['skip_default_values'] is False)) 95 | for action in parser._get_optional_actions(): 96 | if isinstance(action, _HelpAction): 97 | continue 98 | if 'options' not in data: 99 | data['options'] = [] 100 | option = { 101 | 'name': action.option_strings, 102 | 'default': action.default if show_defaults else '==SUPPRESS==', 103 | 'help': action.help or '' 104 | } 105 | if action.choices: 106 | option['choices'] = action.choices 107 | if "==SUPPRESS==" not in option['help']: 108 | data['options'].append(option) 109 | return data 110 | -------------------------------------------------------------------------------- /doc/uninstall.rst: -------------------------------------------------------------------------------- 1 | The ``uninstall`` script 2 | ======================== 3 | 4 | .. argparse:: 5 | :module: robot_upstart.uninstall_script 6 | :func: get_argument_parser 7 | :prog: uninstall 8 | 9 | Caveats 10 | ------- 11 | 12 | The uninstall script (and underlying method) make few guarantees--- all that ``uinstall`` will do 13 | is attempt to remove the files which were recorded as created by the last-run ``install`` action. 14 | It's not able to remove files added manually to a job after installation, nor can it detect and 15 | warn about modifications made to files. 16 | 17 | If the installed files are moved or the ``.installed_files`` manifest file is not intact, 18 | uninstallation will fail. 19 | 20 | 21 | Implementation 22 | -------------- 23 | 24 | You can find this script's implementation in the 25 | :func:`robot_upstart.uninstall_script.main` function. 26 | 27 | .. automodule:: robot_upstart.uninstall_script 28 | :members: 29 | -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | robot_upstart 5 | 1.0.4 6 | 7 | The robot_upstart package provides scripts which may be used to install 8 | and uninstall Ubuntu Linux upstart jobs which launch groups of roslaunch files. 9 | 10 | 11 | Chris Iverach-Brereton 12 | Tony Baltovski 13 | Mike Purvis 14 | 15 | BSD 16 | 17 | ament_index_python 18 | 19 | ament_copyright 20 | ament_flake8 21 | ament_pep257 22 | python3-pytest 23 | 24 | 25 | ament_python 26 | 27 | 28 | -------------------------------------------------------------------------------- /resource/robot_upstart: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clearpathrobotics/robot_upstart/211e84ebb96974dbe829db931bff537bd518814d/resource/robot_upstart -------------------------------------------------------------------------------- /robot_upstart/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Software License Agreement (BSD) 3 | # 4 | # @author Mike Purvis 5 | # @copyright (c) 2015, Clearpath Robotics, Inc., All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that 8 | # the following conditions are met: 9 | # * Redistributions of source code must retain the above copyright notice, this list of conditions and the 10 | # following disclaimer. 11 | # * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the 12 | # following disclaimer in the documentation and/or other materials provided with the distribution. 13 | # * Neither the name of Clearpath Robotics nor the names of its contributors may be used to endorse or 14 | # promote products derived from this software without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED 17 | # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 18 | # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 19 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 23 | # POSSIBILITY OF SUCH DAMAGE. 24 | 25 | from robot_upstart.job import Job 26 | import robot_upstart.providers 27 | -------------------------------------------------------------------------------- /robot_upstart/install_script.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Software License Agreement (BSD) 3 | # 4 | # @author Mike Purvis 5 | # @copyright (c) 2015, Clearpath Robotics, Inc., All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that 8 | # the following conditions are met: 9 | # * Redistributions of source code must retain the above copyright notice, this list of conditions and the 10 | # following disclaimer. 11 | # * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the 12 | # following disclaimer in the documentation and/or other materials provided with the distribution. 13 | # * Neither the name of Clearpath Robotics nor the names of its contributors may be used to endorse or 14 | # promote products derived from this software without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED 17 | # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 18 | # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 19 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 23 | # POSSIBILITY OF SUCH DAMAGE. 24 | 25 | import argparse 26 | import os 27 | 28 | import robot_upstart 29 | # from catkin.find_in_workspaces import find_in_workspaces 30 | from ament_index_python.packages import get_package_share_directory 31 | 32 | 33 | from . import providers 34 | 35 | DESC_PKGPATH = ("Make sure the path starts with the package name" 36 | " (e.g. don't pass absolute path nor a path starting from" 37 | " workspace top folder etc.)") 38 | 39 | 40 | def get_argument_parser(): 41 | p = argparse.ArgumentParser( 42 | description="""Use this tool to quickly and easily create system startup jobs which run one or more 43 | ROS launch files as a daemonized background process on your computer. More advanced users will prefer 44 | to access the Python API from their own setup scripts, but this exists as a simple helper, an example, 45 | and a compatibility shim for previous versions of robot_upstart which were bash-based.""") 46 | 47 | p.add_argument("pkgpath", type=str, nargs='+', metavar="pkg/path", 48 | help="Package and path to install job launch files from. " + 49 | DESC_PKGPATH) 50 | p.add_argument("--job", type=str, 51 | help="Specify job name. If unspecified, will be constructed from package name (first " + 52 | "element before underscore is taken, e.g. 'myrobot' if the package name is 'myrobot_bringup').") 53 | p.add_argument("--rmw", type=str, metavar="rmw_fastrtps_cpp", 54 | help="Specify RMW DDS being used. rmw_fastrtps_cpp or rmw_cyclonedds_cpp") 55 | p.add_argument("--rmw_config", type=str, 56 | help="RMW configuration URI") 57 | p.add_argument("--interface", type=str, metavar="ethN", 58 | help="Specify network interface name to associate job with.") 59 | p.add_argument("--ros_domain_id", type=str, 60 | help="ROS_DOMAIN_ID value.") 61 | p.add_argument("--user", type=str, metavar="NAME", 62 | help="Specify user to launch job as.") 63 | p.add_argument("--setup", type=str, metavar="path/to/setup.bash", 64 | help="Specify workspace setup file for the job launch context.") 65 | p.add_argument("--rosdistro", type=str, metavar="DISTRO", 66 | help="Specify ROS distro this is for.") 67 | p.add_argument("--logdir", type=str, metavar="path/to/logs", 68 | help="Specify an a value for ROS_LOG_DIR in the job launch context.") 69 | p.add_argument("--augment", action='store_true', 70 | help="Bypass creating the job, and only copy user files. Assumes the job was previously created.") 71 | p.add_argument("--provider", type=str, metavar="[upstart|systemd]", 72 | help="Specify provider if the autodetect fails to identify the correct provider") 73 | p.add_argument("--symlink", action='store_true', 74 | help="Create symbolic link to job launch files instead of copying them.") 75 | p.add_argument("--wait", action='store_true', 76 | help="Pass a wait flag to roslaunch.") 77 | p.add_argument("--systemd-after", type=str, metavar="After=", 78 | help="Set the string of the After= section" 79 | "of the generated Systemd service file") 80 | 81 | return p 82 | 83 | 84 | def main(): 85 | """ Implementation of the ``install`` script.""" 86 | 87 | args = get_argument_parser().parse_args() 88 | 89 | pkg, pkgpath = args.pkgpath[0].split('/', 1) 90 | job_name = args.job or pkg.split('_', 1)[0] 91 | 92 | # Any unspecified arguments are on the args object as None. These are filled 93 | # in by the Job constructor when passed as Nones. 94 | j = robot_upstart.Job( 95 | name=job_name, rmw=args.rmw, rmw_config=args.rmw_config, 96 | interface=args.interface, ros_domain_id=args.ros_domain_id, 97 | user=args.user, workspace_setup=args.setup, rosdistro=args.rosdistro, log_path=args.logdir, 98 | systemd_after=args.systemd_after) 99 | 100 | for this_pkgpath in args.pkgpath: 101 | pkg, pkgpath = this_pkgpath.split('/', 1) 102 | if not pkg: 103 | print("Unable to locate package your job launch is in." 104 | " Installation aborted. " + DESC_PKGPATH + 105 | "\npkgpath passed: {}.".format(pkgpath)) 106 | return 1 107 | 108 | # found_path = find_in_workspaces(project=pkg, path=pkgpath, first_match_only=True) 109 | found_path = get_package_share_directory(pkg)+"/"+pkgpath 110 | if not found_path: 111 | print("Unable to locate path %s in package %s. Installation aborted." % (pkgpath, pkg)) 112 | return 1 113 | 114 | # if os.path.isfile(found_path[0]): 115 | print("found_path: %s" % found_path) 116 | if os.path.isfile(found_path): 117 | # Single file, install just that. 118 | j.add(package=pkg, filename=pkgpath) 119 | else: 120 | # Directory found, install everything within. 121 | j.add(package=pkg, glob=os.path.join(pkgpath, "*")) 122 | 123 | if args.augment: 124 | j.generate_system_files = False 125 | if args.wait: 126 | j.roslaunch_wait = True 127 | 128 | provider = providers.detect_provider() 129 | if args.provider == 'upstart': 130 | provider = providers.Upstart 131 | if args.provider == 'systemd': 132 | provider = providers.Systemd 133 | if args.symlink: 134 | j.symlink = True 135 | 136 | j.install(Provider=provider) 137 | 138 | return 0 139 | -------------------------------------------------------------------------------- /robot_upstart/job.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD) 2 | # 3 | # @author Mike Purvis 4 | # @copyright (c) 2015, Clearpath Robotics, Inc., All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that 7 | # the following conditions are met: 8 | # * Redistributions of source code must retain the above copyright notice, this list of conditions and the 9 | # following disclaimer. 10 | # * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the 11 | # following disclaimer in the documentation and/or other materials provided with the distribution. 12 | # * Neither the name of Clearpath Robotics nor the names of its contributors may be used to endorse or 13 | # promote products derived from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED 16 | # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 17 | # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 18 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 19 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 21 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 22 | # POSSIBILITY OF SUCH DAMAGE. 23 | 24 | """ 25 | This file defines the Job class, which is the primary code API to robot_upstart. 26 | """ 27 | 28 | import getpass 29 | import os 30 | import json 31 | import subprocess 32 | from glob import glob as glob_files 33 | 34 | # from catkin.find_in_workspaces import find_in_workspaces 35 | from ament_index_python.packages import get_package_share_directory 36 | 37 | from . import providers 38 | 39 | 40 | class Job(object): 41 | """ Represents a ROS configuration to launch on machine startup. """ 42 | 43 | def __init__(self, name="ros", rmw=None, rmw_config=None, interface=None, 44 | ros_domain_id=None, user=None, workspace_setup=None, 45 | rosdistro=None, log_path=None, 46 | systemd_after=None): 47 | """Construct a new Job definition. 48 | 49 | :param name: Name of job to create. Defaults to "ros", but you might 50 | prefer to use the name of your platform. 51 | :type name: str 52 | :param rmw: RMW DDS being used. rmw_fastrtps_cpp or rmw_cyclonedds_cpp. 53 | :type rmw: str 54 | :param rmw_config: Path to RMW xml profile. 55 | :type rmw_config: str 56 | :param interface: Network interface to bring ROS up with. If specified, 57 | the job will come up with that network interface, and ROS_IP will be set 58 | to that interface's IP address. If unspecified, the job will come up 59 | on system startup, and ROS_HOSTNAME will be set to the system's hostname. 60 | :type interface: str 61 | :param ros_domain_id: ROS_DOMAIN_ID value. Defaults to 0. 62 | :type ros_domain_id: str 63 | :param user: Unprivileged user to launch the job as. Defaults to the user 64 | creating the job. 65 | :type user: str 66 | :param workspace_setup: Location of the workspace setup file to source for 67 | the job's ROS context. Defaults to the current workspace. 68 | :type workspace_setup: str 69 | :param rosdistro: rosdistro to use for the /etc/ros/DISTRO path. Defaults 70 | to $ROS_DISTRO from the current environment. 71 | :type rosdistro: str 72 | :param log_path: The location to set ROS_LOG_DIR to. If changed from the 73 | default of using /tmp, it is the user's responsibility to manage log 74 | rotation. 75 | :type log_path: str 76 | """ 77 | 78 | self.name = name 79 | 80 | self.interface = interface 81 | 82 | # Fall back on current user as the user to run ROS as. 83 | self.user = user or getpass.getuser() 84 | 85 | # Fall back on current distro if not otherwise specified. 86 | self.rosdistro = rosdistro or os.environ['ROS_DISTRO'] 87 | 88 | # Prioritize specified workspace, falling back to current workspace if possible, or 89 | # system workspace as a last-resort 90 | if workspace_setup: 91 | self.workspace_setup = workspace_setup 92 | elif 'CMAKE_PREFIX_PATH' in os.environ.keys(): 93 | self.workspace_setup = os.environ['CMAKE_PREFIX_PATH'].split(':')[0] + '/../setup.bash' 94 | else: 95 | self.workspace_setup = f'/opt/ros/{self.rosdistro}/setup.bash' 96 | 97 | self.rmw = rmw or "rmw_fastrtps_cpp" 98 | 99 | self.rmw_config = rmw_config or "" 100 | 101 | self.ros_domain_id = ros_domain_id or "" 102 | 103 | self.log_path = log_path or "/tmp" 104 | 105 | # Override this to false if you want to bypass generating the 106 | # upstart conf file. 107 | self.generate_system_files = True 108 | 109 | # Override this to True if you want to create symbolic link for 110 | # job launch files instead of copying them. 111 | self.symlink = False 112 | 113 | # Override this to True is you want the --wait flag passed to roslaunch. 114 | # This will be desired if the nodes spawned by this job are intended to 115 | # connect to an existing master. 116 | self.roslaunch_wait = False 117 | 118 | # Set the string of the "After=" section 119 | # of the generated Systemd service file 120 | self.systemd_after = systemd_after or "network.target" 121 | 122 | # Set of files to be installed for the job. This is only launchers 123 | # and other user-specified configs--- nothing related to the system 124 | # startup job itself. List of strs. 125 | self.files = [] 126 | 127 | def add(self, package=None, filename=None, glob=None): 128 | """ Add launch or other configuration files to Job. 129 | 130 | Files may be specified using relative, absolute, or package-relative 131 | paths. Files may also be specified using shell globs. 132 | 133 | :param package: Optionally specify a package to search for the file 134 | or glob relative-to. 135 | :type package: str 136 | :param filename: Name of a file to add to the job. Relative to the 137 | package path, if specified. 138 | :type filename: str 139 | :param glob: Shell glob of files to add to the job. Relative to the 140 | package path, if specified. 141 | :type glob: str 142 | """ 143 | 144 | if package: 145 | # search_paths = reversed(find_in_workspaces(project=package)) 146 | search_paths = (get_package_share_directory(package), ) 147 | else: 148 | search_paths = ('.', ) 149 | 150 | if glob and filename: 151 | raise RuntimeError("You must specify only an exact filename or a glob, not both.") 152 | 153 | # See: https://docs.python.org/2/library/os.html#os.getlogin 154 | if filename: 155 | for path in search_paths: 156 | candidate = os.path.join(path, filename) 157 | if os.path.isfile(candidate): 158 | print("candidate : %s" % candidate) 159 | self.files.append(candidate) 160 | 161 | if glob: 162 | for path in search_paths: 163 | self.files.extend(glob_files(os.path.join(path, glob))) 164 | 165 | def install(self, root="/", sudo="/usr/bin/sudo", Provider=None): 166 | """ Install the job definition to the system. 167 | 168 | :param root: Override the root to install to, useful for testing. 169 | :type root: str 170 | :param sudo: Override which sudo is used, useful for testing or for making 171 | it use gksudo instead. 172 | :type sudo: str 173 | :param provider: Override to use your own generator function for the system 174 | file preparation. 175 | :type provider: Provider 176 | """ 177 | # This is a recipe of files and their contents which is pickled up and 178 | # passed to a sudo process so that it can create the actual files, 179 | # without needing a ROS workspace or any other environmental setup. 180 | if Provider is None: 181 | Provider = providers.detect_provider() 182 | p = Provider(root, self) 183 | installation_files = p.generate_install() 184 | 185 | print("Preparing to install files to the following paths:") 186 | for filename in sorted(installation_files.keys()): 187 | print(" %s" % filename) 188 | 189 | self._call_mutate(sudo, installation_files) 190 | p.post_install() 191 | 192 | def uninstall(self, root="/", sudo="/usr/bin/sudo", Provider=None): 193 | """ Uninstall the job definition from the system. 194 | 195 | :param root: Override the root to uninstall from, useful for testing. 196 | :type root: str 197 | :param sudo: Override which sudo is used, useful for testing or for making 198 | it use gksudo instead. 199 | :type sudo: str 200 | :param provider: Override to use your own generator function for the system 201 | file preparation. 202 | :type provider: Provider 203 | """ 204 | if Provider is None: 205 | Provider = providers.detect_provider() 206 | p = Provider(root, self) 207 | installation_files = p.generate_uninstall() 208 | 209 | if len(installation_files) == 0: 210 | print("Job not found, nothing to remove.") 211 | else: 212 | print("Preparing to remove the following paths:") 213 | for filename in sorted(installation_files.keys()): 214 | print(" %s" % filename) 215 | 216 | self._call_mutate(sudo, installation_files) 217 | 218 | def _call_mutate(self, sudo, installation_files): 219 | try: 220 | # Installed script location 221 | # mutate_files_exec = find_in_workspaces( 222 | # project="robot_upstart", path="mutate_files", first_match_only=True)[0] 223 | mutate_files_exec = get_package_share_directory("robot_upstart") + "/scripts/mutate_files" 224 | except IndexError: 225 | # Devel script location 226 | # mutate_files_exec = find_in_workspaces( 227 | # project="robot_upstart", path="scripts/mutate_files", first_match_only=True)[0] 228 | mutate_files_exec = get_package_share_directory("robot_upstart") + "/scripts/mutate_files" 229 | 230 | 231 | # If sudo is specified, then the user will be prompted at this point. 232 | cmd = [mutate_files_exec] 233 | if sudo: 234 | cmd.insert(0, sudo) 235 | print("Now calling: %s" % ' '.join(cmd)) 236 | 237 | # changed to use json, as pickle gives 0-bytes error 238 | p = subprocess.Popen(cmd + [json.dumps(installation_files)]) 239 | p.communicate() 240 | 241 | if p.returncode == 0: 242 | print("Filesystem operation succeeded.") 243 | else: 244 | print("Error encountered; filesystem operation aborted.") 245 | 246 | return p.returncode 247 | -------------------------------------------------------------------------------- /robot_upstart/providers.py: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD) 2 | # 3 | # @author Mike Purvis 4 | # @copyright (c) 2015, Clearpath Robotics, Inc., All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that 7 | # the following conditions are met: 8 | # * Redistributions of source code must retain the above copyright notice, this list of conditions and the 9 | # following disclaimer. 10 | # * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the 11 | # following disclaimer in the documentation and/or other materials provided with the distribution. 12 | # * Neither the name of Clearpath Robotics nor the names of its contributors may be used to endorse or 13 | # promote products derived from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED 16 | # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 17 | # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 18 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 19 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 21 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 22 | # POSSIBILITY OF SUCH DAMAGE. 23 | 24 | """ 25 | These classes implement the translation from user-intended behaviours 26 | as specified in the state of the Job class to the system-specific configuration 27 | files. At present, there is only an upstart configuration, but similar providers 28 | could be defined for systemd, supervisor, launchd, or other systems. 29 | """ 30 | 31 | import em 32 | import os 33 | import io 34 | 35 | # from catkin.find_in_workspaces import find_in_workspaces 36 | from ament_index_python.packages import get_package_share_directory 37 | 38 | 39 | def detect_provider(): 40 | cmd = open('/proc/1/cmdline', 'rb').read().split(b'\x00')[0] 41 | print(os.path.realpath(cmd)) 42 | if b'systemd' in os.path.realpath(cmd): 43 | return Systemd 44 | return Upstart 45 | 46 | 47 | class Generic(object): 48 | """ Provides only a common constructor for the moment, but as further 49 | providers are implemented, may provide a place to store configuration 50 | common to them. """ 51 | 52 | def __init__(self, root, job): 53 | """ Construct a new Provider. 54 | 55 | :param root: The filesystem location to prefix all file-install 56 | commands with. 57 | :type root: str 58 | :param job: The job definition to transform to a set of system files. 59 | :type job: :py:class:robot_upstart.Job 60 | """ 61 | self.root = root 62 | self.job = job 63 | 64 | # Recipe structure which is serialized to yaml and passed to the mutate_files script. 65 | self.installation_files = {} 66 | 67 | # Bare list of files, stored in the .installed_files manifest file. 68 | self.installed_files_set = set() 69 | 70 | def _add_job_files(self): 71 | # Make up list of files to copy to system locations. 72 | for filename in self.job.files: 73 | dest_filename = os.path.join(self.job.job_path, os.path.basename(filename)) 74 | if self.job.symlink: 75 | self.installation_files[dest_filename] = {"symlink": filename} 76 | else: 77 | with open(filename) as f: 78 | self.installation_files[dest_filename] = {"content": f.read()} 79 | 80 | def _load_installed_files_set(self): 81 | self.installed_files_set_location = os.path.join(self.job.job_path, ".installed_files") 82 | if os.path.exists(self.installed_files_set_location): 83 | with open(self.installed_files_set_location) as f: 84 | self.installed_files_set.update(f.read().split("\n")) 85 | 86 | 87 | class Upstart(Generic): 88 | """ The Upstart implementation places the user-specified files in ``/etc/ros/DISTRO/NAME.d``, 89 | and creates an upstart job configuration in ``/etc/init/NAME.d``. Two additional 90 | helper scripts are created for starting and stopping the job, places in 91 | ``/usr/sbin``. 92 | """ 93 | 94 | def generate_install(self): 95 | # Default for Upstart is /etc/ros/DISTRO/JOBNAME.d 96 | self._set_job_path() 97 | 98 | # User-specified launch files. 99 | self._add_job_files() 100 | 101 | # This is optional to support the old --augment flag where a "job" only adds 102 | # launch files to an existing configuration. 103 | if (self.job.generate_system_files): 104 | # Share a single instance of the empy interpreter. 105 | self.interpreter = em.Interpreter(globals=self.job.__dict__.copy()) 106 | 107 | self.installation_files[os.path.join(self.root, "etc/init", self.job.name + ".conf")] = { 108 | "content": self._fill_template("templates/job.conf.em"), "mode": 0o644} 109 | self.installation_files[os.path.join(self.root, "usr/sbin", self.job.name + "-start")] = { 110 | "content": self._fill_template("templates/job-start.em"), "mode": 0o755} 111 | self.installation_files[os.path.join(self.root, "usr/sbin", self.job.name + "-stop")] = { 112 | "content": self._fill_template("templates/job-stop.em"), "mode": 0o755} 113 | self.interpreter.shutdown() 114 | 115 | # Add an annotation file listing what has been installed. This is a union of what's being 116 | # installed now with what has been installed previously, so that an uninstall should remove 117 | # all of it. A more sophisticated future implementation could track contents or hashes and 118 | # thereby warn users when a new installation is stomping a change they have made. 119 | self._load_installed_files_set() 120 | self.installed_files_set.update(list(self.installation_files.keys())) 121 | 122 | # Remove the job directory. This will fail if it is not empty, and notify the user. 123 | self.installed_files_set.add(self.job.job_path) 124 | 125 | # Remove the annotation file itself. 126 | self.installed_files_set.add(self.installed_files_set_location) 127 | 128 | self.installation_files[self.installed_files_set_location] = { 129 | "content": "\n".join(self.installed_files_set)} 130 | 131 | return self.installation_files 132 | 133 | def post_install(self): 134 | return 135 | 136 | def generate_uninstall(self): 137 | self._set_job_path() 138 | self._load_installed_files_set() 139 | 140 | for filename in self.installed_files_set: 141 | self.installation_files[filename] = {"remove": True} 142 | 143 | return self.installation_files 144 | 145 | def _set_job_path(self): 146 | self.job.job_path = os.path.join( 147 | self.root, "etc/ros", self.job.rosdistro, self.job.name + ".d") 148 | 149 | def _fill_template(self, template): 150 | self.interpreter.output = io.StringIO() 151 | self.interpreter.reset() 152 | # with open(find_in_workspaces(project="robot_upstart", path=template)[0]) as f: 153 | with open(get_package_share_directory("robot_upstart")+"/"+template) as f: 154 | self.interpreter.file(f) 155 | return self.interpreter.output.getvalue() 156 | 157 | 158 | class Systemd(Generic): 159 | """ The Systemd implementation places the user-specified files in ``/etc/ros/DISTRO/NAME.d``, 160 | and creates an systemd job configuration in ``/lib/systemd/system/NAME.d``. Two additional 161 | helper scripts are created for starting and stopping the job, places in 162 | ``/usr/sbin``. 163 | To detect which system you're using run: ps -p1 | grep systemd && echo systemd || echo upstart 164 | """ 165 | 166 | def generate_install(self): 167 | # Default is /etc/ros/DISTRO/JOBNAME.d 168 | self._set_job_path() 169 | 170 | # User-specified launch files. 171 | self._add_job_files() 172 | 173 | # This is optional to support the old --augment flag where a "job" only adds 174 | # launch files to an existing configuration. 175 | if self.job.generate_system_files: 176 | # Share a single instance of the EmPy interpreter. 177 | self.interpreter = em.Interpreter(globals=self.job.__dict__.copy()) 178 | 179 | self.installation_files[os.path.join(self.root, "lib/systemd/system", self.job.name + ".service")] = { 180 | "content": self._fill_template("templates/systemd_job.conf.em"), "mode": 0o644} 181 | self.installation_files[os.path.join(self.root, "etc/systemd/system/multi-user.target.wants", 182 | self.job.name + ".service")] = { 183 | "symlink": os.path.join(self.root, "lib/systemd/system/", self.job.name + ".service")} 184 | self.installation_files[os.path.join(self.root, "usr/sbin", self.job.name + "-start")] = { 185 | "content": self._fill_template("templates/job-start.em"), "mode": 0o755} 186 | self.installation_files[os.path.join(self.root, "usr/sbin", self.job.name + "-stop")] = { 187 | "content": self._fill_template("templates/job-stop.em"), "mode": 0o755} 188 | self.interpreter.shutdown() 189 | 190 | # Add an annotation file listing what has been installed. This is a union of what's being 191 | # installed now with what has been installed previously, so that an uninstall should remove 192 | # all of it. A more sophisticated future implementation could track contents or hashes and 193 | # thereby warn users when a new installation is stomping a change they have made. 194 | self._load_installed_files_set() 195 | self.installed_files_set.update(list(self.installation_files.keys())) 196 | 197 | # Remove the job directory. This will fail if it is not empty, and notify the user. 198 | self.installed_files_set.add(self.job.job_path) 199 | 200 | # Remove the annotation file itself. 201 | self.installed_files_set.add(self.installed_files_set_location) 202 | 203 | self.installation_files[self.installed_files_set_location] = { 204 | "content": "\n".join(self.installed_files_set)} 205 | 206 | return self.installation_files 207 | 208 | def post_install(self): 209 | print("** To complete installation please run the following command:") 210 | print(" sudo systemctl daemon-reload" + 211 | " && sudo systemctl start " + self.job.name) 212 | 213 | def generate_uninstall(self): 214 | self._set_job_path() 215 | self._load_installed_files_set() 216 | 217 | for filename in self.installed_files_set: 218 | self.installation_files[filename] = {"remove": True} 219 | 220 | return self.installation_files 221 | 222 | def _set_job_path(self): 223 | self.job.job_path = os.path.join( 224 | self.root, "etc/ros", self.job.rosdistro, self.job.name + ".d") 225 | 226 | def _fill_template(self, template): 227 | self.interpreter.output = io.StringIO() 228 | self.interpreter.reset() 229 | # with open(find_in_workspaces(project="robot_upstart", path=template)[0]) as f: 230 | with open(get_package_share_directory("robot_upstart")+"/"+template) as f: 231 | 232 | self.interpreter.file(f) 233 | return self.interpreter.output.getvalue() 234 | self.set_job_path() 235 | -------------------------------------------------------------------------------- /robot_upstart/uninstall_script.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Software License Agreement (BSD) 3 | # 4 | # @author Mike Purvis 5 | # @copyright (c) 2015, Clearpath Robotics, Inc., All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that 8 | # the following conditions are met: 9 | # * Redistributions of source code must retain the above copyright notice, this list of conditions and the 10 | # following disclaimer. 11 | # * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the 12 | # following disclaimer in the documentation and/or other materials provided with the distribution. 13 | # * Neither the name of Clearpath Robotics nor the names of its contributors may be used to endorse or 14 | # promote products derived from this software without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED 17 | # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 18 | # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 19 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 23 | # POSSIBILITY OF SUCH DAMAGE. 24 | 25 | import argparse 26 | import os 27 | 28 | import robot_upstart 29 | # from catkin.find_in_workspaces import find_in_workspaces 30 | 31 | 32 | def get_argument_parser(): 33 | p = argparse.ArgumentParser( 34 | description="""Use this script to remove upstart jobs created by the corresponding install script.""") 35 | 36 | p.add_argument("jobname", type=str, nargs=1, metavar=("JOBNAME", ), 37 | help="Name of job to uninstall.") 38 | p.add_argument("--rosdistro", type=str, metavar="DISTRO", 39 | help="Specify ROS distro this is for.") 40 | return p 41 | 42 | 43 | def main(): 44 | """ Implementation of the ``uninstall`` script.""" 45 | 46 | args = get_argument_parser().parse_args() 47 | j = robot_upstart.Job(name=args.jobname[0], rosdistro=args.rosdistro) 48 | j.uninstall() 49 | 50 | return 0 51 | -------------------------------------------------------------------------------- /rosdoc.yaml: -------------------------------------------------------------------------------- 1 | - builder: sphinx 2 | sphinx_root_dir: doc 3 | -------------------------------------------------------------------------------- /scripts/getifip: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Author: Mike Purvis 4 | # Copyright (c) 2013, Clearpath Robotics, Inc. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # * Neither the name of Clearpath Robotics, Inc. nor the 14 | # names of its contributors may be used to endorse or promote products 15 | # derived from this software without specific prior written permission. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | # DISCLAIMED. IN NO EVENT SHALL CLEARPATH ROBOTICS, INC. BE LIABLE FOR ANY 21 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | # 28 | # Please send comments, questions, or patches to code@clearpathrobotics.com 29 | 30 | echo `LANG=C ifconfig $1 | perl -lne '/inet (addr:)?(.[^ ]+)/ && print $2'` 31 | -------------------------------------------------------------------------------- /scripts/install: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Software License Agreement (BSD) 3 | # 4 | # @author Mike Purvis 5 | # @copyright (c) 2015, Clearpath Robotics, Inc., All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that 8 | # the following conditions are met: 9 | # * Redistributions of source code must retain the above copyright notice, this list of conditions and the 10 | # following disclaimer. 11 | # * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the 12 | # following disclaimer in the documentation and/or other materials provided with the distribution. 13 | # * Neither the name of Clearpath Robotics nor the names of its contributors may be used to endorse or 14 | # promote products derived from this software without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED 17 | # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 18 | # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 19 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 23 | # POSSIBILITY OF SUCH DAMAGE. 24 | 25 | """ 26 | The is the main CLI interface to the robot_upstart package. 27 | """ 28 | 29 | from robot_upstart.install_script import main 30 | 31 | if __name__ == "__main__": 32 | exit(main()) 33 | -------------------------------------------------------------------------------- /scripts/mklaunch: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Author: Mike Purvis 4 | # Copyright (c) 2013, Clearpath Robotics, Inc. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # * Neither the name of Clearpath Robotics, Inc. nor the 14 | # names of its contributors may be used to endorse or promote products 15 | # derived from this software without specific prior written permission. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | # DISCLAIMED. IN NO EVENT SHALL CLEARPATH ROBOTICS, INC. BE LIABLE FOR ANY 21 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | # 28 | # Please send comments, questions, or patches to code@clearpathrobotics.com 29 | 30 | path=$1 31 | files=$(ls $path/*launch.[pxy][yma]*) 32 | if [[ "$?" != "0" ]]; then 33 | echo "" 34 | exit 1 35 | fi 36 | 37 | echo "" 38 | echo " " 39 | for file in $files 40 | do 41 | echo " " 42 | done 43 | echo "" 44 | -------------------------------------------------------------------------------- /scripts/mkxacro: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Author: Mike Purvis 4 | # Copyright (c) 2013, Clearpath Robotics, Inc. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # * Neither the name of Clearpath Robotics, Inc. nor the 14 | # names of its contributors may be used to endorse or promote products 15 | # derived from this software without specific prior written permission. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | # DISCLAIMED. IN NO EVENT SHALL CLEARPATH ROBOTICS, INC. BE LIABLE FOR ANY 21 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | # 28 | # Please send comments, questions, or patches to code@clearpathrobotics.com 29 | 30 | path=$1 31 | files=$(ls $path/*.xacro) 32 | if [[ "$?" != "0" ]]; then 33 | echo "" 34 | exit 1 35 | fi 36 | 37 | echo "" 38 | echo " " 39 | for file in $files 40 | do 41 | echo " " 42 | done 43 | echo "" 44 | -------------------------------------------------------------------------------- /scripts/mutate_files: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Software License Agreement (BSD) 3 | # 4 | # @author Mike Purvis 5 | # @copyright (c) 2015, Clearpath Robotics, Inc., All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that 8 | # the following conditions are met: 9 | # * Redistributions of source code must retain the above copyright notice, this list of conditions and the 10 | # following disclaimer. 11 | # * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the 12 | # following disclaimer in the documentation and/or other materials provided with the distribution. 13 | # * Neither the name of Clearpath Robotics nor the names of its contributors may be used to endorse or 14 | # promote products derived from this software without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED 17 | # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 18 | # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 19 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 23 | # POSSIBILITY OF SUCH DAMAGE. 24 | 25 | """ 26 | This script is called by the Job.install and Job.uninstall methods. Its purpose is to be passed a 27 | small recipe of files to be created as the root user. So this script may be invoked under sudo 28 | while the majority of the installation process occurs as the unprivileged user. 29 | """ 30 | 31 | import os, sys, json 32 | 33 | if len(sys.argv) != 2: 34 | print("This script is not intended to be called manually.") 35 | exit(1) 36 | 37 | installation_files = json.loads(os.fsencode(sys.argv[1])) 38 | # Check first for writability, creating paths if necessary. 39 | paths = set([os.path.dirname(filename) for filename in installation_files.keys()]) 40 | 41 | for path in paths: 42 | if os.path.exists(path): 43 | if not os.access(path, os.W_OK | os.X_OK): 44 | print("Unable to write to path: %s" % path) 45 | exit(1) 46 | else: 47 | try: 48 | os.makedirs(path) 49 | except: 50 | print("Unable to create path: %s" % path) 51 | 52 | # Mutate files according to the provided spec. We sort the list in reverse length 53 | # order so that when we are trying to remove a directory it is after everything in 54 | # it has already been removed. 55 | paths = list(installation_files.keys()) 56 | paths.sort(key=len, reverse=True) 57 | for path in paths: 58 | recipe = installation_files[path] 59 | 60 | if 'symlink' in recipe: 61 | try: 62 | os.symlink(recipe['symlink'], path) 63 | except OSError as e: 64 | # In case the symlink already exists, remove it before creating 65 | if e.errno == 17: 66 | os.unlink(path) 67 | os.symlink(recipe['symlink'], path) 68 | else: 69 | print("Unable to create symlink: %s" % path) 70 | 71 | if 'content' in recipe: 72 | with open(path, 'w') as f: 73 | f.write(recipe['content']) 74 | 75 | if 'mode' in recipe: 76 | os.chmod(path, recipe['mode']) 77 | 78 | if 'remove' in recipe and recipe['remove']: 79 | if os.path.isdir(path): 80 | try: 81 | os.rmdir(path) 82 | except OSError: 83 | print("WARNING: Unable to remove directory %s. Additional user-added files may be present." % path) 84 | else: 85 | os.remove(path) 86 | 87 | exit(0) 88 | -------------------------------------------------------------------------------- /scripts/uninstall: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Software License Agreement (BSD) 3 | # 4 | # @author Mike Purvis 5 | # @copyright (c) 2015, Clearpath Robotics, Inc., All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that 8 | # the following conditions are met: 9 | # * Redistributions of source code must retain the above copyright notice, this list of conditions and the 10 | # following disclaimer. 11 | # * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the 12 | # following disclaimer in the documentation and/or other materials provided with the distribution. 13 | # * Neither the name of Clearpath Robotics nor the names of its contributors may be used to endorse or 14 | # promote products derived from this software without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED 17 | # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 18 | # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 19 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 23 | # POSSIBILITY OF SUCH DAMAGE. 24 | 25 | """ 26 | The is the uninstall CLI interface to the robot_upstart package. 27 | """ 28 | 29 | from robot_upstart.uninstall_script import main 30 | 31 | if __name__ == "__main__": 32 | exit(main()) 33 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [develop] 2 | script_dir=$base/lib/robot_upstart 3 | [install] 4 | install_scripts=$base/lib/robot_upstart 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | from glob import glob 4 | 5 | PACKAGE_NAME = 'robot_upstart' 6 | 7 | setup( 8 | name=PACKAGE_NAME, 9 | version='1.0.4', 10 | packages=[PACKAGE_NAME], 11 | data_files=[ 12 | ('share/ament_index/resource_index/packages', 13 | ['resource/' + PACKAGE_NAME]), 14 | ('share/' + PACKAGE_NAME, ['package.xml']), 15 | (os.path.join('lib', PACKAGE_NAME, 'scripts'), glob('scripts/*')), 16 | (os.path.join('share', PACKAGE_NAME, 'scripts'), glob('scripts/*')), 17 | (os.path.join('share', PACKAGE_NAME, 'templates'), glob('templates/*')), 18 | (os.path.join('share', PACKAGE_NAME, 'test'), glob('test/*.py')), 19 | (os.path.join('share', PACKAGE_NAME, 'test/launch'), glob('test/launch/*.launch')), 20 | ], 21 | install_requires=['setuptools'], 22 | zip_safe=True, 23 | maintainer='Tony Baltovski', 24 | maintainer_email='tbaltovski@clearpathrobotics.com', 25 | description='The robot_upstart package provides scripts which may be used to install and uninstall Ubuntu Linux upstart jobs which launch groups of roslaunch files.', 26 | license='BSD', 27 | tests_require=['pytest'], 28 | entry_points={ 29 | 'console_scripts': [ 30 | ], 31 | }, 32 | ) 33 | -------------------------------------------------------------------------------- /templates/job-start.em: -------------------------------------------------------------------------------- 1 | @# 2 | @# Author: Mike Purvis 3 | @# Copyright (c) 2013-2014, Clearpath Robotics, Inc. 4 | @# 5 | @# Redistribution and use in source and binary forms, with or without 6 | @# modification, are permitted provided that the following conditions are met: 7 | @# * Redistributions of source code must retain the above copyright 8 | @# notice, this list of conditions and the following disclaimer. 9 | @# * Redistributions in binary form must reproduce the above copyright 10 | @# notice, this list of conditions and the following disclaimer in the 11 | @# documentation and/or other materials provided with the distribution. 12 | @# * Neither the name of Clearpath Robotics, Inc. nor the 13 | @# names of its contributors may be used to endorse or promote products 14 | @# derived from this software without specific prior written permission. 15 | @# 16 | @# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | @# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | @# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | @# DISCLAIMED. IN NO EVENT SHALL CLEARPATH ROBOTICS, INC. BE LIABLE FOR ANY 20 | @# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | @# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | @# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | @# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | @# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | @# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | @# 27 | @# Please send comments, questions, or patches to code@clearpathrobotics.com 28 | #!/bin/bash 29 | # THIS IS A GENERATED FILE, NOT RECOMMENDED TO EDIT. 30 | 31 | function log() { 32 | logger -s -p user.$1 ${@@:2} 33 | } 34 | 35 | log info "@(name): Using workspace setup file @(workspace_setup)" 36 | 37 | @[if rmw]@ 38 | export RMW_IMPLEMENTATION=@(rmw) 39 | @[else]@ 40 | export RMW_IMPLEMENTATION=rmw_fastrtps_cpp 41 | @[end if]@ 42 | 43 | rmw="@(rmw)" 44 | rmw_config="@(rmw_config)" 45 | 46 | if [[ "$rmw" == "rmw_fastrtps_cpp" ]] 47 | then 48 | if [[ ! -z $rmw_config ]] 49 | then 50 | export FASTRTPS_DEFAULT_PROFILES_FILE=$rmw_config 51 | fi 52 | elif [[ "$rmw" == "rmw_cyclonedds_cpp" ]] 53 | then 54 | if [[ ! -z $rmw_config ]] 55 | then 56 | export CYCLONEDDS_URI=$rmw_config 57 | fi 58 | fi 59 | 60 | @[if ros_domain_id]@ 61 | export ROS_DOMAIN_ID=@(ros_domain_id) 62 | @[end if]@ 63 | 64 | source @(workspace_setup) 65 | JOB_FOLDER=@(job_path) 66 | 67 | log_path="@(log_path)" 68 | if [[ ! -d $log_path ]]; then 69 | CREATED_LOGDIR=true 70 | trap 'CREATED_LOGDIR=false' ERR 71 | log warn "@(name): The log directory you specified \"$log_path\" does not exist. Attempting to create." 72 | mkdir -p $log_path 2>/dev/null 73 | chown @(user):@(user) $log_path 2>/dev/null 74 | chmod ug+wr $log_path 2>/dev/null 75 | trap - ERR 76 | # if log_path could not be created, default to tmp 77 | if [[ $CREATED_LOGDIR == false ]]; then 78 | log warn "@(name): The log directory you specified \"$log_path\" cannot be created. Defaulting to \"/tmp\"!" 79 | log_path="/tmp" 80 | fi 81 | fi 82 | 83 | @[if interface]@ 84 | export ROS_IP=`ros2 run robot_upstart getifip @(interface)` 85 | if [ "$ROS_IP" = "" ]; then 86 | log err "@(name): No IP address on @(interface), cannot ros2 launch." 87 | exit 1 88 | fi 89 | @[else]@ 90 | export ROS_HOSTNAME=$(hostname) 91 | @[end if]@ 92 | 93 | export ROS_HOME=${ROS_HOME:=$(echo ~@(user))/.ros} 94 | export ROS_LOG_DIR=$log_path 95 | 96 | log info "@(name): Launching ROS_HOSTNAME=$ROS_HOSTNAME, ROS_IP=$ROS_IP, ROS_MASTER_URI=$ROS_MASTER_URI, ROS_HOME=$ROS_HOME, ROS_LOG_DIR=$log_path" 97 | 98 | # If xacro files are present in job folder, generate and expand an amalgamated urdf. 99 | XACRO_FILENAME=$log_path/@(name).xacro 100 | XACRO_ROBOT_NAME=$(echo "@(name)" | cut -d- -f1) 101 | ros2 run robot_upstart mkxacro $JOB_FOLDER $XACRO_ROBOT_NAME > $XACRO_FILENAME 102 | if [[ "$?" == "0" ]]; then 103 | URDF_FILENAME=$log_path/@(name).urdf 104 | ros2 run xacro xacro $XACRO_FILENAME -o $URDF_FILENAME 105 | if [[ "$?" == "0" ]]; then 106 | log info "@(name): Generated URDF: $URDF_FILENAME" 107 | else 108 | log warn "@(name): URDF macro expansion failure. Robot description will not function." 109 | fi 110 | export ROBOT_URDF_FILENAME=$URDF_FILENAME 111 | fi 112 | 113 | # Assemble amalgamated launchfile. 114 | LAUNCH_FILENAME=$log_path/@(name).launch.py 115 | ros2 run robot_upstart mklaunch $JOB_FOLDER > $LAUNCH_FILENAME 116 | if [[ "$?" != "0" ]]; then 117 | log err "@(name): Unable to generate amalgamated launchfile. $JOB_FOLDER $LAUNCH_FILENAME" 118 | exit 1 119 | fi 120 | log info "@(name): Generated launchfile: $LAUNCH_FILENAME" 121 | 122 | # Warn and exit if setpriv is missing from the system. 123 | which setpriv > /dev/null 124 | if [ "$?" != "0" ]; then 125 | log err "@(name): Can't launch as unprivileged user without setpriv. Please install the setpriv package." 126 | exit 1 127 | fi 128 | 129 | # Punch it. 130 | setpriv --reuid @(user) ros2 launch $LAUNCH_FILENAME @(roslaunch_wait?'--wait ')& 131 | PID=$! 132 | 133 | log info "@(name): Started ros2 launch as background process, PID $PID, ROS_LOG_DIR=$ROS_LOG_DIR" 134 | echo "$PID" > $log_path/@(name).pid 135 | 136 | wait "$PID" 137 | -------------------------------------------------------------------------------- /templates/job-stop.em: -------------------------------------------------------------------------------- 1 | @# 2 | @# Author: Mike Purvis 3 | @# Copyright (c) 2013-2014, Clearpath Robotics, Inc. 4 | @# 5 | @# Redistribution and use in source and binary forms, with or without 6 | @# modification, are permitted provided that the following conditions are met: 7 | @# * Redistributions of source code must retain the above copyright 8 | @# notice, this list of conditions and the following disclaimer. 9 | @# * Redistributions in binary form must reproduce the above copyright 10 | @# notice, this list of conditions and the following disclaimer in the 11 | @# documentation and/or other materials provided with the distribution. 12 | @# * Neither the name of Clearpath Robotics, Inc. nor the 13 | @# names of its contributors may be used to endorse or promote products 14 | @# derived from this software without specific prior written permission. 15 | @# 16 | @# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | @# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | @# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | @# DISCLAIMED. IN NO EVENT SHALL CLEARPATH ROBOTICS, INC. BE LIABLE FOR ANY 20 | @# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | @# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | @# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | @# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | @# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | @# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | @# 27 | @# Please send comments, questions, or patches to code@clearpathrobotics.com !/bin/bash 28 | #!/bin/bash 29 | # THIS IS A GENERATED FILE, NOT RECOMMENDED TO EDIT. 30 | 31 | PID=$(cat @(log_path)/@(name).pid) 32 | logger -p user.info "Attempting to stop @(name) (PID $PID)" 33 | kill $PID 34 | logger -s -p user.info "Waiting for ros2 launch process to end" 35 | while kill -0 $PID 2>/dev/null; do sleep 0.2; done 36 | -------------------------------------------------------------------------------- /templates/job.conf.em: -------------------------------------------------------------------------------- 1 | @# 2 | @# Author: Mike Purvis 3 | @# Copyright (c) 2013-2014, Clearpath Robotics, Inc. 4 | @# 5 | @# Redistribution and use in source and binary forms, with or without 6 | @# modification, are permitted provided that the following conditions are met: 7 | @# * Redistributions of source code must retain the above copyright 8 | @# notice, this list of conditions and the following disclaimer. 9 | @# * Redistributions in binary form must reproduce the above copyright 10 | @# notice, this list of conditions and the following disclaimer in the 11 | @# documentation and/or other materials provided with the distribution. 12 | @# * Neither the name of Clearpath Robotics, Inc. nor the 13 | @# names of its contributors may be used to endorse or promote products 14 | @# derived from this software without specific prior written permission. 15 | @# 16 | @# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | @# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | @# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | @# DISCLAIMED. IN NO EVENT SHALL CLEARPATH ROBOTICS, INC. BE LIABLE FOR ANY 20 | @# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | @# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | @# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | @# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | @# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | @# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | @# 27 | @# Please send comments, questions, or patches to code@clearpathrobotics.com 28 | # THIS IS A GENERATED FILE, NOT RECOMMENDED TO EDIT. 29 | 30 | description "bringup @(name)" 31 | 32 | @[if interface]@ 33 | @# Start and stop ROS with the availability of the network interface 34 | @# which is used as the source for ROS_IP. 35 | start on net-device-up IFACE=@(interface) 36 | stop on net-device-down IFACE=@(interface) 37 | @[else]@ 38 | @# We must start ROS after the local filesystems are up, since the catkin setup 39 | @# script requires the use of /tmp. 40 | start on local-filesystems 41 | stop on runlevel [016] 42 | @[end if]@ 43 | 44 | console log 45 | respawn 46 | 47 | exec @(name)-start 48 | pre-stop exec @(name)-stop 49 | -------------------------------------------------------------------------------- /templates/systemd_job.conf.em: -------------------------------------------------------------------------------- 1 | @# 2 | @# Author: Mike Purvis 3 | @# Copyright (c) 2013-2014, Clearpath Robotics, Inc. 4 | @# 5 | @# Redistribution and use in source and binary forms, with or without 6 | @# modification, are permitted provided that the following conditions are met: 7 | @# * Redistributions of source code must retain the above copyright 8 | @# notice, this list of conditions and the following disclaimer. 9 | @# * Redistributions in binary form must reproduce the above copyright 10 | @# notice, this list of conditions and the following disclaimer in the 11 | @# documentation and/or other materials provided with the distribution. 12 | @# * Neither the name of Clearpath Robotics, Inc. nor the 13 | @# names of its contributors may be used to endorse or promote products 14 | @# derived from this software without specific prior written permission. 15 | @# 16 | @# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | @# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | @# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | @# DISCLAIMED. IN NO EVENT SHALL CLEARPATH ROBOTICS, INC. BE LIABLE FOR ANY 20 | @# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | @# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | @# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | @# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | @# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | @# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | @# 27 | @# Please send comments, questions, or patches to code@clearpathrobotics.com 28 | # THIS IS A GENERATED FILE, NOT RECOMMENDED TO EDIT. 29 | 30 | [Unit] 31 | Description="bringup @(name)" 32 | After=@(systemd_after) 33 | 34 | [Service] 35 | Type=simple 36 | ExecStart=/usr/sbin/@(name)-start 37 | 38 | [Install] 39 | WantedBy=multi-user.target 40 | -------------------------------------------------------------------------------- /test/launch/a.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /test/launch/b.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /test/test_basics.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import em 4 | import os 5 | import shutil 6 | import subprocess 7 | import tempfile 8 | import unittest 9 | 10 | import robot_upstart 11 | 12 | 13 | class TestBasics(unittest.TestCase): 14 | 15 | def setUp(self): 16 | self.root_dir = tempfile.mkdtemp() 17 | 18 | def tearDown(self): 19 | shutil.rmtree(self.root_dir) 20 | 21 | # A bit of extra cleanup required which doesn't happen in Interpreter.shutdown. This isn't necessary 22 | # in the usual course of things where we only do one Job operation per run, but it is required for 23 | # testing, where we do a bunch of them back to back. 24 | em.Interpreter._wasProxyInstalled = False 25 | 26 | def pjoin(self, *p): 27 | return os.path.join(self.root_dir, *p) 28 | 29 | def test_install(self): 30 | j = robot_upstart.Job(name="foo") 31 | j.install(sudo=None, root=self.root_dir) 32 | 33 | self.assertTrue(os.path.exists(self.pjoin("usr/sbin/foo-start")), "Start script not created.") 34 | self.assertTrue(os.path.exists(self.pjoin("usr/sbin/foo-stop")), "Stop script not created.") 35 | 36 | # Systemd config 37 | self.assertTrue(os.path.exists(self.pjoin("etc/systemd/system/multi-user.target.wants/foo.service")), "Systemd service file not created.") 38 | 39 | # Upstart config 40 | #self.assertTrue(os.path.exists(self.pjoin("etc/init/foo.conf")), "Upstart configuration file not created.") 41 | 42 | self.assertEqual(0, subprocess.call(["bash", "-n", self.pjoin("usr/sbin/foo-start")]), 43 | "Start script not valid bash syntax.") 44 | self.assertEqual(0, subprocess.call(["bash", "-n", self.pjoin("usr/sbin/foo-stop")]), 45 | "Stop script not valid bash syntax.") 46 | 47 | def test_install_launcher(self): 48 | j = robot_upstart.Job(name="bar") 49 | j.add('robot_upstart', 'test/launch/a.launch') 50 | j.install(sudo=None, root=self.root_dir) 51 | 52 | self.assertTrue(os.path.exists(self.pjoin("etc/ros", os.getenv("ROS_DISTRO"), "bar.d/a.launch")), 53 | "Launch file not copied.") 54 | self.assertFalse(os.path.exists(self.pjoin("etc/ros", os.getenv("ROS_DISTRO"), "bar.d/b.launch")), 55 | "Launch copied which shouldn't have been.") 56 | 57 | def test_install_glob(self): 58 | j = robot_upstart.Job(name="baz") 59 | j.add('robot_upstart', glob='test/launch/*.launch') 60 | j.install(sudo=None, root=self.root_dir) 61 | 62 | self.assertTrue(os.path.exists(self.pjoin("etc/ros", os.getenv("ROS_DISTRO"), "baz.d/a.launch")), 63 | "Launch file not copied.") 64 | self.assertTrue(os.path.exists(self.pjoin("etc/ros", os.getenv("ROS_DISTRO"), "baz.d/b.launch")), 65 | "Launch file not copied.") 66 | 67 | def test_uninstall(self): 68 | j = robot_upstart.Job(name="boo") 69 | j.add('robot_upstart', glob='test/launch/*.launch') 70 | j.install(sudo=None, root=self.root_dir) 71 | j.uninstall(sudo=None, root=self.root_dir) 72 | 73 | self.assertFalse(os.path.exists(self.pjoin("etc/ros", os.getenv("ROS_DISTRO"), "boo.d")), 74 | "Job dir not removed.") 75 | self.assertFalse(os.path.exists(self.pjoin("etc/ros", os.getenv("ROS_DISTRO"), "usr/sbin/foo-start")), 76 | "Start script not removed.") 77 | 78 | def test_uninstall_user_file(self): 79 | j = robot_upstart.Job(name="goo") 80 | j.add('robot_upstart', glob='test/launch/*.launch') 81 | j.install(sudo=None, root=self.root_dir) 82 | with open(self.pjoin("etc/ros", os.getenv("ROS_DISTRO"), "goo.d/c.launch"), "w") as f: 83 | f.write("") 84 | j.uninstall(sudo=None, root=self.root_dir) 85 | 86 | self.assertTrue(os.path.exists(self.pjoin("etc/ros", os.getenv("ROS_DISTRO"), "goo.d/c.launch")), 87 | "User launch file wrongly removed.") 88 | 89 | 90 | if __name__ == '__main__': 91 | import rosunit 92 | rosunit.unitrun('robot_upstart', 'test_basics', TestBasics) 93 | --------------------------------------------------------------------------------