Package mrv :: Package cmd :: Module base
[hide private]
[frames] | no frames]

Source Code for Module mrv.cmd.base

  1  # -*- coding: utf-8 -*- 
  2  """Contains routines required to initialize mrv""" 
  3  import os 
  4  import sys 
  5  import subprocess 
  6  from mrv.path import make_path, BasePath 
  7  import logging 
  8  import optparse 
  9   
 10  log = logging.getLogger("mrv.cmd.base") 
 11   
 12  __docformat__ = "restructuredtext" 
 13   
 14  __all__ = [ 'is_supported_maya_version', 'python_version_of', 'parse_maya_version', 'update_env_path',  
 15                          'maya_location', 'update_maya_environment', 'exec_python_interpreter', 'uses_mayapy',  
 16                          'exec_maya_binary', 'available_maya_versions', 'python_executable', 'find_mrv_script', 
 17                          'log_exception', 'SpawnedHelpFormatter', 'SpawnedOptionParser', 'SpawnedCommand'] 
 18   
 19  #{ Globals 
 20  maya_to_py_version_map = { 
 21          8.5 : 2.4,  
 22          2008: 2.5,  
 23          2009: 2.5,  
 24          2010: 2.6, 
 25          2011: 2.6, 
 26          2012: 2.6 
 27  } 
