Package roslaunch :: Module core
[frames] | no frames]

Source Code for Module roslaunch.core

  1  # Software License Agreement (BSD License) 
  2  # 
  3  # Copyright (c) 2008, Willow Garage, Inc. 
  4  # All rights reserved. 
  5  # 
  6  # Redistribution and use in source and binary forms, with or without 
  7  # modification, are permitted provided that the following conditions 
  8  # are met: 
  9  # 
 10  #  * Redistributions of source code must retain the above copyright 
 11  #    notice, this list of conditions and the following disclaimer. 
 12  #  * Redistributions in binary form must reproduce the above 
 13  #    copyright notice, this list of conditions and the following 
 14  #    disclaimer in the documentation and/or other materials provided 
 15  #    with the distribution. 
 16  #  * Neither the name of Willow Garage, Inc. nor the names of its 
 17  #    contributors may be used to endorse or promote products derived 
 18  #    from this software without specific prior written permission. 
 19  # 
 20  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 21  # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
 22  # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 
 23  # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 
 24  # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 25  # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 26  # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 27  # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
 28  # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
 29  # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 30  # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 31  # POSSIBILITY OF SUCH DAMAGE. 
 32   
 33  """ 
 34  Core roslaunch model and lower-level utility routines. 
 35  """ 
 36   
 37  import os 
 38  import logging 
 39   
 40  import socket 
 41  import sys 
 42  import xmlrpclib 
 43   
 44  import rospkg 
 45   
 46  import rosgraph 
 47  import rosgraph.names  
 48  import rosgraph.network 
 49   
 50  from xml.sax.saxutils import escape  
 51  try: 
 52      unicode 
 53  except NameError: 
 54      # Python 3: for _xml_escape 
 55      basestring = unicode = str 
 56       
