1
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
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 }
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
43 """:return: True if the executable is mayapy"""
44 try:
45 mayapy_maya_version()
46 return True
47 except EnvironmentError:
48 return False
49
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
58
59 exec_path = make_path(os.path.realpath(sys.executable))
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
65
66 if version_token.endswith('-x64'):
67 version_token = version_token[:-4]
68
69
70 return float(version_token)
71
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
85
86 except ValueError:
87 pass
88
89
90 return parsed_arg, version
91
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
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
108 if curval:
109 if append:
110 value = curval + os.pathsep + value
111 else:
112 value = value + os.pathsep + curval
113
114
115 environment[env_var] = value
116
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
129
130 return versions
131
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
144 elif sys.platform == 'darwin':
145 mayaroot = "/Applications/Autodesk/maya"
146 elif sys.platform.startswith('win'):
147
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
157 tried_paths.append(basepath)
158
159 if mayaroot is None:
160 raise EnvironmentError("Could not find any maya installation, searched %s" % (', '.join(tried_paths)))
161
162
163 if mayaroot is None:
164 raise EnvironmentError("Current platform %r is unsupported" % sys.platform)
165
166
167 mayalocation = "%s%g%s" % (mayaroot, maya_version, suffix)
168
169
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
176
177 return mayalocation
178
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
196
197
198
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
205
206
207 env = os.environ
208
209
210
211
212
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
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
228
229
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
240
241
242
243
244
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
251
252
253
254 update_env_path(env, envppath, ppath, append=True)
255
256
257
258
259 env['MAYA_LOCATION'] = mayalocation
260
261
262 env['MRV_MAYA_VERSION'] = "%g" % maya_version
263
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
277 newargs.append(arg)
278
279 return tuple(newargs)
280
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
291
292
293
294
295 if ' ' in executable:
296 os.chdir(os.path.dirname(executable))
297 executable = os.path.basename(executable)
298
299 return executable
300
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
309 maya_version=None
310 if args:
311 parsed_successfully, maya_version = parse_maya_version(args[0], default=None)
312 if parsed_successfully:
313
314 args = args[1:]
315
316
317
318
319 if maya_version is None:
320
321
322 if uses_mayapy():
323 maya_version = mayapy_maya_version()
324
325
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
333
334
335
336
337
338
339
340
341
342
343
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
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
353
354 update_maya_environment(maya_version)
355 return (False, maya_version, tuple(args))
356
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
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
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
379
380 py_executable = "python%g" % py_version
381 if sys.platform.startswith('win'):
382
383
384 py_executable = "python%g" % (py_version*10)
385
386 return py_executable
387
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
409 if os.path.isfile(path):
410 return make_path(path)
411 tried_paths.append(path)
412
413
414
415 raise EnvironmentError("Script named %s not found, looked at %s" % (name, ', '.join(tried_paths)))
416
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
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
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
462
463 _execute(mayabin, tuple(args))
464
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
484
485
486 wrapper.__name__ = func.__name__
487 return wrapper
488
509
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
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
530 exc_type, value, traceback = sys.exc_info()
531 if value:
532 raise
533 else:
534 raise optparse.OptParseError(msg)
535
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
561
562
563 k_log_application_id = None
564
565
566 k_class_path = "package.module.YourClass"
567
568
569 k_version = None
570
571
572 _exec_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'bin', 'mrv')
573
574
575
576 _mrv_info_dir = None
577
578
579 _add_args = ['--mrv-no-maya']
580
581
582
583 k_usage = None
584
585
586 k_description = None
587
588
589 k_program_name = None
590
591
592
593 _daemon_umask = None
594
595
596
597 _daemon_workdir = None
598
599
600 _daemon_maxfd = 64
601
602
603 if (hasattr(os, "devnull")):
604 _daemon_redirect_to = os.devnull
605 else:
606 _daemon_redirect_to = "/dev/null"
607
608
609
610 __slots__ = ('parser', 'log')
611
612
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
624
625
626
627 pass
628
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
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
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
668
669
670 @classmethod
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
685
686 try:
687
688
689
690
691
692 pid = os.fork()
693 except OSError, e:
694 raise Exception, "%s [%d]" % (e.strerror, e.errno)
695
696 if (pid != 0):
697
698
699
700
701
702
703
704 return None
705
706
707
708
709
710
711
712
713 os.setsid()
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744 try:
745
746
747
748
749
750
751
752
753 pid = os.fork()
754 except OSError, e:
755 raise Exception, "%s [%d]" % (e.strerror, e.errno)
756
757 if (pid != 0):
758
759 os._exit(0)
760
761
762
763
764
765
766
767
768 if cls._daemon_workdir is not None:
769 os.chdir(cls._daemon_workdir)
770
771
772
773
774 if cls._daemon_umask is not None:
775 os.umask(cls._daemon_umask)
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806 import resource
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
814 for fd in range(debug_daemon*3, maxfd):
815 try:
816 os.close(fd)
817 except OSError:
818 pass
819
820
821
822
823
824
825
826
827
828 if not debug_daemon:
829 os.open(cls._daemon_redirect_to, os.O_RDWR)
830
831
832 os.dup2(0, 1)
833 os.dup2(0, 2)
834
835
836
837
838 cmdinstance = cls(_spawned=True)
839 return cmdinstance._execute(*args)
840
842 """:return: tuple(options, args) tuple of parsed options and remaining args
843 The arguments can be preprocessed"""
844 return options, args
845
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
855 logging.basicConfig()
856
857
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
867
868
869 @log_exception
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
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
883
884
885
886