launch.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 #
00003 # License: BSD
00004 #   https://raw.github.com/robotics-in-concert/rocon_multimaster/license/LICENSE
00005 #
00006 
00007 ##############################################################################
00008 # Imports
00009 ##############################################################################
00010 
00011 import os
00012 import argparse
00013 import subprocess
00014 import signal
00015 import sys
00016 from time import sleep
00017 import roslaunch
00018 import tempfile
00019 import rocon_python_utils
00020 import rosgraph
00021 import rocon_console.console as console
00022 import xml.etree.ElementTree as ElementTree
00023 
00024 ##############################################################################
00025 # Global variables
00026 ##############################################################################
00027 
00028 processes = []
00029 roslaunch_pids = []
00030 hold = False  # keep terminals open when sighandling them
00031 
00032 ##############################################################################
00033 # Methods
00034 ##############################################################################
00035 
00036 
00037 def preexec():
00038     '''
00039       Don't forward signals.
00040 
00041       http://stackoverflow.com/questions/3791398/how-to-stop-python-from-propagating-signals-to-subprocesses
00042     '''
00043     os.setpgrp()  # setpgid(0,0)
00044 
00045 
00046 def get_roslaunch_pids(parent_pid):
00047     '''
00048       Search the pstree of the parent pid for any rocon launched process.
00049     '''
00050     ps_command = subprocess.Popen("ps -o pid -o comm --ppid %d --noheaders" % parent_pid, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
00051     ps_output = ps_command.stdout.read()
00052 
00053     retcode = ps_command.wait()
00054     pids = []
00055     if retcode == 0:
00056         for pair in ps_output.split("\n")[:-1]:
00057             [pid, command] = pair.lstrip(' ').split(" ")
00058             if command == 'roslaunch':
00059                 pids.append(int(pid))
00060             else:
00061                 pids.extend(get_roslaunch_pids(int(pid)))
00062     else:
00063         # Presume this roslaunch was killed by ctrl-c or terminated already.
00064         # Am not worrying about classifying between the above presumption and real errors for now
00065         pass
00066     return pids
00067 
00068 
00069 def signal_handler(sig, frame):
00070     global processes
00071     global roslaunch_pids
00072     global hold
00073     for p in processes:
00074         roslaunch_pids.extend(get_roslaunch_pids(p.pid))
00075     # kill roslaunch's
00076     for pid in roslaunch_pids:
00077         try:
00078             os.kill(pid, signal.SIGHUP)
00079         except OSError:
00080             continue
00081     for pid in roslaunch_pids:
00082         console.pretty_println("Terminating roslaunch [pid: %d]" % pid, console.bold)
00083         rocon_python_utils.system.wait_pid(pid)
00084         #console.pretty_println("Terminated roslaunch [pid: %d]" % pid, console.bold)
00085     sleep(1)
00086     if hold:
00087         try:
00088             raw_input("Press <Enter> to close terminals...")
00089         except RuntimeError:
00090             pass  # this happens when you ctrl-c again instead of enter
00091     # now kill konsoles
00092     for p in processes:
00093         os.killpg(p.pid, signal.SIGTERM)
00094         #p.terminate()
00095 
00096 
00097 def _process_arg_tag(tag, args_dict=None):
00098     '''
00099       Process the arg tag. Kind of hate replicating what roslaunch does with
00100       arg tags, but there's no easy way to pull roslaunch code.
00101 
00102       @param args_dict : dictionary of args previously discovered
00103     '''
00104     name = tag.get('name')  # returns None if not found.
00105     if name is None:
00106         console.error("<arg> tag must have a name attribute.")
00107         sys.exit(1)
00108     value = tag.get('value')
00109     default = tag.get('default')
00110     #print("Arg tag processing: (%s, %s, %s)" % (name, value, default))
00111     if value is not None and default is not None:
00112         console.error("<arg> tag must have one and only one of value/default attributes specified.")
00113         sys.exit(1)
00114     if value is None and default is None:
00115         console.error("<arg> tag must have one of value/default attributes specified.")
00116         sys.exit(1)
00117     if value is None:
00118         value = default
00119     if value and '$' in value:
00120         value = roslaunch.substitution_args.resolve_args(value, args_dict)
00121     return (name, value)
00122 
00123 
00124 def parse_rocon_launcher(rocon_launcher, default_roslaunch_options, args_mappings={}):
00125     '''
00126       Parses an rocon multi-launcher (xml file).
00127 
00128       :param rocon_launcher str: xml string in rocon_launch format
00129       :param default_roslaunch_options list: options to pass to roslaunch (usually "--screen")
00130       :param args_mappings dict: command line mapping overrides, { arg_name : arg_value }
00131       @return launchers : list with launcher parameters as dictionary elements of the list.
00132 
00133       @raise IOError : if it can't find any of the individual launchers on the filesystem.
00134     '''
00135     tree = ElementTree.parse(rocon_launcher)
00136     root = tree.getroot()
00137     # should check for root concert tag
00138     launchers = []
00139     ports = []
00140     default_port = 11311
00141     # These are intended for re-use in launcher args via $(arg ...) like regular roslaunch
00142     vars_dict = {}
00143     # We do this the roslaunch way since we use their resolvers, even if we only do it for args.
00144     vars_dict['arg'] = {}
00145     args_dict = vars_dict['arg']  # convenience ref to the vars_dict['args'] variable
00146     for tag in root.findall('arg'):
00147         name, value = _process_arg_tag(tag, args_dict)
00148         args_dict[name] = value
00149     args_dict.update(args_mappings)  # bring in command line overrides
00150     for launch in root.findall('launch'):
00151         parameters = {}
00152         parameters['args'] = []
00153         parameters['options'] = default_roslaunch_options
00154         parameters['package'] = launch.get('package')
00155         parameters['name'] = launch.get('name')
00156         parameters['title'] = launch.get('title')
00157         parameters['path'] = rocon_python_utils.ros.find_resource(parameters['package'], parameters['name'])  # raises an IO error if there is a problem.
00158         parameters['port'] = launch.get('port', str(default_port))
00159         if parameters['port'] == str(default_port):
00160             default_port += 1
00161         if parameters['port'] in ports:
00162             parameters['options'] = parameters['options'] + " " + "--wait"
00163         else:
00164             ports.append(parameters['port'])
00165         if parameters['title'] is None:
00166             parameters['title'] = 'rocon_launch:%s' % parameters['port']
00167         launchers.append(parameters)
00168         for tag in launch.findall('arg'):
00169             name, value = _process_arg_tag(tag, vars_dict)
00170             parameters['args'].append((name, value))
00171     return launchers
00172 
00173 
00174 def parse_arguments():
00175     global hold
00176     parser = argparse.ArgumentParser(description="Rocon's multiple master launcher.")
00177     terminal_group = parser.add_mutually_exclusive_group()
00178     terminal_group.add_argument('-k', '--konsole', default=False, action='store_true', help='spawn individual ros systems via multiple konsole terminals')
00179     terminal_group.add_argument('-g', '--gnome', default=False, action='store_true', help='spawn individual ros systems via multiple gnome terminals')
00180     parser.add_argument('--screen', action='store_true', help='run each roslaunch with the --screen option')
00181     parser.add_argument('--no-terminals', action='store_true', help='do not spawn terminals for each roslaunch')
00182     parser.add_argument('--hold', action='store_true', help='hold terminals open after upon completion (incompatible with --no-terminals)')
00183     # Force package, launcher pairs, I like this better than roslaunch style which is a bit vague
00184     parser.add_argument('package', nargs='?', default='', help='name of the package in which to find the concert launcher')
00185     parser.add_argument('launcher', nargs=1, help='name of the concert launch configuration (xml) file')
00186     #parser.add_argument('launchers', nargs='+', help='package and concert launch configuration (xml) file configurations, roslaunch style')
00187     mappings = rosgraph.names.load_mappings(sys.argv)  # gets the arg mappings, e.g. scheduler_type:=simple
00188     argv = rosgraph.myargv(sys.argv[1:])  # strips the mappings
00189     args = parser.parse_args(argv)
00190     hold = args.hold  # global argument
00191     return (args, mappings)
00192 
00193 
00194 def choose_terminal(gnome_flag, konsole_flag):
00195     '''
00196       Use ubuntu's x-terminal-emulator to choose the shell, or over-ride if it there is a flag.
00197     '''
00198     if konsole_flag:
00199         if not rocon_python_utils.system.which('konsole'):
00200             console.error("Cannot find 'konsole' [hint: try --gnome for gnome-terminal instead]")
00201             sys.exit(1)
00202         return 'konsole'
00203     elif gnome_flag:
00204         if not rocon_python_utils.system.which('gnome-terminal'):
00205             console.error("Cannot find 'gnome' [hint: try --konsole for konsole instead]")
00206             sys.exit(1)
00207         return 'gnome-terminal'
00208     else:
00209         if not rocon_python_utils.system.which('x-terminal-emulator'):
00210             console.error("Cannot find 'x-terminal-emulator' [hint: try --gnome or --konsole instead]")
00211             sys.exit(1)
00212         p = subprocess.Popen([rocon_python_utils.system.which('update-alternatives'), '--query', 'x-terminal-emulator'], stdout=subprocess.PIPE)
00213         terminal = None
00214         for line in p.stdout:
00215             if line.startswith("Value:"):
00216                 terminal = os.path.basename(line.split()[1])
00217                 break
00218         if terminal not in ["gnome-terminal", "gnome-terminal.wrapper", "konsole"]:
00219             console.warning("You are using an esoteric unsupported terminal [%s]" % terminal)
00220             if rocon_python_utils.system.which('konsole'):
00221                 terminal = 'konsole'
00222                 console.warning(" --> falling back to 'konsole'")
00223             elif rocon_python_utils.system.which('gnome-terminal'):
00224                 console.warning(" --> falling back to 'gnome-terminal'")
00225                 terminal = 'gnome-terminal'
00226             else:
00227                 console.error("Unsupported terminal set for 'x-terminal-emulator' [%s][hint: try --gnome or --konsole instead]" % terminal)
00228                 sys.exit(1)
00229         return terminal
00230 
00231 
00232 def main():
00233     global processes
00234     global roslaunch_pids
00235     signal.signal(signal.SIGINT, signal_handler)
00236     (args, mappings) = parse_arguments()
00237     terminal = None
00238     if not args.no_terminals:
00239         if not rocon_python_utils.system.which('konsole') and not rocon_python_utils.system.which('gnome-terminal')and not rocon_python_utils.system.which('x-terminal-emulator'):
00240             console.error("Cannot find a suitable terminal [x-terminal-emulator, konsole, gnome-termional]")
00241             sys.exit(1)
00242         terminal = choose_terminal(args.gnome, args.konsole)
00243 
00244     if args.package == '':
00245         rocon_launcher = roslaunch.rlutil.resolve_launch_arguments(args.launcher)[0]
00246     else:
00247         rocon_launcher = roslaunch.rlutil.resolve_launch_arguments([args.package] + args.launcher)[0]
00248     if args.screen:
00249         roslaunch_options = "--screen"
00250     else:
00251         roslaunch_options = ""
00252     launchers = parse_rocon_launcher(rocon_launcher, roslaunch_options, mappings)
00253     temporary_launchers = []
00254     for launcher in launchers:
00255         console.pretty_println("Launching [%s, %s] on port %s" % (launcher['package'], launcher['name'], launcher['port']), console.bold)
00256         ##########################
00257         # Customise the launcher
00258         ##########################
00259         temp = tempfile.NamedTemporaryFile(mode='w+t', delete=False)
00260         print("Launching %s" % temp.name)
00261         launcher_filename = rocon_python_utils.ros.find_resource(launcher['package'], launcher['name'])
00262         launch_text = '<launch>\n'
00263         if args.screen:
00264             launch_text += '  <param name="rocon/screen" value="true"/>\n'
00265         else:
00266             launch_text += '  <param name="rocon/screen" value="false"/>\n'
00267         launch_text += '  <include file="%s">\n' % launcher_filename
00268         for (arg_name, arg_value) in launcher['args']:
00269             launch_text += '    <arg name="%s" value="%s"/>\n' % (arg_name, arg_value)
00270         launch_text += '  </include>\n'
00271         launch_text += '</launch>\n'
00272         #print launch_text
00273         temp.write(launch_text)
00274         temp.close()  # unlink it later
00275         temporary_launchers.append(temp)
00276         ##########################
00277         # Start the terminal
00278         ##########################
00279         if terminal == 'konsole':
00280             p = subprocess.Popen([terminal, '-p', 'tabtitle=%s' % launcher['title'], '--nofork', '--hold', '-e', "/bin/bash", "-c", "roslaunch %s --disable-title --port %s %s" %
00281                               (launcher['options'], launcher['port'], temp.name)], preexec_fn=preexec)
00282         elif terminal == 'gnome-terminal.wrapper' or terminal == 'gnome-terminal':
00283             # --disable-factory inherits the current environment, bit wierd.
00284             cmd = ['gnome-terminal', '--title=%s' % launcher['title'], '--disable-factory', "-e", "/bin/bash -c 'roslaunch %s --disable-title --port %s %s';/bin/bash" %
00285                               (launcher['options'], launcher['port'], temp.name)]
00286             p = subprocess.Popen(cmd, preexec_fn=preexec)
00287         else:
00288             cmd = ["roslaunch"]
00289             if launcher['options']:
00290                 cmd.append(launcher['options'])
00291             cmd.extend(["--port", launcher['port'], temp.name])
00292             p = subprocess.Popen(cmd, preexec_fn=preexec)
00293         processes.append(p)
00294     signal.pause()
00295     # Have to unlink them here rather than in the for loop above, because the whole gnome-terminal
00296     # subprocess takes a while to kick in (in the background) and the unlinking may occur before
00297     # it actually runs the roslaunch that needs the file.
00298     for temporary_launcher in temporary_launchers:
00299         os.unlink(temporary_launcher.name)


rocon_launch
Author(s): Daniel Stonier
autogenerated on Fri May 2 2014 10:35:40