57 -class RLException(Exception):
58 """Base roslaunch exception type""" 59 pass
60 61 ## Phases allow executables to be assigned to a particular run period 62 PHASE_SETUP = 'setup' 63 PHASE_RUN = 'run' 64 PHASE_TEARDOWN = 'teardown' 65 66 _child_mode = False
67 -def is_child_mode():
68 """ 69 :returns: ``True`` if roslaunch is running in remote child mode, ``bool`` 70 """ 71 return _child_mode
72 -def set_child_mode(child_mode):
73 """ 74 :param child_mode: True if roslaunch is running in remote 75 child mode, ``bool`` 76 """ 77 global _child_mode 78 _child_mode = child_mode
79
80 -def is_machine_local(machine):
81 """ 82 Check to see if machine is local. NOTE: a machine is not local if 83 its user credentials do not match the current user. 84 :param machine: Machine, ``Machine`` 85 :returns: True if machine is local and doesn't require remote login, ``bool`` 86 """ 87 try: 88 machine_ips = [host[4][0] for host in socket.getaddrinfo(machine.address, 0, 0, 0, socket.SOL_TCP)] 89 except socket.gaierror: 90 raise RLException("cannot resolve host address for machine [%s]"%machine.address) 91 local_addresses = ['localhost'] + rosgraph.network.get_local_addresses() 92 # check 127/8 and local addresses 93 is_local = ([ip for ip in machine_ips if (ip.startswith('127.') or ip == '::1')] != []) or (set(machine_ips) & set(local_addresses) != set()) 94 95 #491: override local to be ssh if machine.user != local user 96 if is_local and machine.user: 97 import getpass 98 is_local = machine.user == getpass.getuser() 99 return is_local
100 101 _printlog_handlers = [] 102 _printerrlog_handlers = []
103 -def printlog(msg):
104 """ 105 Core utility for printing message to stdout as well as printlog handlers 106 :param msg: message to print, ``str`` 107 """ 108 for h in _printlog_handlers: 109 try: # don't let this bomb out the actual code 110 h(msg) 111 except: 112 pass 113 try: # don't let this bomb out the actual code 114 print msg 115 except: 116 pass
117
118 -def printlog_bold(msg):
119 """ 120 Similar to L{printlog()}, but the message printed to screen is bolded for greater clarity 121 :param msg: message to print, ``str`` 122 """ 123 for h in _printlog_handlers: 124 try: # don't let this bomb out the actual code 125 h(msg) 126 except: 127 pass 128 try: # don't let this bomb out the actual code 129 if sys.platform in ['win32']: 130 print '%s'%msg #windows console is terrifically boring 131 else: 132 print '\033[1m%s\033[0m'%msg 133 except: 134 pass
135
136 -def printerrlog(msg):
137 """ 138 Core utility for printing message to stderr as well as printerrlog handlers 139 :param msg: message to print, ``str`` 140 """ 141 for h in _printerrlog_handlers: 142 try: # don't let this bomb out the actual code 143 h(msg) 144 except: 145 pass 146 # #1003: this has raised IOError (errno 104) in robot use. Better to 147 # trap than let a debugging routine fault code. 148 try: # don't let this bomb out the actual code 149 print >> sys.stderr, '\033[31m%s\033[0m'%msg 150 except: 151 pass
152
153 -def add_printlog_handler(h):
154 """ 155 Register additional handler for printlog() 156 """ 157 _printlog_handlers.append(h)
158
159 -def add_printerrlog_handler(h):
160 """ 161 Register additional handler for printerrlog() 162 """ 163 _printerrlog_handlers.append(h)
164
165 -def clear_printlog_handlers():
166 """ 167 Delete all printlog handlers. required for testing 168 """ 169 del _printlog_handlers[:]
170
171 -def clear_printerrlog_handlers():
172 """ 173 Delete all printerrlog handlers. required for testing 174 """ 175 del _printerrlog_handlers[:]
176
177 -def setup_env(node, machine, master_uri, env=None):
178 """ 179 Create dictionary of environment variables to set for launched 180 process. 181 182 setup_env() will only set ROS_*, PYTHONPATH, and user-specified 183 environment variables. 184 185 :param machine: machine being launched on, ``Machine`` 186 :param node: node that is being launched or None, ``Node`` 187 :param master_uri: ROS master URI, ``str`` 188 :param env: base environment configuration, defaults to ``os.environ`` 189 :returns: process env dictionary, ``dict`` 190 """ 191 if env is None: 192 env = os.environ 193 194 d = env.copy() 195 d[rosgraph.ROS_MASTER_URI] = master_uri 196 197 # add node-specific env args last as they have highest precedence 198 if node: 199 if rosgraph.ROS_NAMESPACE in d: 200 del d[rosgraph.ROS_NAMESPACE] 201 ns = node.namespace 202 if ns[-1] == '/': 203 ns = ns[:-1] 204 if ns: 205 d[rosgraph.ROS_NAMESPACE] = ns 206 for name, value in node.env_args: 207 d[name] = value 208 209 return d
210
211 -def rle_wrapper(fn):
212 """ 213 Wrap lower-level exceptions in RLException class 214 :returns:: function wrapper that throws an RLException if the 215 wrapped function throws an Exception, ``fn`` 216 """ 217 def wrapped_fn(*args): 218 try: 219 return fn(*args) 220 except Exception as e: 221 # we specifically catch RLExceptions and print their messages differently 222 raise RLException("ERROR: %s"%e)
223 return wrapped_fn 224 225 get_ros_root = rospkg.get_ros_root 226 get_master_uri_env = rle_wrapper(rosgraph.get_master_uri) 227 get_ros_package_path = rospkg.get_ros_package_path 228
229 -def remap_localhost_uri(uri, force_localhost=False):
230 """ 231 Resolve localhost addresses to an IP address so that 232 :param uri: XML-RPC URI, ``str`` 233 :param force_localhost: if True, URI is mapped onto the local machine no matter what, ``bool`` 234 """ 235 hostname, port = rosgraph.network.parse_http_host_and_port(uri) 236 if force_localhost or hostname == 'localhost': 237 return rosgraph.network.create_local_xmlrpc_uri(port) 238 else: 239 return uri
240 241 ################################################################## 242 # DATA STRUCTURES 243
244 -class Master:
245 """ 246 Data structure for representing and querying state of master 247 """ 248 __slots__ = ['type', 'auto', 'uri'] 249 ROSMASTER = 'rosmaster' 250 251 # deprecated 252 ZENMASTER = 'zenmaster' 253
254 - def __init__(self, type_=None, uri=None, auto=None):
255 """ 256 Create new Master instance. 257 :param uri: master URI. Defaults to ROS_MASTER_URI environment variable, ``str`` 258 :param type_: Currently only support 'rosmaster', ``str`` 259 """ 260 self.auto = None # no longer used 261 self.type = type_ or Master.ROSMASTER 262 self.uri = uri or get_master_uri_env()
263
264 - def get_host(self):
265 # parse from the URI 266 host, _ = rosgraph.network.parse_http_host_and_port(self.uri) 267 return host
268
269 - def get_port(self):
270 """ 271 Get the port this master is configured for. 272 """ 273 # parse from the URI 274 _, urlport = rosgraph.network.parse_http_host_and_port(self.uri) 275 return urlport
276
277 - def __eq__(self, m2):
278 if not isinstance(m2, Master): 279 return False 280 else: 281 return m2.type == self.type and m2.uri == self.uri
282
283 - def get(self):
284 """ 285 :returns:: XMLRPC proxy for communicating with master, ``xmlrpclib.ServerProxy`` 286 """ 287 return xmlrpclib.ServerProxy(self.uri)
288
289 - def get_multi(self):
290 """ 291 :returns:: multicall XMLRPC proxy for communicating with master, ``xmlrpclib.MultiCall`` 292 """ 293 return xmlrpclib.MultiCall(self.get())
294
295 - def is_running(self):
296 """ 297 Check if master is running. 298 :returns:: True if the master is running, ``bool`` 299 """ 300 try: 301 try: 302 to_orig = socket.getdefaulttimeout() 303 # enable timeout 304 socket.setdefaulttimeout(5.0) 305 logging.getLogger('roslaunch').info('master.is_running[%s]'%self.uri) 306 code, status, val = self.get().getPid('/roslaunch') 307 if code != 1: 308 raise RLException("ERROR: master failed status check: %s"%msg) 309 logging.getLogger('roslaunch.core').debug('master.is_running[%s]: True'%self.uri) 310 return True 311 finally: 312 socket.setdefaulttimeout(to_orig) 313 except: 314 logging.getLogger('roslaunch.core').debug('master.is_running[%s]: False'%self.uri) 315 return False
316 317 ## number of seconds that a child machine is allowed to register with 318 ## the parent before being considered failed 319 _DEFAULT_REGISTER_TIMEOUT = 10.0 320
321 -class Machine(object):
322 """ 323 Data structure for storing information about a machine in the ROS 324 system. Corresponds to the 'machine' tag in the launch 325 specification. 326 """ 327 __slots__ = ['name', 'address', 'ssh_port', 'user', 'password', 'assignable', 328 'env_loader', 'timeout']
329 - def __init__(self, name, address, 330 env_loader=None, ssh_port=22, user=None, password=None, 331 assignable=True, env_args=[], timeout=None):
332 """ 333 :param name: machine name, ``str`` 334 :param address: network address of machine, ``str`` 335 :param env_loader: Path to environment loader, ``str`` 336 :param ssh_port: SSH port number, ``int`` 337 :param user: SSH username, ``str`` 338 :param password: SSH password. Not recommended for use. Use SSH keys instead., ``str`` 339 """ 340 self.name = name 341 self.env_loader = env_loader 342 self.user = user or None 343 self.password = password or None 344 self.address = address 345 self.ssh_port = ssh_port 346 self.assignable = assignable 347 self.timeout = timeout or _DEFAULT_REGISTER_TIMEOUT
348
349 - def __str__(self):
350 return "Machine(name[%s] env_loader[%s] address[%s] ssh_port[%s] user[%s] assignable[%s] timeout[%s])"%(self.name, self.env_loader, self.address, self.ssh_port, self.user, self.assignable, self.timeout)
351 - def __eq__(self, m2):
352 if not isinstance(m2, Machine): 353 return False 354 return self.name == m2.name and \ 355 self.assignable == m2.assignable and \ 356 self.config_equals(m2)
357
358 - def config_key(self):
359 """ 360 Get a key that represents the configuration of the 361 machine. machines with identical configurations have identical 362 keys 363 364 :returns:: configuration key, ``str`` 365 """ 366 return "Machine(address[%s] env_loader[%s] ssh_port[%s] user[%s] password[%s] timeout[%s])"%(self.address, self.env_loader, self.ssh_port, self.user or '', self.password or '', self.timeout)
367
368 - def config_equals(self, m2):
369 """ 370 :returns:: True if machines have identical configurations, ``bool`` 371 """ 372 if not isinstance(m2, Machine): 373 return False 374 return self.config_key() == m2.config_key()
375
376 - def __ne__(self, m2):
377 return not self.__eq__(m2)
378
379 -class Param(object):
380 """ 381 Data structure for storing information about a desired parameter in 382 the ROS system Corresponds to the 'param' tag in the launch 383 specification. 384 """
385 - def __init__(self, key, value):
386 self.key = rosgraph.names.canonicalize_name(key) 387 self.value = value
388 - def __eq__(self, p):
389 if not isinstance(p, Param): 390 return False 391 return p.key == self.key and p.value == self.value
392 - def __ne__(self, p):
393 return not self.__eq__(p)
394 - def __str__(self):
395 return "%s=%s"%(self.key, self.value)
396 - def __repr__(self):
397 return "%s=%s"%(self.key, self.value)
398 399 _local_m = None
400 -def local_machine():
401 """ 402 :returns:: Machine instance representing the local machine, ``Machine`` 403 """ 404 global _local_m 405 if _local_m is None: 406 _local_m = Machine('', 'localhost') 407 return _local_m
408
409 -class Node(object):
410 """ 411 Data structure for storing information about a desired node in 412 the ROS system Corresponds to the 'node' tag in the launch 413 specification. 414 """ 415 __slots__ = ['package', 'type', 'name', 'namespace', \ 416 'machine_name', 'machine', 'args', 'respawn', \ 417 'remap_args', 'env_args',\ 418 'process_name', 'output', 'cwd', 419 'launch_prefix', 'required', 420 'filename'] 421
422 - def __init__(self, package, node_type, name=None, namespace='/', \ 423 machine_name=None, args='', respawn=False, \ 424 remap_args=None,env_args=None, output=None, cwd=None, \ 425 launch_prefix=None, required=False, filename='<unknown>'):
426 """ 427 :param package: node package name, ``str`` 428 :param node_type: node type, ``str`` 429 :param name: node name, ``str`` 430 :param namespace: namespace for node, ``str`` 431 :param machine_name: name of machine to run node on, ``str`` 432 :param args: argument string to pass to node executable, ``str`` 433 :param respawn: if True, respawn node if it dies, ``bool`` 434 :param remap_args: list of [(from, to)] remapping arguments, ``[(str, str)]`` 435 :param env_args: list of [(key, value)] of 436 additional environment vars to set for node, ``[(str, str)]`` 437 :param output: where to log output to, either Node, 'screen' or 'log', ``str`` 438 :param cwd: current working directory of node, either 'node', 'ROS_HOME'. Default: ROS_HOME, ``str`` 439 :param launch_prefix: launch command/arguments to prepend to node executable arguments, ``str`` 440 :param required: node is required to stay running (launch fails if node dies), ``bool`` 441 :param filename: name of file Node was parsed from, ``str`` 442 443 :raises: :exc:`ValueError` If parameters do not validate 444 """ 445 446 self.package = package 447 self.type = node_type 448 self.name = name or None 449 self.namespace = rosgraph.names.make_global_ns(namespace or '/') 450 self.machine_name = machine_name or None 451 self.respawn = respawn 452 self.args = args or '' 453 self.remap_args = remap_args or [] 454 self.env_args = env_args or [] 455 self.output = output 456 self.cwd = cwd 457 if self.cwd == 'ros_home': # be lenient on case 458 self.cwd = 'ROS_HOME' 459 460 self.launch_prefix = launch_prefix or None 461 self.required = required 462 self.filename = filename 463 464 if self.respawn and self.required: 465 raise ValueError("respawn and required cannot both be set to true") 466 467 # validation 468 if self.name and rosgraph.names.SEP in self.name: # #1821, namespaces in nodes need to be banned 469 raise ValueError("node name cannot contain a namespace") 470 if not len(self.package.strip()): 471 raise ValueError("package must be non-empty") 472 if not len(self.type.strip()): 473 raise ValueError("type must be non-empty") 474 if not self.output in ['log', 'screen', None]: 475 raise ValueError("output must be one of 'log', 'screen'") 476 if not self.cwd in ['ROS_HOME', 'node', None]: 477 raise ValueError("cwd must be one of 'ROS_HOME', 'node'") 478 479 # Extra slots for assigning later 480 481 # slot to store the process name in so that we can query the 482 # associated process state 483 self.process_name = None 484 485 # machine is the assigned machine instance. should probably 486 # consider storing this elsewhere as it can be inconsistent 487 # with machine_name and is also a runtime, rather than 488 # configuration property 489 self.machine = None
490 491 492
493 - def xmltype(self):
494 return 'node'
495
496 - def xmlattrs(self):
497 name_str = cwd_str = respawn_str = None 498 if self.name: 499 name_str = self.name 500 if self.cwd: 501 cwd_str = self.cwd 502 503 return [ 504 ('pkg', self.package), 505 ('type', self.type), 506 ('machine', self.machine_name), 507 ('ns', self.namespace), 508 ('args', self.args), 509 ('output', self.output), 510 ('cwd', cwd_str), 511 ('respawn', self.respawn), #not valid on <test> 512 ('name', name_str), 513 ('launch-prefix', self.launch_prefix), 514 ('required', self.required), 515 ]
516 517 #TODO: unify with to_remote_xml using a filter_fn
518 - def to_xml(self):
519 """ 520 convert representation into XML representation. Currently cannot represent private parameters. 521 :returns:: XML representation for remote machine, ``str`` 522 """ 523 t = self.xmltype() 524 attrs = [(a, v) for a, v in self.xmlattrs() if v != None] 525 xmlstr = '<%s %s>\n'%(t, ' '.join(['%s="%s"'%(val[0], _xml_escape(val[1])) for val in attrs])) 526 xmlstr += ''.join([' <remap from="%s" to="%s" />\n'%tuple(r) for r in self.remap_args]) 527 xmlstr += ''.join([' <env name="%s" value="%s" />\n'%tuple(e) for e in self.env_args]) 528 xmlstr += "</%s>"%t 529 return xmlstr
530
531 - def to_remote_xml(self):
532 """ 533 convert representation into remote representation. Remote representation does 534 not include parameter settings or 'machine' attribute 535 :returns:: XML representation for remote machine, ``str`` 536 """ 537 t = self.xmltype() 538 attrs = [(a, v) for a, v in self.xmlattrs() if v != None and a != 'machine'] 539 xmlstr = '<%s %s>\n'%(t, ' '.join(['%s="%s"'%(val[0], _xml_escape(val[1])) for val in attrs])) 540 xmlstr += ''.join([' <remap from="%s" to="%s" />\n'%tuple(r) for r in self.remap_args]) 541 xmlstr += ''.join([' <env name="%s" value="%s" />\n'%tuple(e) for e in self.env_args]) 542 xmlstr += "</%s>"%t 543 return xmlstr
544
545 -def _xml_escape(s):
546 """ 547 Escape string for XML 548 :param s: string to escape, ``str`` 549 :returns:: string with XML entities (<, >, \", &) escaped, ``str`` 550 """ 551 # use official escaping to preserve unicode. 552 # see import at the top for py3k-compat 553 if isinstance(s, basestring): 554 return escape(s, entities={'"': '&quot;'}) 555 else: 556 # don't escape non-string attributes 557 return s
558 559 TEST_TIME_LIMIT_DEFAULT = 1 * 60 #seconds 560 561
562 -class Test(Node):
563 """ 564 A Test is a Node with special semantics that it performs a 565 unit/integration test. The data model is the same except the 566 option to set the respawn flag is removed. 567 """ 568 __slots__ = ['test_name', 'time_limit', 'retry'] 569
570 - def __init__(self, test_name, package, node_type, name=None, \ 571 namespace='/', machine_name=None, args='', \ 572 remap_args=None, env_args=None, time_limit=None, cwd=None, 573 launch_prefix=None, retry=None, filename="<unknown>"):
574 """ 575 Construct a new test node. 576 :param test_name: name of test for recording in test results, ``str`` 577 :param time_limit: number of seconds that a test 578 should run before marked as a failure, ``int/float/long`` 579 """ 580 super(Test, self).__init__(package, node_type, name=name, \ 581 namespace=namespace, \ 582 machine_name=machine_name, args=args, \ 583 remap_args=remap_args, 584 env_args=env_args, 585 #output always is log 586 output='log', cwd=cwd, 587 launch_prefix=launch_prefix, filename=filename) 588 self.test_name = test_name 589 590 self.retry = retry or 0 591 time_limit = time_limit or TEST_TIME_LIMIT_DEFAULT 592 if not type(time_limit) in (float, int, long): 593 raise ValueError("'time-limit' must be a number") 594 time_limit = float(time_limit) #force to floating point 595 if time_limit <= 0: 596 raise ValueError("'time-limit' must be a positive number") 597 598 self.time_limit = time_limit
599
600 - def xmltype(self):
601 return 'test'
602
603 - def xmlattrs(self):
604 """ 605 NOTE: xmlattrs does not necessarily produce identical XML as 606 to what it was initialized with, though the properties are the same 607 """ 608 attrs = Node.xmlattrs(self) 609 attrs = [(a, v) for (a, v) in attrs if a != 'respawn'] 610 attrs.append(('test-name', self.test_name)) 611 612 if self.retry: 613 attrs.append(('retry', str(self.retry))) 614 if self.time_limit != TEST_TIME_LIMIT_DEFAULT: 615 attrs.append(('time-limit', self.time_limit)) 616 return attrs
617 618
619 -class Executable(object):
620 """ 621 Executable is a generic container for exectuable commands. 622 """ 623
624 - def __init__(self, cmd, args, phase=PHASE_RUN):
625 """ 626 :param cmd: name of command to run, ``str`` 627 :param args: arguments to command, ``(str,)`` 628 :param phase: PHASE_SETUP|PHASE_RUN|PHASE_TEARDOWN. Indicates whether the 629 command should be run before, during, or after launch, ``str`` 630 """ 631 self.command = cmd 632 self.args = args 633 self.phase = phase
634 - def __repr__(self):
635 return "%s %s"%(self.command, ' '.join(self.args))
636 - def __str__(self):
637 return "%s %s"%(self.command, ' '.join(self.args))
638
639 -class RosbinExecutable(Executable):
640 """ 641 RosbinExecutables are exectuables stored in ROS_ROOT/bin. 642 """
643 - def __init__(self, cmd, args, phase=PHASE_RUN):
644 super(RosbinExecutable, self).__init__(cmd, args, phase)
645 - def __repr__(self):
646 return "ros/bin/%s %s"%(self.command, ' '.join(self.args))
647 - def __str__(self):
648 return "ros/bin/%s %s"%(self.command, ' '.join(self.args))
649 650
651 -def generate_run_id():
652 """ 653 Utility routine for generating run IDs (UUIDs) 654 :returns: guid, ``str`` 655 """ 656 import uuid 657 return str(uuid.uuid1())
658