mrv.cmd.base
Covered: 246 lines
Missed: 247 lines
Skipped 394 lines
Percent: 49 %
  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
 10
log = logging.getLogger("mrv.cmd.base")
 12
__docformat__ = "restructuredtext"
 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']
 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
}
 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
 40
	return str(version)[:2] == "20"
 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
 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")
 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)
 66
	if version_token.endswith('-x64'):
 67
		version_token = version_token[:-4]
 70
	return float(version_token)
 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
 86
	except ValueError:
 87
		pass
 90
	return parsed_arg, version
 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) 
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
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)
108
	if curval:
109
		if append:
110
			value = curval + os.pathsep + value
111
		else:
112
			value = value + os.pathsep + curval
115
	environment[env_var] = value
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
130
	return versions
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 = ''
139
	if sys.platform.startswith('linux'):
140
		mayaroot = "/usr/autodesk/maya"
141
		if os.path.isdir('/lib64'):
142
			suffix = "-x64"
144
	elif sys.platform == 'darwin':
145
		mayaroot = "/Applications/Autodesk/maya"
146
	elif sys.platform.startswith('win'):
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
157
			tried_paths.append(basepath)
159
		if mayaroot is None:
160
			raise EnvironmentError("Could not find any maya installation, searched %s" % (', '.join(tried_paths)))
163
	if mayaroot is None:
164
		raise EnvironmentError("Current platform %r is unsupported" % sys.platform)
167
	mayalocation = "%s%g%s" % (mayaroot, maya_version, suffix)
170
	if sys.platform == 'darwin':
171
		mayalocation=os.path.join(mayalocation, 'Maya.app', 'Contents')
173
	if not os.path.isdir(mayalocation):
174
		raise EnvironmentError("Could not find maya installation at %r" % mayalocation)
177
	return mayalocation
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)
186
	pylibdir = None
187
	envppath = "PYTHONPATH"
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"
200
	mayalocation = maya_location(maya_version)
202
	if not os.path.isdir(mayalocation):
203
		raise EnvironmentError("Could not find maya installation at %r" % mayalocation)
207
	env = os.environ
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':
219
		dldpath = os.path.join(mayalocation, 'MacOS')
220
		update_env_path(env, "DYLD_LIBRARY_PATH", dldpath)
222
		dldframeworkpath = os.path.join(mayalocation, 'Frameworks')
223
		update_env_path(env, "DYLD_FRAMEWORK_PATH", dldframeworkpath)
225
		env['MAYA_NO_BUNDLE_RESOURCES'] = "1"
230
		ppath = "/Library/Python/%s/site-packages" % py_version
231
		update_env_path(env, envppath, ppath, append=True)
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)
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")
254
	update_env_path(env, envppath, ppath, append=True)
259
	env['MAYA_LOCATION'] = mayalocation 
262
	env['MRV_MAYA_VERSION'] = "%g" % maya_version
264
def mangle_args(args):
265
	"""Enclose arguments in quotes if they contain spaces ... on windows only
266
	:return: tuple of possibly modified arguments
268
	:todo: remove this function, its unused"""
269
	if not sys.platform.startswith('win'):
270
		return args
272
	newargs = list()
273
	for arg in args:
274
		if ' ' in arg:
275
			arg = '"%s"' % arg
277
		newargs.append(arg)
279
	return tuple(newargs)
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.
285
	:note: Will change working dir
286
	:todo: remove this function, its unused"""
287
	if not sys.platform.startswith('win'):
288
		return executable
295
	if ' ' in executable:
296
		os.chdir(os.path.dirname(executable))
297
		executable = os.path.basename(executable)
299
	return executable
301
def init_environment(args):
302
	"""Intialize MRV up to the point where we can replace this process with the 
303
	one we prepared
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"""
309
	maya_version=None
310
	if args:
311
		parsed_successfully, maya_version = parse_maya_version(args[0], default=None)
312
		if parsed_successfully:
314
			args = args[1:]
319
	if maya_version is None:
322
		if uses_mayapy():
323
			maya_version = mayapy_maya_version()
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)
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)
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")
354
	update_maya_environment(maya_version)
355
	return (False, maya_version, tuple(args))
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 !
362
	:param args: arguments, without the executable as first argument
363
	:note: does not return """
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)
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'
380
	py_executable = "python%g" % py_version
381
	if sys.platform.startswith('win'):
384
		py_executable = "python%g" % (py_version*10)
386
	return py_executable
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.
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__)
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)
409
			if os.path.isfile(path):
410
				return make_path(path)
