1 # -*- test-case-name: twisted.test.test_application,twisted.test.test_twistd -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
5 import sys, os, pdb, getpass, traceback, signal
6 from operator import attrgetter
8 from twisted.python import runtime, log, usage, failure, util, logfile
9 from twisted.python.versions import Version
10 from twisted.python.reflect import qual, namedAny
11 from twisted.python.deprecate import deprecated
12 from twisted.python.log import ILogObserver
13 from twisted.persisted import sob
14 from twisted.application import service, reactors
15 from twisted.internet import defer
16 from twisted import copyright, plugin
18 # Expose the new implementation of installReactor at the old location.
19 from twisted.application.reactors import installReactor
20 from twisted.application.reactors import NoSuchReactor
24 class _BasicProfiler(object):
26 @ivar saveStats: if C{True}, save the stats information instead of the
28 @type saveStats: C{bool}
30 @ivar profileOutput: the name of the file use to print profile data.
31 @type profileOutput: C{str}
34 def __init__(self, profileOutput, saveStats):
35 self.profileOutput = profileOutput
36 self.saveStats = saveStats
39 def _reportImportError(self, module, e):
41 Helper method to report an import error with a profile module. This
42 has to be explicit because some of these modules are removed by
43 distributions due to them being non-free.
45 s = "Failed to import module %s: %s" % (module, e)
47 This is most likely caused by your operating system not including
48 the module due to it being non-free. Either do not use the option
49 --profile, or install the module; your operating system vendor
50 may provide it in a separate package.
56 class ProfileRunner(_BasicProfiler):
58 Runner for the standard profile module.
61 def run(self, reactor):
63 Run reactor under the standard profiler.
67 except ImportError, e:
68 self._reportImportError("profile", e)
71 p.runcall(reactor.run)
73 p.dump_stats(self.profileOutput)
75 tmp, sys.stdout = sys.stdout, open(self.profileOutput, 'a')
79 sys.stdout, tmp = tmp, sys.stdout
84 class HotshotRunner(_BasicProfiler):
86 Runner for the hotshot profile module.
89 def run(self, reactor):
91 Run reactor under the hotshot profiler.
95 except (ImportError, SystemExit), e:
96 # Certain versions of Debian (and Debian derivatives) raise
97 # SystemExit when importing hotshot if the "non-free" profiler
98 # module is not installed. Someone eventually recognized this
99 # as a bug and changed the Debian packaged Python to raise
100 # ImportError instead. Handle both exception types here in
101 # order to support the versions of Debian which have this
102 # behavior. The bug report which prompted the introduction of
103 # this highly undesirable behavior should be available online at
104 # <http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=334067>.
105 # There seems to be no corresponding bug report which resulted
106 # in the behavior being removed. -exarkun
107 self._reportImportError("hotshot", e)
109 # this writes stats straight out
110 p = hotshot.Profile(self.profileOutput)
111 p.runcall(reactor.run)
113 # stats are automatically written to file, nothing to do
116 s = hotshot.stats.load(self.profileOutput)
119 if getattr(s, 'stream', None) is not None:
120 # Python 2.5 and above supports a stream attribute
121 s.stream = open(self.profileOutput, 'w')
125 # But we have to use a trick for Python < 2.5
126 tmp, sys.stdout = sys.stdout, open(self.profileOutput, 'w')
130 sys.stdout, tmp = tmp, sys.stdout
135 class CProfileRunner(_BasicProfiler):
137 Runner for the cProfile module.
140 def run(self, reactor):
142 Run reactor under the cProfile profiler.
145 import cProfile, pstats
146 except ImportError, e:
147 self._reportImportError("cProfile", e)
149 p = cProfile.Profile()
150 p.runcall(reactor.run)
152 p.dump_stats(self.profileOutput)
154 stream = open(self.profileOutput, 'w')
155 s = pstats.Stats(p, stream=stream)
163 class AppProfiler(object):
165 Class which selects a specific profile runner based on configuration
168 @ivar profiler: the name of the selected profiler.
169 @type profiler: C{str}
171 profilers = {"profile": ProfileRunner, "hotshot": HotshotRunner,
172 "cprofile": CProfileRunner}
174 def __init__(self, options):
175 saveStats = options.get("savestats", False)
176 profileOutput = options.get("profile", None)
177 self.profiler = options.get("profiler", "hotshot").lower()
178 if self.profiler in self.profilers:
179 profiler = self.profilers[self.profiler](profileOutput, saveStats)
180 self.run = profiler.run
182 raise SystemExit("Unsupported profiler name: %s" % (self.profiler,))
186 class AppLogger(object):
188 Class managing logging faciliy of the application.
190 @ivar _logfilename: The name of the file to which to log, if other than the
192 @type _logfilename: C{str}
194 @ivar _observerFactory: Callable object that will create a log observer, or
197 @ivar _observer: log observer added at C{start} and removed at C{stop}.
198 @type _observer: C{callable}
202 def __init__(self, options):
203 self._logfilename = options.get("logfile", "")
204 self._observerFactory = options.get("logger") or None
207 def start(self, application):
209 Initialize the logging system.
211 If a customer logger was specified on the command line it will be
212 used. If not, and an L{ILogObserver} component has been set on
213 C{application}, then it will be used as the log observer. Otherwise a
214 log observer will be created based on the command-line options for
215 built-in loggers (e.g. C{--logfile}).
217 @param application: The application on which to check for an
220 if self._observerFactory is not None:
221 observer = self._observerFactory()
223 observer = application.getComponent(ILogObserver, None)
226 observer = self._getLogObserver()
227 self._observer = observer
228 log.startLoggingWithObserver(self._observer)
232 def _initialLog(self):
234 Print twistd start log message.
236 from twisted.internet import reactor
237 log.msg("twistd %s (%s %s) starting up." % (copyright.version,
239 runtime.shortPythonVersion()))
240 log.msg('reactor class: %s.' % (qual(reactor.__class__),))
243 def _getLogObserver(self):
245 Create a log observer to be added to the logging system before running
248 if self._logfilename == '-' or not self._logfilename:
251 logFile = logfile.LogFile.fromFullPath(self._logfilename)
252 return log.FileLogObserver(logFile).emit
257 Print twistd stop log message.
259 log.msg("Server Shut Down.")
260 if self._observer is not None:
261 log.removeObserver(self._observer)
262 self._observer = None
267 def do_stop(self, arg):
268 self.clear_all_breaks()
270 from twisted.internet import reactor
271 reactor.callLater(0, reactor.stop)
276 print """stop - Continue execution, then cleanly shutdown the twisted reactor."""
282 pdb.Pdb.set_quit = set_quit
283 pdb.Pdb.do_stop = do_stop
284 pdb.Pdb.help_stop = help_stop
288 def runReactorWithLogging(config, oldstdout, oldstderr, profiler=None, reactor=None):
290 Start the reactor, using profiling if specified by the configuration, and
291 log any error happening in the process.
293 @param config: configuration of the twistd application.
294 @type config: L{ServerOptions}
296 @param oldstdout: initial value of C{sys.stdout}.
297 @type oldstdout: C{file}
299 @param oldstderr: initial value of C{sys.stderr}.
300 @type oldstderr: C{file}
302 @param profiler: object used to run the reactor with profiling.
303 @type profiler: L{AppProfiler}
305 @param reactor: The reactor to use. If C{None}, the global reactor will
309 from twisted.internet import reactor
311 if config['profile']:
312 if profiler is not None:
313 profiler.run(reactor)
314 elif config['debug']:
315 sys.stdout = oldstdout
316 sys.stderr = oldstderr
317 if runtime.platformType == 'posix':
318 signal.signal(signal.SIGUSR2, lambda *args: pdb.set_trace())
319 signal.signal(signal.SIGINT, lambda *args: pdb.set_trace())
321 pdb.runcall(reactor.run)
325 if config['nodaemon']:
328 file = open("TWISTD-CRASH.log",'a')
329 traceback.print_exc(file=file)
334 def getPassphrase(needed):
336 return getpass.getpass('Passphrase: ')
342 def getSavePassphrase(needed):
344 passphrase = util.getPassword("Encryption passphrase: ")
350 class ApplicationRunner(object):
352 An object which helps running an application based on a config object.
354 Subclass me and implement preApplication and postApplication
355 methods. postApplication generally will want to run the reactor
356 after starting the application.
358 @ivar config: The config object, which provides a dict-like interface.
360 @ivar application: Available in postApplication, but not
361 preApplication. This is the application object.
363 @ivar profilerFactory: Factory for creating a profiler object, able to
364 profile the application if options are set accordingly.
366 @ivar profiler: Instance provided by C{profilerFactory}.
368 @ivar loggerFactory: Factory for creating object responsible for logging.
370 @ivar logger: Instance provided by C{loggerFactory}.
372 profilerFactory = AppProfiler
373 loggerFactory = AppLogger
375 def __init__(self, config):
377 self.profiler = self.profilerFactory(config)
378 self.logger = self.loggerFactory(config)
385 self.preApplication()
386 self.application = self.createOrGetApplication()
388 self.logger.start(self.application)
390 self.postApplication()
394 def startReactor(self, reactor, oldstdout, oldstderr):
396 Run the reactor with the given configuration. Subclasses should
397 probably call this from C{postApplication}.
399 @see: L{runReactorWithLogging}
401 runReactorWithLogging(
402 self.config, oldstdout, oldstderr, self.profiler, reactor)
405 def preApplication(self):
407 Override in subclass.
409 This should set up any state necessary before loading and
410 running the Application.
412 raise NotImplementedError()
415 def postApplication(self):
417 Override in subclass.
419 This will be called after the application has been loaded (so
420 the C{application} attribute will be set). Generally this
421 should start the application and run the reactor.
423 raise NotImplementedError()
426 def createOrGetApplication(self):
428 Create or load an Application based on the parameters found in the
429 given L{ServerOptions} instance.
431 If a subcommand was used, the L{service.IServiceMaker} that it
432 represents will be used to construct a service to be added to
433 a newly-created Application.
435 Otherwise, an application will be loaded based on parameters in
438 if self.config.subCommand:
439 # If a subcommand was given, it's our responsibility to create
440 # the application, instead of load it from a file.
442 # loadedPlugins is set up by the ServerOptions.subCommands
443 # property, which is iterated somewhere in the bowels of
445 plg = self.config.loadedPlugins[self.config.subCommand]
446 ser = plg.makeService(self.config.subOptions)
447 application = service.Application(plg.tapname)
448 ser.setServiceParent(application)
450 passphrase = getPassphrase(self.config['encrypted'])
451 application = getApplication(self.config, passphrase)
456 def getApplication(config, passphrase):
458 for t in ['python', 'source', 'file'] if config[t]][0]
459 filename, style = s[0], {'file':'pickle'}.get(s[1],s[1])
461 log.msg("Loading %s..." % filename)
462 application = service.loadApplication(filename, style, passphrase)
465 s = "Failed to load application: %s" % e
466 if isinstance(e, KeyError) and e.args[0] == "application":
468 Could not find 'application' in the file. To use 'twistd -y', your .tac
469 file must create a suitable object (e.g., by calling service.Application())
470 and store it in a variable named 'application'. twistd loads your .tac file
471 and scans the global variables for one of this name.
473 Please read the 'Using Application' HOWTO for details.
475 traceback.print_exc(file=log.logfile)
478 sys.exit('\n' + s + '\n')
483 def _reactorAction():
484 return usage.CompleteList([r.shortName for r in reactors.getReactorTypes()])
487 class ReactorSelectionMixin:
489 Provides options for selecting a reactor to install.
491 If a reactor is installed, the short name which was used to locate it is
492 saved as the value for the C{"reactor"} key.
494 compData = usage.Completions(
495 optActions={"reactor": _reactorAction})
497 messageOutput = sys.stdout
498 _getReactorTypes = staticmethod(reactors.getReactorTypes)
501 def opt_help_reactors(self):
503 Display a list of possibly available reactor names.
505 rcts = sorted(self._getReactorTypes(), key=attrgetter('shortName'))
507 self.messageOutput.write(' %-4s\t%s\n' %
508 (r.shortName, r.description))
512 def opt_reactor(self, shortName):
514 Which reactor to use (see --help-reactors for a list of possibilities)
516 # Actually actually actually install the reactor right at this very
517 # moment, before any other code (for example, a sub-command plugin)
518 # runs and accidentally imports and installs the default reactor.
520 # This could probably be improved somehow.
522 installReactor(shortName)
523 except NoSuchReactor:
524 msg = ("The specified reactor does not exist: '%s'.\n"
525 "See the list of available reactors with "
526 "--help-reactors" % (shortName,))
527 raise usage.UsageError(msg)
529 msg = ("The specified reactor cannot be used, failed with error: "
530 "%s.\nSee the list of available reactors with "
531 "--help-reactors" % (e,))
532 raise usage.UsageError(msg)
534 self["reactor"] = shortName
540 class ServerOptions(usage.Options, ReactorSelectionMixin):
542 longdesc = ("twistd reads a twisted.application.service.Application out "
543 "of a file and runs it.")
545 optFlags = [['savestats', None,
546 "save the Stats object rather than the text output of "
548 ['no_save','o', "do not save state on shutdown"],
550 "The specified tap/aos file is encrypted."]]
552 optParameters = [['logfile','l', None,
553 "log to a specified file, - for stdout"],
554 ['logger', None, None,
555 "A fully-qualified name to a log observer factory to use "
556 "for the initial log observer. Takes precedence over "
557 "--logfile and --syslog (when available)."],
558 ['profile', 'p', None,
559 "Run in profile mode, dumping results to specified file"],
560 ['profiler', None, "hotshot",
561 "Name of the profiler to use (%s)." %
562 ", ".join(AppProfiler.profilers)],
563 ['file','f','twistd.tap',
564 "read the given .tap file"],
566 "read an application from within a Python file "
568 ['source', 's', None,
569 "Read an application from a .tas file (AOT format)."],
571 'Change to a supplied directory before running']]
573 compData = usage.Completions(
574 mutuallyExclusive=[("file", "python", "source")],
575 optActions={"file": usage.CompleteFiles("*.tap"),
576 "python": usage.CompleteFiles("*.(tac|py)"),
577 "source": usage.CompleteFiles("*.tas"),
578 "rundir": usage.CompleteDirs()}
581 _getPlugins = staticmethod(plugin.getPlugins)
583 def __init__(self, *a, **kw):
584 self['debug'] = False
585 usage.Options.__init__(self, *a, **kw)
590 Run the application in the Python Debugger (implies nodaemon),
591 sending SIGUSR2 will drop into debugger
593 defer.setDebugging(True)
594 failure.startDebugMode()
601 Print an insanely verbose log of everything that happens.
602 Useful when debugging freezes or locks in complex code."""
603 sys.settrace(util.spewer)
608 threading.settrace(util.spewer)
611 def parseOptions(self, options=None):
613 options = sys.argv[1:] or ["--help"]
614 usage.Options.parseOptions(self, options)
617 def postOptions(self):
618 if self.subCommand or self['python']:
619 self['no_save'] = True
620 if self['logger'] is not None:
622 self['logger'] = namedAny(self['logger'])
624 raise usage.UsageError("Logger '%s' could not be imported: %s"
625 % (self['logger'], e))
628 def subCommands(self):
629 plugins = self._getPlugins(service.IServiceMaker)
630 self.loadedPlugins = {}
631 for plug in sorted(plugins, key=attrgetter('tapname')):
632 self.loadedPlugins[plug.tapname] = plug
635 # Avoid resolving the options attribute right away, in case
636 # it's a property with a non-trivial getter (eg, one which
638 lambda plug=plug: plug.options(),
640 subCommands = property(subCommands)
644 def run(runApp, ServerOptions):
645 config = ServerOptions()
647 config.parseOptions()
648 except usage.error, ue:
650 print "%s: %s" % (sys.argv[0], ue)
656 def convertStyle(filein, typein, passphrase, fileout, typeout, encrypt):
657 application = service.loadApplication(filein, typein, passphrase)
658 sob.IPersistable(application).setStyle(typeout)
659 passphrase = getSavePassphrase(encrypt)
662 sob.IPersistable(application).save(filename=fileout, passphrase=passphrase)
666 def startApplication(application, save):
667 from twisted.internet import reactor
668 service.IService(application).startService()
670 p = sob.IPersistable(application)
671 reactor.addSystemEventTrigger('after', 'shutdown', p.save, 'shutdown')
672 reactor.addSystemEventTrigger('before', 'shutdown',
673 service.IService(application).stopService)