00001
00002
00003
00004
00005
00006
00007
00008
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
00026
00027
00028 processes = []
00029 roslaunch_pids = []
00030 hold = False
00031
00032
00033
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()
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
00064
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
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
00085 sleep(1)
00086 if hold:
00087 try:
00088 raw_input("Press <Enter> to close terminals...")
00089 except RuntimeError:
00090 pass
00091
00092 for p in processes:
00093 os.killpg(p.pid, signal.SIGTERM)
00094
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')
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
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
00138 launchers = []
00139 ports = []
00140 default_port = 11311
00141
00142 vars_dict = {}
00143
00144 vars_dict['arg'] = {}
00145 args_dict = vars_dict['arg']
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)
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'])
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
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
00187 mappings = rosgraph.names.load_mappings(sys.argv)
00188 argv = rosgraph.myargv(sys.argv[1:])
00189 args = parser.parse_args(argv)
00190 hold = args.hold
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
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
00273 temp.write(launch_text)
00274 temp.close()
00275 temporary_launchers.append(temp)
00276
00277
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
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
00296
00297
00298 for temporary_launcher in temporary_launchers:
00299 os.unlink(temporary_launcher.name)