28 29 #} END globals 30 31 32 #{ Maya-Intiialization 33 34 -def is_supported_maya_version(version):
35 """:return: True if version is a supported maya version 36 :param version: float which is either 8.5 or 2008 to 20XX""" 37 if version == 8.5: 38 return True 39 40 return str(version)[:2] == "20"
41
42 -def uses_mayapy():
43 """:return: True if the executable is mayapy""" 44 try: 45 mayapy_maya_version() 46 return True 47 except EnvironmentError: 48 return False
49 # END handle exceptions
50 51 -def mayapy_maya_version():
52 """:return: float representing the maya version of the currently running 53 mayapy interpreter. 54 :raise EnvironmentError: If called from a 'normal' python interpreter""" 55 if 'maya' not in sys.executable.lower(): 56 raise EnvironmentError("Not running mayapy") 57 # END quick first check 58 59 exec_path = make_path(os.path.realpath(sys.executable)) # Maya is capitalized on windows 60 try: 61 version_token = [ t[4:] for t in exec_path.splitall() if t.lower().startswith('maya') ][0] 62 except IndexError: 63 raise EnvironmentError("Not running mayapy or invalid path mayapy path: %s" % exec_path) 64 # END handle errors 65 66 if version_token.endswith('-x64'): 67 version_token = version_token[:-4] 68 # END handle 64 bit paths 69 70 return float(version_token)
71
72 -def parse_maya_version(arg, default):
73 """:return: tuple(bool, version) tuple of bool indicating whether the version could 74 be parsed and was valid, and a float representing the parsed or default version. 75 :param default: The desired default maya version""" 76 parsed_arg = False 77 version = default 78 try: 79 candidate = float(arg) 80 if is_supported_maya_version(candidate): 81 parsed_arg, version = True, candidate 82 else: 83 pass 84 # in that case, we don't claim the arg and just use the default 85 # END handle candidate 86 except ValueError: 87 pass 88 # END exception handling 89 90 return parsed_arg, version
91
92 -def python_version_of(maya_version):
93 """:return: python version matching the given maya version 94 :raise EnvironmentError: If there is no known matching python version""" 95 try: 96 return maya_to_py_version_map[maya_version] 97 except KeyError: 98 raise EnvironmentError("Do not know python version matching the given maya version %g" % maya_version)
99
100 -def update_env_path(environment, env_var, value, append=False):
101 """Set the given env_var to the given value, but append the existing value 102 to it using the system path separator 103 104 :param append: if True, value will be appended to existing values, otherwise it will 105 be prepended""" 106 curval = environment.get(env_var, None) 107 # rule out empty strings 108 if curval: 109 if append: 110 value = curval + os.pathsep + value 111 else: 112 value = value + os.pathsep + curval 113 # END handle append 114 # END handle existing value 115 environment[env_var] = value
116
117 -def available_maya_versions():
118 """:return: list of installed maya versions which are locally available - 119 they can be used in methods that require the maya_version to be given. 120 Versions are ordered such that the latest version is given last.""" 121 versions = list() 122 for version_candidate in sorted(maya_to_py_version_map.keys()): 123 try: 124 loc = maya_location(version_candidate) 125 versions.append(version_candidate) 126 except Exception: 127 pass 128 # END check maya location 129 # END for each version 130 return versions
131
132 -def maya_location(maya_version):
133 """:return: string path to the existing maya installation directory for the 134 given maya version 135 :raise EnvironmentError: if it was not found""" 136 mayaroot = None 137 suffix = '' 138 139 if sys.platform.startswith('linux'): 140 mayaroot = "/usr/autodesk/maya" 141 if os.path.isdir('/lib64'): 142 suffix = "-x64" 143 # END handle 64 bit systems 144 elif sys.platform == 'darwin': 145 mayaroot = "/Applications/Autodesk/maya" 146 elif sys.platform.startswith('win'): 147 # try to find it in all kinds of program files, prefer 64 bit versions 148 tried_paths = list() 149 for envvar in ('PROGRAMW6432', 'PROGRAMFILES','PROGRAMFILES(X86)'): 150 if envvar not in os.environ: 151 continue 152 basepath = make_path(os.environ[envvar]) / "Autodesk" 153 if basepath.isdir(): 154 mayaroot = basepath / 'Maya' 155 break 156 # END if we have found Autodesk installations 157 tried_paths.append(basepath) 158 # END for each envvar 159 if mayaroot is None: 160 raise EnvironmentError("Could not find any maya installation, searched %s" % (', '.join(tried_paths))) 161 # END os specific adjustments 162 163 if mayaroot is None: 164 raise EnvironmentError("Current platform %r is unsupported" % sys.platform) 165 # END assure existance of maya root 166 167 mayalocation = "%s%g%s" % (mayaroot, maya_version, suffix) 168 169 # OSX special handling 170 if sys.platform == 'darwin': 171 mayalocation=os.path.join(mayalocation, 'Maya.app', 'Contents') 172 173 if not os.path.isdir(mayalocation): 174 raise EnvironmentError("Could not find maya installation at %r" % mayalocation) 175 # END verfy maya location 176 177 return mayalocation
178
179 -def update_maya_environment(maya_version):
180 """Configure os.environ to allow Maya to run in standalone mode 181 :param maya_version: The maya version to prepare to run, either 8.5 or 2008 to 182 20XX. This requires the respective maya version to be installed in a default location. 183 :raise EnvironmentError: If the platform is unsupported or if the maya installation could not be found""" 184 py_version = python_version_of(maya_version) 185 186 pylibdir = None 187 envppath = "PYTHONPATH" 188 189 if sys.platform.startswith('linux'): 190 pylibdir = "lib" 191 elif sys.platform == 'darwin': 192 pylibdir = "Frameworks/Python.framework/Versions/Current/lib" 193 elif sys.platform.startswith('win'): 194 pylibdir = "Python" 195 # END os specific adjustments 196 197 198 # GET MAYA LOCATION 199 ################### 200 mayalocation = maya_location(maya_version) 201 202 if not os.path.isdir(mayalocation): 203 raise EnvironmentError("Could not find maya installation at %r" % mayalocation) 204 # END verfy maya location 205 206 207 env = os.environ 208 209 # ADJUST LD_LIBRARY_PATH or PATH 210 ################################ 211 # Note: if you need something like LD_PRELOAD or equivalent, add the respective 212 # variables to the environment of this process before starting it 213 if sys.platform.startswith('linux'): 214 envld = "LD_LIBRARY_PATH" 215 ldpath = os.path.join(mayalocation, 'lib') 216 update_env_path(env, envld, ldpath) 217 elif sys.platform == 'darwin': 218 # adjust maya location to point to the actual directtoy 219 dldpath = os.path.join(mayalocation, 'MacOS') 220 update_env_path(env, "DYLD_LIBRARY_PATH", dldpath) 221 222 dldframeworkpath = os.path.join(mayalocation, 'Frameworks') 223 update_env_path(env, "DYLD_FRAMEWORK_PATH", dldframeworkpath) 224 225 env['MAYA_NO_BUNDLE_RESOURCES'] = "1" 226 227 # on osx, python will only use the main frameworks path and ignore 228 # its own sitelibraries. We put them onto the PYTHONPATH for that reason 229 # MayaRV will take care of the initialization 230 ppath = "/Library/Python/%s/site-packages" % py_version 231 update_env_path(env, envppath, ppath, append=True) 232 233 elif sys.platform.startswith('win'): 234 mayadll = os.path.join(mayalocation, 'bin') 235 mayapydll = os.path.join(mayalocation, 'Python', 'DLLs') 236 update_env_path(env, 'PATH', mayadll+os.pathsep+mayapydll, append=False) 237 else: 238 raise EnvironmentError("Current platform %s is unsupported" % sys.platform) 239 # END handle os's 240 241 242 # ADJUST PYTHON PATH 243 #################### 244 # root project is already in the path, we add additional paths as well 245 ospd = os.path.dirname 246 if not sys.platform.startswith('win'): 247 ppath = os.path.join(mayalocation, pylibdir, "python%s"%py_version, "site-packages") 248 else: 249 ppath = os.path.join(mayalocation, pylibdir, "lib", "site-packages") 250 # END windows special handling 251 252 # don't prepend, otherwise system-interpreter mrv versions will not be able 253 # to override the possibly existing mrv version installed in maya. 254 update_env_path(env, envppath, ppath, append=True) 255 256 # SET MAYA LOCATION 257 ################### 258 # its important to do it here as osx adjusts it 259 env['MAYA_LOCATION'] = mayalocation 260 261 # export the actual maya version to allow scripts to pick it up even before maya is launched 262 env['MRV_MAYA_VERSION'] = "%g" % maya_version
263
264 -def mangle_args(args):
265 """Enclose arguments in quotes if they contain spaces ... on windows only 266 :return: tuple of possibly modified arguments 267 268 :todo: remove this function, its unused""" 269 if not sys.platform.startswith('win'): 270 return args 271 272 newargs = list() 273 for arg in args: 274 if ' ' in arg: 275 arg = '"%s"' % arg 276 # END put quotes around strings with spaces 277 newargs.append(arg) 278 # END for each arg 279 return tuple(newargs)
280
281 -def mangle_executable(executable):
282 """:return: possibly adjusted path to executable in order to allow its execution 283 This currently only kicks in on windows as we can't handle spaces properly. 284 285 :note: Will change working dir 286 :todo: remove this function, its unused""" 287 if not sys.platform.startswith('win'): 288 return executable 289 290 # execv appears to call the shell, hence we make sure we handle whitespaecs 291 # in the path, which usually happens on windows ! 292 # Problem here is that it cannot find the executable if it has a space in the 293 # path as it will split it, and if quotes are put around, it can't find 294 # it either. Hence we chdir into it and use a relative path 295 if ' ' in executable: 296 os.chdir(os.path.dirname(executable)) 297 executable = os.path.basename(executable) 298 # END handle freakin' spaces 299 return executable
300
301 -def init_environment(args):
302 """Intialize MRV up to the point where we can replace this process with the 303 one we prepared 304 305 :param args: commandline arguments excluding the executable ( usually first arg ) 306 :return: tuple(use_this_interpreter, maya_version, args) tuple of Bool, maya_version, and the remaining args 307 The boolean indicates whether we have to reuse this interpreter, as it is mayapy""" 308 # see if first argument is the maya version 309 maya_version=None 310 if args: 311 parsed_successfully, maya_version = parse_maya_version(args[0], default=None) 312 if parsed_successfully: 313 # if the user wants a specific maya version, he should get it no matter what 314 args = args[1:] 315 # END cut version arg 316 # END if there are args at all 317 318 # choose the newest available maya version if none was specified 319 if maya_version is None: 320 # If there is no version given, and we are in mayapy, we use the maypy 321 # version. Otherwise we use the newest available version 322 if uses_mayapy(): 323 maya_version = mayapy_maya_version() 324 # in that case, we have a valid maya environment already. This also means 325 # that we must use this interpreter ! 326 return (True, maya_version, tuple(args)) 327 else: 328 versions = available_maya_versions() 329 if versions: 330 maya_version = versions[-1] 331 log.info("Using newest available maya version: %g" % maya_version) 332 # END set latest 333 # END handle maya version 334 # END set maya version 335 336 # The user has specified a maya version to start. As mayapy sets up everything in 337 # a distinctive way, we are kind of too late to alter that. 338 # Hence we must prevent the user from starting different maya versions than 339 # the one defined by mayapy. 340 # NOTE: If we use mayapy, the things mentioned above are an issue. If we 341 # use the delivered python-bin (linux) or Python (osx) executables, this 342 # is not an issue. This way, its consistent among all platforms though as 343 # windows always uses mayapy. 344 if uses_mayapy(): 345 mayapy_version = mayapy_maya_version() 346 if mayapy_version != maya_version: 347 raise EnvironmentError("If using mayapy, you cannot run any other maya version than the one mayapy uses: %g" % mayapy_version) 348 # END assert version 349 350 if maya_version is None: 351 raise EnvironmentError("Maya version not specified on the commandline, couldn't find any maya version on this system") 352 # END abort if not installed 353 354 update_maya_environment(maya_version) 355 return (False, maya_version, tuple(args))
356
357 -def _execute(executable, args):
358 """Perform the actual execution of the executable with the given args. 359 This method does whatever is required to get it right on windows, which is 360 the only reason this method exists ! 361 362 :param args: arguments, without the executable as first argument 363 :note: does not return """ 364 # on windows we spawn, otherwise we don't get the interactive input right 365 actual_args = (executable, ) + args 366 if sys.platform.startswith('win'): 367 p = subprocess.Popen(actual_args, stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr) 368 sys.exit(p.wait()) 369 else: 370 os.execvp(executable, actual_args)
371 # END handle windows
372 373 -def python_executable(py_version=None):
374 """:return: name or path to python executable in this system, deals with 375 linux and windows specials""" 376 if py_version is None: 377 return 'python' 378 # END handle simple case 379 380 py_executable = "python%g" % py_version 381 if sys.platform.startswith('win'): 382 # so, on windows the executables don't have a . in their name, most likely 383 # because windows threats the '.' in a special way as ... anyway. 384 py_executable = "python%g" % (py_version*10) 385 # END win specials 386 return py_executable
387
388 -def find_mrv_script(name):
389 """Find an mrv script of the given name. This method should be used if you 390 want to figure out where the mrv executable with the given name is located. 391 The returned path is either relative or absolute. 392 393 :return: Path to script 394 :raise EnvironmentError: if the executable could not be found 395 :note: Currently it only looks for executables, but handles projects 396 which use mrv as a subproject""" 397 import mrv 398 mrvroot = os.path.dirname(mrv.__file__) 399 400 tried_paths = list() 401 for base in ('', 'ext', mrvroot): 402 for subdir in ('bin', 'doc', os.path.join('test', 'bin')): 403 path = None 404 if base: 405 path = os.path.join(base, subdir, name) 406 else: 407 path = os.path.join(subdir, name) 408 # END handle base 409 if os.path.isfile(path): 410 return make_path(path) 411 tried_paths.append(path) 412 # END for each subdir 413 # END for each base 414 415 raise EnvironmentError("Script named %s not found, looked at %s" % (name, ', '.join(tried_paths)))
416
417 -def exec_python_interpreter(args, maya_version, mayapy_only=False):
418 """Replace this process with a python process as determined by the given options. 419 This will either be the respective python interpreter, or mayapy. 420 If it works, the function does not return 421 422 :param args: remaining arguments which should be passed to the process 423 :param maya_version: float indicating the maya version to use 424 :param mayapy_only: If True, only mayapy will be considered for startup. 425 Use this option in case the python interpreter crashes for some reason. 426 :raise EnvironmentError: If no suitable executable could be started""" 427 py_version = python_version_of(maya_version) 428 py_executable = python_executable(py_version) 429 430 args = tuple(args) 431 tried_paths = list() 432 try: 433 if mayapy_only: 434 raise OSError() 435 tried_paths.append(py_executable) 436 _execute(py_executable, args) 437 except OSError: 438 if not mayapy_only: 439 print "Python interpreter named %r not found, trying mayapy ..." % py_executable 440 # END print error message 441 mayalocation = maya_location(maya_version) 442 mayapy_executable = os.path.join(mayalocation, "bin", "mayapy") 443 444 try: 445 tried_paths.append(mayapy_executable) 446 _execute(mayapy_executable, args) 447 except OSError, e: 448 raise EnvironmentError("Could not find suitable python interpreter at paths %s : %s" % (', '.join(tried_paths), e))
449 # END final exception handling
450 # END exception handling 451 452 -def exec_maya_binary(args, maya_version):
453 """Replace this process with the maya executable as specified by maya_version. 454 455 :param args: The arguments to be provided to maya 456 :param maya_version: Float identifying the maya version to be launched 457 :rase EnvironmentError: if the respective maya version could not be found""" 458 mayalocation = maya_location(maya_version) 459 mayabin = os.path.join(mayalocation, 'bin', 'maya') 460 461 # although execv would work on windows, we use our specialized _execute method 462 # in order to keep things consistent 463 _execute(mayabin, tuple(args))
464
465 466 #} END Maya initialization 467 468 #{ Decorators 469 470 -def log_exception( func ):
471 """Assures that exceptions result in a logging message. 472 Currently only works with a SpawnedCommand as we need a log instance. 473 On error, the server exits with status 64""" 474 def wrapper(self, *args, **kwargs): 475 try: 476 return func(self, *args, **kwargs) 477 except Exception, e: 478 if self.parser.spawned: 479 self.log.critical("Program %r aborted with a unhandled exception: %s" % (self.k_log_application_id, str(e)), exc_info=True) 480 sys.exit(64) 481 else: 482 raise
483 # END handle sysexit gracefully 484 # END wrapper 485 486 wrapper.__name__ = func.__name__ 487 return wrapper 488
489 #} END decorators 490 491 #{ Classes 492 493 -class SpawnedHelpFormatter(optparse.TitledHelpFormatter):
494 """Formatter assuring our help looks good""" 495
496 - def _format_text(self, text):
497 """Don't wrap the text at all""" 498 if self.parser: 499 text = self.parser.expand_prog_name(text) 500 # END program name expansion if possible 501 502 if self.level == 0: 503 return text 504 lines = text.splitlines(True) 505 return ''.join(' '*self.level + l for l in lines)
506
507 - def format_usage(self, usage):
508 return "usage: %s\n" % usage
509
510 511 -class SpawnedOptionParser(optparse.OptionParser):
512 """Customized version to ease use of SpawnedCommand 513 514 Initialized with the 'spawned' keyword in addition 515 to the default keywords to prevent a system exit""" 516
517 - def __init__(self, *args, **kwargs):
518 self.spawned = kwargs.pop('spawned', False) 519 kwargs['formatter'] = SpawnedHelpFormatter() 520 optparse.OptionParser.__init__(self, *args, **kwargs)
521
522 - def exit(self, status=0, msg=None):
523 if msg: 524 sys.stderr.write(msg) 525 526 if self.spawned: 527 sys.exit(status) 528 else: 529 # reraise if possible as we have not been spawned 530 exc_type, value, traceback = sys.exc_info() 531 if value: 532 raise 533 else: 534 raise optparse.OptParseError(msg)
535 # END options
536 537 538 539 -class SpawnedCommand(object):
540 """Implements a command which can be started easily by specifying a class path 541 such as package.cmd.module.CommandClass whose instance should be started in 542 a standalone process. 543 544 The target command must be derived from this class and must implement the 545 'execute' method. 546 547 To use this class, derive from it and change the configuration variables 548 accordingly. 549 550 The instance will always own a logger instance at its member called 'log', 551 the configuration will be applied according to k_log_application_id 552 553 The parser used to parse all options is vailable at its member called 'parser', 554 its set during ``option_parser`` 555 556 The instance may also be created within an existing process and 557 executed manually - in that case it will not exit automatically if a 558 serious event occours""" 559 560 #{ Configuration 561 # If not None, the name will be available for printing help text, and other tasks 562 # such as application specific initialization of modules 563 k_log_application_id = None 564 565 # path at which your class is located. It must be derived from SpawnedCommand 566 k_class_path = "package.module.YourClass" 567 568 # An identifier for the version of your command 569 k_version = None 570 571 # Path to the executable 572 _exec_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'bin', 'mrv') 573 574 # If set, its the directory containing your project's info file. It must be set from the 575 # derived class, as it knows its actual position 576 _mrv_info_dir = None 577 578 # additional arguments to pass on to the newly created process 579 _add_args = ['--mrv-no-maya'] 580 581 # The usage of your command in BMF 582 # i.e. %prog [options] 583 k_usage = None 584 585 # a short description of your command printed below its usage 586 k_description = None 587 588 # the name of your program's actual executable 589 k_program_name = None 590 591 # File mode creation mask of the daemon. 592 # If None, current one will not be changed 593 _daemon_umask = None 594 595 # Default working directory for the daemon. 596 # If None, the current one will not be changed 597 _daemon_workdir = None 598 599 # Default maximum for the number of available file descriptors that we try to close 600 _daemon_maxfd = 64 601 602 # The standard I/O file descriptors are redirected to /dev/null by default. 603 if (hasattr(os, "devnull")): 604 _daemon_redirect_to = os.devnull 605 else: 606 _daemon_redirect_to = "/dev/null" 607 608 #} END configuration 609 610 __slots__ = ('parser', 'log') 611 612
613 - def __init__(self, *args, **kwargs):
614 """ 615 :param _spawned: If True, default False, we assume we have our own process. 616 Otherwise we will do nothing that would adjust the current process, such as: 617 618 * sys.exit 619 * change configuration of logging system""" 620 try: 621 super(SpawnedCommand, self).__init__(*args, **kwargs) 622 except TypeError: 623 # in python 2.6 and newer, object will not accept to be called with 624 # arguments anymore. Problem is that we don't know whether super is an 625 # object or some mixed in type, which is a design flaw, and which breaks 626 # code as well. 627 pass 628 # END py 2.6 special case 629 630 631 spawned = kwargs.get('_spawned', False) 632 self.parser = SpawnedOptionParser( usage=self.k_usage, version=self.k_version, 633 description=self.k_description, add_help_option=True, 634 prog=self.k_program_name, spawned=spawned) 635 self.log = logging.getLogger(self.k_program_name)
636 637 @classmethod
638 - def spawn(cls, *args, **kwargs):
639 """Spawn a new standalone process of this command type 640 Additional arguments passed to the command process 641 642 :param kwargs: Additional keyword arguments to be passed to Subprocess.Popen, 643 use it to configure your IO 644 645 Returns: Subprocess.Popen instance""" 646 import spcmd 647 margs = [cls._exec_path, spcmd.__file__, cls.k_class_path] 648 margs.extend(args) 649 margs.extend(cls._add_args) 650 651 if os.name == 'nt': 652 margs.insert(0, sys.executable) 653 # END handle windows inabilitiess 654 655 old_mrvinfo_val = None 656 env_mrv_info = 'MRV_INFO_DIR' 657 if cls._mrv_info_dir is not None: 658 old_mrvinfo_val = os.environ.get(env_mrv_info) 659 os.environ[env_mrv_info] = cls._mrv_info_dir 660 #END handle mrv_info 661 662 try: 663 return subprocess.Popen(margs, **kwargs) 664 finally: 665 if old_mrvinfo_val is not None: 666 os.environ[env_mrv_info] = old_mrvinfo_val
667 #END reset prevous value 668 #END handle environment change 669 670 @classmethod
671 - def daemonize(cls, *args):
672 """ 673 Damonize the spawned command, passing *args to the instanciated command's 674 execute method. 675 676 :return: None in calling process, no return in the daemon 677 as sys.exit will be called. 678 :note: see configuration variables prefixed with _daemon_ 679 :note: based on Chad J. Schroeder createDaemon method, 680 see http://code.activestate.com/recipes/278731-creating-a-daemon-the-python-way 681 """ 682 if sys.platform.startswith("win"): 683 raise OSError("Cannot daemonize on windows") 684 # END handle operating system 685 686 try: 687 # Fork a child process so the parent can exit. This returns control to 688 # the command-line or shell. It also guarantees that the child will not 689 # be a process group leader, since the child receives a new process ID 690 # and inherits the parent's process group ID. This step is required 691 # to insure that the next call to os.setsid is successful. 692 pid = os.fork() 693 except OSError, e: 694 raise Exception, "%s [%d]" % (e.strerror, e.errno) 695 696 if (pid != 0): 697 # exit() or _exit()? 698 # _exit is like exit(), but it doesn't call any functions registered 699 # with atexit (and on_exit) or any registered signal handlers. It also 700 # closes any open file descriptors. Using exit() may cause all stdio 701 # streams to be flushed twice and any temporary files may be unexpectedly 702 # removed. It's therefore recommended that child branches of a fork() 703 # and the parent branch(es) of a daemon use _exit(). 704 return None 705 # END exit 706 707 ################## 708 # The first child. 709 ################## 710 # To become the session leader of this new session and the process group 711 # leader of the new process group, we call os.setsid(). The process is 712 # also guaranteed not to have a controlling terminal. 713 os.setsid() 714 715 # Is ignoring SIGHUP necessary? 716 # 717 # It's often suggested that the SIGHUP signal should be ignored before 718 # the second fork to avoid premature termination of the process. The 719 # reason is that when the first child terminates, all processes, e.g. 720 # the second child, in the orphaned group will be sent a SIGHUP. 721 # 722 # "However, as part of the session management system, there are exactly 723 # two cases where SIGHUP is sent on the death of a process: 724 # 725 # 1) When the process that dies is the session leader of a session that 726 # is attached to a terminal device, SIGHUP is sent to all processes 727 # in the foreground process group of that terminal device. 728 # 2) When the death of a process causes a process group to become 729 # orphaned, and one or more processes in the orphaned group are 730 # stopped, then SIGHUP and SIGCONT are sent to all members of the 731 # orphaned group." [2] 732 # 733 # The first case can be ignored since the child is guaranteed not to have 734 # a controlling terminal. The second case isn't so easy to dismiss. 735 # The process group is orphaned when the first child terminates and 736 # POSIX.1 requires that every STOPPED process in an orphaned process 737 # group be sent a SIGHUP signal followed by a SIGCONT signal. Since the 738 # second child is not STOPPED though, we can safely forego ignoring the 739 # SIGHUP signal. In any case, there are no ill-effects if it is ignored. 740 # 741 # import signal # Set handlers for asynchronous events. 742 # signal.signal(signal.SIGHUP, signal.SIG_IGN) 743 744 try: 745 # Fork a second child and exit immediately to prevent zombies. This 746 # causes the second child process to be orphaned, making the init 747 # process responsible for its cleanup. And, since the first child is 748 # a session leader without a controlling terminal, it's possible for 749 # it to acquire one by opening a terminal in the future (System V- 750 # based systems). This second fork guarantees that the child is no 751 # longer a session leader, preventing the daemon from ever acquiring 752 # a controlling terminal. 753 pid = os.fork() # Fork a second child. 754 except OSError, e: 755 raise Exception, "%s [%d]" % (e.strerror, e.errno) 756 757 if (pid != 0): 758 # exit() or _exit()? See below. 759 os._exit(0) # Exit parent (the first child) of the second child. 760 # END exit second child 761 762 ################### 763 # The second child. 764 ################### 765 # Since the current working directory may be a mounted filesystem, we 766 # avoid the issue of not being able to unmount the filesystem at 767 # shutdown time by changing it to the root directory. 768 if cls._daemon_workdir is not None: 769 os.chdir(cls._daemon_workdir) 770 # END set working dir 771 772 # We probably don't want the file mode creation mask inherited from 773 # the parent, so we give the child complete control over permissions. 774 if cls._daemon_umask is not None: 775 os.umask(cls._daemon_umask) 776 # END set umask 777 778 779 780 # Close all open file descriptors. This prevents the child from keeping 781 # open any file descriptors inherited from the parent. There is a variety 782 # of methods to accomplish this task. Three are listed below. 783 # 784 # Try the system configuration variable, SC_OPEN_MAX, to obtain the maximum 785 # number of open file descriptors to close. If it doesn't exists, use 786 # the default value (configurable). 787 # 788 # try: 789 # maxfd = os.sysconf("SC_OPEN_MAX") 790 # except (AttributeError, ValueError): 791 # maxfd = MAXFD 792 # 793 # OR 794 # 795 # if (os.sysconf_names.has_key("SC_OPEN_MAX")): 796 # maxfd = os.sysconf("SC_OPEN_MAX") 797 # else: 798 # maxfd = MAXFD 799 # 800 # OR 801 # 802 # Use the getrlimit method to retrieve the maximum file descriptor number 803 # that can be opened by this process. If there is not limit on the 804 # resource, use the default value. 805 # 806 import resource # Resource usage information. 807 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1] 808 if (maxfd == resource.RLIM_INFINITY): 809 maxfd = cls._daemon_maxfd 810 811 debug_daemon = False 812 813 # Iterate through and close all file descriptors. 814 for fd in range(debug_daemon*3, maxfd): 815 try: 816 os.close(fd) 817 except OSError: # ERROR, fd wasn't open to begin with (ignored) 818 pass 819 # END for each fd in range 820 821 # Redirect the standard I/O file descriptors to the specified file. Since 822 # the daemon has no controlling terminal, most daemons redirect stdin, 823 # stdout, and stderr to /dev/null. This is done to prevent side-effects 824 # from reads and writes to the standard I/O file descriptors. 825 826 # This call to open is guaranteed to return the lowest file descriptor, 827 # which will be 0 (stdin), since it was closed above. 828 if not debug_daemon: 829 os.open(cls._daemon_redirect_to, os.O_RDWR) # standard input (0) 830 831 # Duplicate standard input to standard output and standard error. 832 os.dup2(0, 1) # standard output (1) 833 os.dup2(0, 2) # standard error (2) 834 # END handle standard descriptors 835 836 # RUN THE SPAWNED COMMAND 837 ######################### 838 cmdinstance = cls(_spawned=True) 839 return cmdinstance._execute(*args)
840
841 - def _preprocess_args(self, options, args):
842 """:return: tuple(options, args) tuple of parsed options and remaining args 843 The arguments can be preprocessed""" 844 return options, args
845
846 - def _execute(self, *args):
847 """internal method handling the basic arguments in a pre-process before 848 calling ``execute`` 849 850 We will parse all options, process the default ones and pass on the 851 call to the ``execute`` method""" 852 options, args = self._preprocess_args(*self.option_parser().parse_args(list(args))) 853 854 # make sure we have a default setup at least ! 855 logging.basicConfig() 856 857 # call the subclassed method 858 try: 859 return self.execute(options, args) 860 except Exception, e: 861 if self.parser.spawned: 862 sys.stderr.write('%s: %s\n' % (type(e).__name__, str(e))) 863 sys.exit(1) 864 else: 865 raise
866 # END help the user in case he provides invalid options 867 868 #{ Overridable 869 @log_exception
870 - def execute(self, options, args):
871 """Method implementing the actual functionality of the command 872 :param options: Values instance of the optparse module 873 :param args: remaining positional arguments passed to the process on the commandline 874 :note: if you like to terminate, raise an exception""" 875 pass
876
877 - def option_parser(self):
878 """:return: OptionParser Instance containing all supported options 879 :note: Should be overridden by subclass to add additional options and 880 option groups themselves after calling the base class implementation""" 881 return self.parser
882 #} END needing subclass 883 884 885 #} END classes 886