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

Source Code for Module roslaunch.loader

  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  # Revision $Id$ 
 34   
 35  """ 
 36  General routines and representations for loading roslaunch model. 
 37  """ 
 38   
 39  import errno 
 40  import os 
 41  import sys 
 42  from copy import deepcopy 
 43   
 44  import yaml 
 45   
 46  from roslaunch.core import Param, RosbinExecutable, RLException, PHASE_SETUP 
 47   
 48  from rosgraph.names import make_global_ns, ns_join, PRIV_NAME, load_mappings, is_legal_name, canonicalize_name 
 49   
 50  #lazy-import global for yaml and rosparam 
 51  rosparam = None 
 52   
53 -class LoadException(RLException):
54 """Error loading data as specified (e.g. cannot find included files, etc...)""" 55 pass
56 57 #TODO: lists, maps(?)
58 -def convert_value(value, type_):
59 """ 60 Convert a value from a string representation into the specified 61 type 62 63 @param value: string representation of value 64 @type value: str 65 @param type_: int, double, string, bool, or auto 66 @type type_: str 67 @raise ValueError: if parameters are invalid 68 """ 69 type_ = type_.lower() 70 # currently don't support XML-RPC date, dateTime, maps, or list 71 # types 72 if type_ == 'auto': 73 #attempt numeric conversion 74 try: 75 if '.' in value: 76 return float(value) 77 else: 78 return int(value) 79 except ValueError as e: 80 pass 81 #bool 82 lval = value.lower() 83 if lval == 'true' or lval == 'false': 84 return convert_value(value, 'bool') 85 #string 86 return value 87 elif type_ == 'str' or type_ == 'string': 88 return value 89 elif type_ == 'int': 90 return int(value) 91 elif type_ == 'double': 92 return float(value) 93 elif type_ == 'bool' or type_ == 'boolean': 94 value = value.lower().strip() 95 if value == 'true' or value == '1': 96 return True 97 elif value == 'false' or value == '0': 98 return False 99 raise ValueError("%s is not a '%s' type"%(value, type_)) 100 elif type_ == 'yaml': 101 try: 102 return yaml.safe_load(value) 103 except yaml.parser.ParserError as e: 104 raise ValueError(e) 105 else: 106 raise ValueError("Unknown type '%s'"%type_)
107
108 -def process_include_args(context):
109 """ 110 Processes arg declarations in context and makes sure that they are 111 properly declared for passing into an included file. Also will 112 correctly setup the context for passing to the included file. 113 """ 114 115 # make sure all arguments have values. arg_names and resolve_dict 116 # are cleared when the context was initially created. 117 arg_dict = context.include_resolve_dict.get('arg', {}) 118 for arg in context.arg_names: 119 if not arg in arg_dict: 120 raise LoadException("include args must have declared values") 121 122 # save args that were passed so we can check for unused args in post-processing 123 context.args_passed = list(arg_dict.keys()) 124 # clear arg declarations so included file can re-declare 125 context.arg_names = [] 126 127 # swap in new resolve dict for passing 128 context.resolve_dict = context.include_resolve_dict 129 context.include_resolve_dict = None
130
131 -def post_process_include_args(context):
132 bad = [a for a in context.args_passed if a not in context.arg_names] 133 if bad: 134 raise LoadException("unused args [%s] for include of [%s]"%(', '.join(bad), context.filename))
135
136 -def load_sysargs_into_context(context, argv):
137 """ 138 Load in ROS remapping arguments as arg assignments for context. 139 140 @param context: context to load into. context's resolve_dict for 'arg' will be reinitialized with values. 141 @type context: L{LoaderContext{ 142 @param argv: command-line arguments 143 @type argv: [str] 144 """ 145 # we use same command-line spec as ROS nodes 146 mappings = load_mappings(argv) 147 context.resolve_dict['arg'] = mappings
148
149 -class LoaderContext(object):
150 """ 151 Container for storing current loader context (e.g. namespace, 152 local parameter state, remapping state). 153 """ 154
155 - def __init__(self, ns, filename, parent=None, params=None, env_args=None, \ 156 resolve_dict=None, include_resolve_dict=None, arg_names=None):
157 """ 158 @param ns: namespace 159 @type ns: str 160 @param filename: name of file this is being loaded from 161 @type filename: str 162 @param resolve_dict: (optional) resolution dictionary for substitution args 163 @type resolve_dict: dict 164 @param include_resolve_dict: special resolution dictionary for 165 <include> tags. Must be None if this is not an <include> 166 context. 167 @type include_resolve_dict: dict 168 @param arg_names: name of args that have been declared in this context 169 @type arg_names: [str] 170 """ 171 self.parent = parent 172 self.ns = make_global_ns(ns or '/') 173 self._remap_args = [] 174 self.params = params or [] 175 self.env_args = env_args or [] 176 self.filename = filename 177 # for substitution args 178 self.resolve_dict = resolve_dict or {} 179 # arg names. Args that have been declared in this context 180 self.arg_names = arg_names or [] 181 # special scoped resolve dict for processing in <include> tag 182 self.include_resolve_dict = include_resolve_dict or None 183 # when this context was created via include, was pass_all_args set? 184 self.pass_all_args = False
185
186 - def add_param(self, p):
187 """ 188 Add a ~param to the context. ~params are evaluated by any node 189 declarations that occur later in the same context. 190 191 @param p: parameter 192 @type p: L{Param} 193 """ 194 195 # override any params already set 196 matches = [m for m in self.params if m.key == p.key] 197 for m in matches: 198 self.params.remove(m) 199 self.params.append(p)
200
201 - def add_remap(self, remap):
202 """ 203 Add a new remap setting to the context. if a remap already 204 exists with the same from key, it will be removed 205 206 @param remap: remap setting 207 @type remap: (str, str) 208 """ 209 remap = [canonicalize_name(x) for x in remap] 210 if not remap[0] or not remap[1]: 211 raise RLException("remap from/to attributes cannot be empty") 212 if not is_legal_name(remap[0]): 213 raise RLException("remap from [%s] is not a valid ROS name"%remap[0]) 214 if not is_legal_name(remap[1]): 215 raise RLException("remap to [%s] is not a valid ROS name"%remap[1]) 216 217 matches = [r for r in self._remap_args if r[0] == remap[0]] 218 for m in matches: 219 self._remap_args.remove(m) 220 self._remap_args.append(remap)
221
222 - def add_arg(self, name, default=None, value=None, doc=None):
223 """ 224 Add 'arg' to existing context. Args are only valid for their immediate context. 225 """ 226 if name in self.arg_names: 227 # Ignore the duplication if pass_all_args was set 228 if not self.pass_all_args: 229 raise LoadException("arg '%s' has already been declared"%name) 230 else: 231 self.arg_names.append(name) 232 233 resolve_dict = self.resolve_dict if self.include_resolve_dict is None else self.include_resolve_dict 234 235 if not 'arg' in resolve_dict: 236 resolve_dict['arg'] = {} 237 arg_dict = resolve_dict['arg'] 238 239 # args can only be declared once. they must also have one and 240 # only value at the time that they are declared. 241 if value is not None: 242 # value is set, error if declared in our arg dict as args 243 # with set values are constant/grounded. 244 # But don't error if pass_all_args was used to include this 245 # context; rather just override the passed-in value. 246 if name in arg_dict and not self.pass_all_args: 247 raise LoadException("cannot override arg '%s', which has already been set"%name) 248 arg_dict[name] = value 249 elif default is not None: 250 # assign value if not in context 251 if name not in arg_dict: 252 arg_dict[name] = default 253 else: 254 # no value or default: appending to arg_names is all we 255 # need to do as it declares the existence of the arg. 256 pass 257 258 # add arg documentation string dict if it doesn't exist yet and if it can be used 259 if not 'arg_doc' in resolve_dict: 260 resolve_dict['arg_doc'] = {} 261 arg_doc_dict = resolve_dict['arg_doc'] 262 263 if not value: 264 # store the documentation for this argument 265 arg_doc_dict[name] = (doc, default)
266 267
268 - def remap_args(self):
269 """ 270 @return: copy of the current remap arguments 271 @rtype: [(str, str)] 272 """ 273 if self.parent: 274 args = [] 275 # filter out any parent remap args that have the same from key 276 for pr in self.parent.remap_args(): 277 if not [r for r in self._remap_args if r[0] == pr[0]]: 278 args.append(pr) 279 args.extend(self._remap_args) 280 return args 281 return self._remap_args[:]
282
283 - def include_child(self, ns, filename):
284 """ 285 Create child namespace based on include inheritance rules 286 @param ns: sub-namespace of child context, or None if the 287 child context shares the same namespace 288 @type ns: str 289 @param filename: name of include file 290 @type filename: str 291 @return: A child xml context that inherits from this context 292 @rtype: L{LoaderContext}jj 293 """ 294 ctx = self.child(ns) 295 # arg declarations are reset across include boundaries 296 ctx.arg_names = [] 297 ctx.filename = filename 298 # keep the resolve_dict for now, we will do all new assignments into include_resolve_dict 299 ctx.include_resolve_dict = {} 300 #del ctx.resolve_dict['arg'] 301 return ctx
302
303 - def child(self, ns):
304 """ 305 @param ns: sub-namespace of child context, or None if the 306 child context shares the same namespace 307 @type ns: str 308 @return: A child xml context that inherits from this context 309 @rtype: L{LoaderContext} 310 """ 311 if ns: 312 if ns[0] == '/': # global (discouraged) 313 child_ns = ns 314 elif ns == PRIV_NAME: # ~name 315 # private names can only be scoped privately or globally 316 child_ns = PRIV_NAME 317 else: 318 child_ns = ns_join(self.ns, ns) 319 else: 320 child_ns = self.ns 321 return LoaderContext(child_ns, self.filename, parent=self, 322 params=self.params, env_args=self.env_args[:], 323 resolve_dict=deepcopy(self.resolve_dict), 324 arg_names=self.arg_names[:], include_resolve_dict=self.include_resolve_dict)
325 326 #TODO: in-progress refactorization. I'm slowly separating out 327 #non-XML-specific logic from xmlloader and moving into Loader. Soon 328 #this will mean that it will be easier to write coverage tests for 329 #lower-level logic. 330
331 -class Loader(object):
332 """ 333 Lower-level library for loading ROS launch model. It provides an 334 abstraction between the representation (e.g. XML) and the 335 validation of the property values. 336 """ 337
338 - def add_param(self, ros_config, param_name, param_value, verbose=True):
339 """ 340 Add L{Param} instances to launch config. Dictionary values are 341 unrolled into individual parameters. 342 343 @param ros_config: launch configuration 344 @type ros_config: L{ROSLaunchConfig} 345 @param param_name: name of parameter namespace to load values 346 into. If param_name is '/', param_value must be a dictionary 347 @type param_name: str 348 @param param_value: value to assign to param_name. If 349 param_value is a dictionary, it's values will be unrolled 350 into individual parameters. 351 @type param_value: str 352 @raise ValueError: if parameters cannot be processed into valid Params 353 """ 354 355 # shouldn't ever happen 356 if not param_name: 357 raise ValueError("no parameter name specified") 358 359 if param_name == '/' and type(param_value) != dict: 360 raise ValueError("Cannot load non-dictionary types into global namespace '/'") 361 362 if type(param_value) == dict: 363 # unroll params 364 for k, v in param_value.items(): 365 self.add_param(ros_config, ns_join(param_name, k), v, verbose=verbose) 366 else: 367 ros_config.add_param(Param(param_name, param_value), verbose=verbose)
368
369 - def load_rosparam(self, context, ros_config, cmd, param, file_, text, verbose=True, subst_function=None):
370 """ 371 Load rosparam setting 372 373 @param context: Loader context 374 @type context: L{LoaderContext} 375 @param ros_config: launch configuration 376 @type ros_config: L{ROSLaunchConfig} 377 @param cmd: 'load', 'dump', or 'delete' 378 @type cmd: str 379 @param file_: filename for rosparam to use or None 380 @type file_: str 381 @param text: text for rosparam to load. Ignored if file_ is set. 382 @type text: str 383 @raise ValueError: if parameters cannot be processed into valid rosparam setting 384 """ 385 if not cmd in ('load', 'dump', 'delete'): 386 raise ValueError("command must be 'load', 'dump', or 'delete'") 387 if file_ is not None: 388 if cmd == 'load' and not os.path.isfile(file_): 389 raise ValueError("file does not exist [%s]"%file_) 390 if cmd == 'delete': 391 raise ValueError("'file' attribute is invalid with 'delete' command.") 392 393 full_param = ns_join(context.ns, param) if param else context.ns 394 395 if cmd == 'dump': 396 ros_config.add_executable(RosbinExecutable('rosparam', (cmd, file_, full_param), PHASE_SETUP)) 397 elif cmd == 'delete': 398 ros_config.add_executable(RosbinExecutable('rosparam', (cmd, full_param), PHASE_SETUP)) 399 elif cmd == 'load': 400 # load YAML text 401 if file_: 402 with open(file_, 'r') as f: 403 text = f.read() 404 405 if subst_function is not None: 406 text = subst_function(text) 407 # parse YAML text 408 # - lazy import: we have to import rosparam in oder to to configure the YAML constructors 409 global rosparam 410 if rosparam is None: 411 import rosparam 412 try: 413 data = yaml.safe_load(text) 414 # #3162: if there is no YAML, load() will return an 415 # empty string. We want an empty dictionary instead 416 # for our representation of empty. 417 if data is None: 418 data = {} 419 except yaml.MarkedYAMLError as e: 420 if not file_: 421 raise ValueError("Error within YAML block:\n\t%s\n\nYAML is:\n%s"%(str(e), text)) 422 else: 423 raise ValueError("file %s contains invalid YAML:\n%s"%(file_, str(e))) 424 except Exception as e: 425 if not file_: 426 raise ValueError("invalid YAML: %s\n\nYAML is:\n%s"%(str(e), text)) 427 else: 428 raise ValueError("file %s contains invalid YAML:\n%s"%(file_, str(e))) 429 430 # 'param' attribute is required for non-dictionary types 431 if not param and type(data) != dict: 432 raise ValueError("'param' attribute must be set for non-dictionary values") 433 434 self.add_param(ros_config, full_param, data, verbose=verbose) 435 436 else: 437 raise ValueError("unknown command %s"%cmd)
438 439
440 - def load_env(self, context, ros_config, name, value):
441 """ 442 Load environment variable setting 443 444 @param context: Loader context 445 @type context: L{LoaderContext} 446 @param ros_config: launch configuration 447 @type ros_config: L{ROSLaunchConfig} 448 @param name: environment variable name 449 @type name: str 450 @param value: environment variable value 451 @type value: str 452 """ 453 if not name: 454 raise ValueError("'name' attribute must be non-empty") 455 context.env_args.append((name, value))
456 457
458 - def param_value(self, verbose, name, ptype, value, textfile, binfile, command):
459 """ 460 Parse text representation of param spec into Python value 461 @param name: param name, for error message use only 462 @type name: str 463 @param verbose: print verbose output 464 @type verbose: bool 465 @param textfile: name of text file to load from, or None 466 @type textfile: str 467 @param binfile: name of binary file to load from, or None 468 @type binfile: str 469 @param command: command to execute for parameter value, or None 470 @type command: str 471 @raise ValueError: if parameters are invalid 472 """ 473 if value is not None: 474 return convert_value(value.strip(), ptype) 475 elif textfile is not None: 476 with open(textfile, 'r') as f: 477 return convert_value(f.read(), ptype) 478 elif binfile is not None: 479 try: 480 from xmlrpc.client import Binary 481 except ImportError: 482 from xmlrpclib import Binary 483 with open(binfile, 'rb') as f: 484 return Binary(f.read()) 485 elif command is not None: 486 try: 487 if type(command) == unicode: 488 command = command.encode('utf-8') #attempt to force to string for shlex/subprocess 489 except NameError: 490 pass 491 if verbose: 492 print("... executing command param [%s]" % command) 493 import subprocess, shlex #shlex rocks 494 try: 495 if os.name != 'nt': 496 command = shlex.split(command) 497 else: 498 cl = shlex.split(command, posix=False) # use non-posix method on Windows 499 500 # On Linux, single quotes are commonly used to enclose a path to escape spaces. 501 # However, on Windows, the single quotes are treated as part of the arguments. 502 # Special handling is required to remove the extra single quotes. 503 if "'" in command: 504 cl = [token[1:-1] if token.startswith("'") and token.endswith("'") else token for token in cl] 505 command = cl 506 507 # Python scripts in ROS tend to omit .py extension since they could become executable with shebang line 508 # special handle the use of Python scripts in Windows environment: 509 # 1. search for a wrapper executable (of the same name) under the same directory with stat.S_IXUSR flag 510 # 2. if no wrapper is present, prepend command with 'python' executable 511 if os.path.isabs(cl[0]): 512 # trying to launch an executable from a specific location(package), e.g. xacro 513 import stat 514 rx_flag = stat.S_IRUSR | stat.S_IXUSR 515 if not os.path.exists(cl[0]) or os.stat(cl[0]).st_mode & rx_flag != rx_flag: 516 d = os.path.dirname(cl[0]) 517 files_of_same_name = [ 518 os.path.join(d, f) for f in os.listdir(d) 519 if os.path.splitext(f)[0].lower() == os.path.splitext(os.path.basename(cl[0]))[0].lower() 520 ] if os.path.exists(d) else [] 521 executable_command = None 522 for f in files_of_same_name: 523 if os.stat(f).st_mode & rx_flag == rx_flag: 524 # found an executable wrapper of the desired Python script 525 executable_command = f 526 527 if not executable_command: 528 for f in files_of_same_name: 529 mode = os.stat(f).st_mode 530 if (mode & stat.S_IRUSR == stat.S_IRUSR) and (mode & stat.S_IXUSR != stat.S_IXUSR): 531 # when there is read permission but not execute permission, this is typically a Python script (in ROS) 532 if os.path.splitext(f)[1].lower() in ['.py', '']: 533 executable_command = ' '.join([sys.executable, f]) 534 if executable_command: 535 command[0] = executable_command 536 p = subprocess.Popen(command, stdout=subprocess.PIPE) 537 c_value = p.communicate()[0] 538 if not isinstance(c_value, str): 539 c_value = c_value.decode('utf-8') 540 if p.returncode != 0: 541 raise ValueError("Cannot load command parameter [%s]: command [%s] returned with code [%s]"%(name, command, p.returncode)) 542 except OSError as e: 543 if e.errno == errno.ENOENT: 544 raise ValueError("Cannot load command parameter [%s]: no such command [%s]"%(name, command)) 545 raise 546 if c_value is None: 547 raise ValueError("parameter: unable to get output of command [%s]"%command) 548 return convert_value(c_value, ptype) 549 else: #_param_tag prevalidates, so this should not be reachable 550 raise ValueError("unable to determine parameter value")
551