411
			tried_paths.append(path)
415
	raise EnvironmentError("Script named %s not found, looked at %s" % (name, ', '.join(tried_paths))) 
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
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) 
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
441
		mayalocation = maya_location(maya_version)
442
		mayapy_executable = os.path.join(mayalocation, "bin", "mayapy")
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))
452
def exec_maya_binary(args, maya_version):
453
	"""Replace this process with the maya executable as specified by maya_version.
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')
463
	_execute(mayabin, tuple(args))
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
486
	wrapper.__name__ = func.__name__
487
	return wrapper
493
class SpawnedHelpFormatter(optparse.TitledHelpFormatter):
494
	"""Formatter assuring our help looks good"""
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)
502
		if self.level == 0:
503
			return text
504
		lines = text.splitlines(True)
505
		return ''.join('  '*self.level + l for l in lines)
507
	def format_usage(self, usage):
508
		return "usage: %s\n" % usage
511
class SpawnedOptionParser(optparse.OptionParser):
512
	"""Customized version to ease use of SpawnedCommand
514
	Initialized with the 'spawned' keyword in addition 
515
	to the default keywords to prevent a system exit"""
517
	def __init__(self, *args, **kwargs):
518
		self.spawned = kwargs.pop('spawned', False)
519
		kwargs['formatter'] = SpawnedHelpFormatter()
520
		optparse.OptionParser.__init__(self, *args, **kwargs)
522
	def exit(self, status=0, msg=None):
523
		if msg:
524
			sys.stderr.write(msg)
526
		if self.spawned:
527
			sys.exit(status)
528
		else:
530
			exc_type, value, traceback = sys.exc_info()
531
			if value:
532
				raise
533
			else:
534
				raise optparse.OptParseError(msg)
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.
544
	The target command must be derived from this class and must implement the 
545
	'execute' method.
547
	To use this class, derive from it and change the configuration variables
548
	accordingly.
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
553
	The parser used to parse all options is vailable at its member called 'parser', 
554
	its set during ``option_parser``
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"""
563
	k_log_application_id = None
566
	k_class_path = "package.module.YourClass"
569
	k_version = None
572
	_exec_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'bin', 'mrv')
576
	_mrv_info_dir = None
579
	_add_args = ['--mrv-no-maya']
583
	k_usage = None
586
	k_description = None
589
	k_program_name = None
593
	_daemon_umask = None
597
	_daemon_workdir = None
600
	_daemon_maxfd = 64
603
	if (hasattr(os, "devnull")):
604
	   _daemon_redirect_to = os.devnull
605
	else:
606
	   _daemon_redirect_to = "/dev/null"
610
	__slots__ = ('parser', 'log')
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:
618
			* sys.exit
619
			* change configuration of logging system"""
620
		try:
621
			super(SpawnedCommand, self).__init__(*args, **kwargs)
622
		except TypeError:
627
			pass
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)
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
642
		:param kwargs: Additional keyword arguments to be passed to Subprocess.Popen, 
643
			use it to configure your IO
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)
651
		if os.name == 'nt':
652
			margs.insert(0, sys.executable)
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
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
670
	@classmethod
671
	def daemonize(cls, *args):
672
		"""
673
		Damonize the spawned command, passing *args to the instanciated command's
674
		execute method.
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")
686
		try:
692
			pid = os.fork()
693
		except OSError, e:
694
			raise Exception, "%s [%d]" % (e.strerror, e.errno)
696
		if (pid != 0):
704
			return None
713
		os.setsid()
744
		try:
753
			pid = os.fork()	# Fork a second child.
754
		except OSError, e:
755
			raise Exception, "%s [%d]" % (e.strerror, e.errno)
757
		if (pid != 0):
759
			os._exit(0) # Exit parent (the first child) of the second child.
768
		if cls._daemon_workdir is not None:
769
			os.chdir(cls._daemon_workdir)
774
		if cls._daemon_umask is not None:
775
			os.umask(cls._daemon_umask)
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
811
		debug_daemon = False
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
828
		if not debug_daemon:
829
			os.open(cls._daemon_redirect_to, os.O_RDWR)	# standard input (0)
832
			os.dup2(0, 1)			# standard output (1)
833
			os.dup2(0, 2)			# standard error (2)
838
		cmdinstance = cls(_spawned=True)
839
		return cmdinstance._execute(*args)
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
846
	def _execute(self, *args):
847
		"""internal method handling the basic arguments in a pre-process before 
848
		calling ``execute``
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)))
855
		logging.basicConfig()
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
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
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