Ausgabe der neuen DB Einträge
This commit is contained in:
parent
bad48e1627
commit
cfbbb9ee3d
2399 changed files with 843193 additions and 43 deletions
|
|
@ -0,0 +1,6 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Configuration objects for Twisted Applications.
|
||||
"""
|
||||
708
venv/lib/python3.9/site-packages/twisted/application/app.py
Normal file
708
venv/lib/python3.9/site-packages/twisted/application/app.py
Normal file
|
|
@ -0,0 +1,708 @@
|
|||
# -*- test-case-name: twisted.test.test_application,twisted.test.test_twistd -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import sys
|
||||
import os
|
||||
import pdb
|
||||
import getpass
|
||||
import traceback
|
||||
import signal
|
||||
import warnings
|
||||
|
||||
from operator import attrgetter
|
||||
|
||||
from twisted import copyright, plugin, logger
|
||||
from twisted.application import service, reactors
|
||||
from twisted.internet import defer
|
||||
from twisted.persisted import sob
|
||||
from twisted.python import runtime, log, usage, failure, util, logfile
|
||||
from twisted.python._oldstyle import _oldStyle
|
||||
from twisted.python.reflect import (qual, namedAny, namedModule)
|
||||
from twisted.internet.interfaces import _ISupportsExitSignalCapturing
|
||||
|
||||
# Expose the new implementation of installReactor at the old location.
|
||||
from twisted.application.reactors import installReactor
|
||||
from twisted.application.reactors import NoSuchReactor
|
||||
|
||||
|
||||
class _BasicProfiler(object):
|
||||
"""
|
||||
@ivar saveStats: if C{True}, save the stats information instead of the
|
||||
human readable format
|
||||
@type saveStats: C{bool}
|
||||
|
||||
@ivar profileOutput: the name of the file use to print profile data.
|
||||
@type profileOutput: C{str}
|
||||
"""
|
||||
|
||||
def __init__(self, profileOutput, saveStats):
|
||||
self.profileOutput = profileOutput
|
||||
self.saveStats = saveStats
|
||||
|
||||
|
||||
def _reportImportError(self, module, e):
|
||||
"""
|
||||
Helper method to report an import error with a profile module. This
|
||||
has to be explicit because some of these modules are removed by
|
||||
distributions due to them being non-free.
|
||||
"""
|
||||
s = "Failed to import module %s: %s" % (module, e)
|
||||
s += """
|
||||
This is most likely caused by your operating system not including
|
||||
the module due to it being non-free. Either do not use the option
|
||||
--profile, or install the module; your operating system vendor
|
||||
may provide it in a separate package.
|
||||
"""
|
||||
raise SystemExit(s)
|
||||
|
||||
|
||||
|
||||
class ProfileRunner(_BasicProfiler):
|
||||
"""
|
||||
Runner for the standard profile module.
|
||||
"""
|
||||
|
||||
def run(self, reactor):
|
||||
"""
|
||||
Run reactor under the standard profiler.
|
||||
"""
|
||||
try:
|
||||
import profile
|
||||
except ImportError as e:
|
||||
self._reportImportError("profile", e)
|
||||
|
||||
p = profile.Profile()
|
||||
p.runcall(reactor.run)
|
||||
if self.saveStats:
|
||||
p.dump_stats(self.profileOutput)
|
||||
else:
|
||||
tmp, sys.stdout = sys.stdout, open(self.profileOutput, 'a')
|
||||
try:
|
||||
p.print_stats()
|
||||
finally:
|
||||
sys.stdout, tmp = tmp, sys.stdout
|
||||
tmp.close()
|
||||
|
||||
|
||||
|
||||
class CProfileRunner(_BasicProfiler):
|
||||
"""
|
||||
Runner for the cProfile module.
|
||||
"""
|
||||
|
||||
def run(self, reactor):
|
||||
"""
|
||||
Run reactor under the cProfile profiler.
|
||||
"""
|
||||
try:
|
||||
import cProfile
|
||||
import pstats
|
||||
except ImportError as e:
|
||||
self._reportImportError("cProfile", e)
|
||||
|
||||
p = cProfile.Profile()
|
||||
p.runcall(reactor.run)
|
||||
if self.saveStats:
|
||||
p.dump_stats(self.profileOutput)
|
||||
else:
|
||||
with open(self.profileOutput, 'w') as stream:
|
||||
s = pstats.Stats(p, stream=stream)
|
||||
s.strip_dirs()
|
||||
s.sort_stats(-1)
|
||||
s.print_stats()
|
||||
|
||||
|
||||
|
||||
class AppProfiler(object):
|
||||
"""
|
||||
Class which selects a specific profile runner based on configuration
|
||||
options.
|
||||
|
||||
@ivar profiler: the name of the selected profiler.
|
||||
@type profiler: C{str}
|
||||
"""
|
||||
profilers = {"profile": ProfileRunner, "cprofile": CProfileRunner}
|
||||
|
||||
def __init__(self, options):
|
||||
saveStats = options.get("savestats", False)
|
||||
profileOutput = options.get("profile", None)
|
||||
self.profiler = options.get("profiler", "cprofile").lower()
|
||||
if self.profiler in self.profilers:
|
||||
profiler = self.profilers[self.profiler](profileOutput, saveStats)
|
||||
self.run = profiler.run
|
||||
else:
|
||||
raise SystemExit("Unsupported profiler name: %s" %
|
||||
(self.profiler,))
|
||||
|
||||
|
||||
|
||||
class AppLogger(object):
|
||||
"""
|
||||
An L{AppLogger} attaches the configured log observer specified on the
|
||||
commandline to a L{ServerOptions} object, a custom L{logger.ILogObserver},
|
||||
or a legacy custom {log.ILogObserver}.
|
||||
|
||||
@ivar _logfilename: The name of the file to which to log, if other than the
|
||||
default.
|
||||
@type _logfilename: C{str}
|
||||
|
||||
@ivar _observerFactory: Callable object that will create a log observer, or
|
||||
None.
|
||||
|
||||
@ivar _observer: log observer added at C{start} and removed at C{stop}.
|
||||
@type _observer: a callable that implements L{logger.ILogObserver} or
|
||||
L{log.ILogObserver}.
|
||||
"""
|
||||
_observer = None
|
||||
|
||||
def __init__(self, options):
|
||||
"""
|
||||
Initialize an L{AppLogger} with a L{ServerOptions}.
|
||||
"""
|
||||
self._logfilename = options.get("logfile", "")
|
||||
self._observerFactory = options.get("logger") or None
|
||||
|
||||
|
||||
def start(self, application):
|
||||
"""
|
||||
Initialize the global logging system for the given application.
|
||||
|
||||
If a custom logger was specified on the command line it will be used.
|
||||
If not, and an L{logger.ILogObserver} or legacy L{log.ILogObserver}
|
||||
component has been set on C{application}, then it will be used as the
|
||||
log observer. Otherwise a log observer will be created based on the
|
||||
command line options for built-in loggers (e.g. C{--logfile}).
|
||||
|
||||
@param application: The application on which to check for an
|
||||
L{logger.ILogObserver} or legacy L{log.ILogObserver}.
|
||||
@type application: L{twisted.python.components.Componentized}
|
||||
"""
|
||||
if self._observerFactory is not None:
|
||||
observer = self._observerFactory()
|
||||
else:
|
||||
observer = application.getComponent(logger.ILogObserver, None)
|
||||
if observer is None:
|
||||
# If there's no new ILogObserver, try the legacy one
|
||||
observer = application.getComponent(log.ILogObserver, None)
|
||||
|
||||
if observer is None:
|
||||
observer = self._getLogObserver()
|
||||
self._observer = observer
|
||||
|
||||
if logger.ILogObserver.providedBy(self._observer):
|
||||
observers = [self._observer]
|
||||
elif log.ILogObserver.providedBy(self._observer):
|
||||
observers = [logger.LegacyLogObserverWrapper(self._observer)]
|
||||
else:
|
||||
warnings.warn(
|
||||
("Passing a logger factory which makes log observers which do "
|
||||
"not implement twisted.logger.ILogObserver or "
|
||||
"twisted.python.log.ILogObserver to "
|
||||
"twisted.application.app.AppLogger was deprecated in "
|
||||
"Twisted 16.2. Please use a factory that produces "
|
||||
"twisted.logger.ILogObserver (or the legacy "
|
||||
"twisted.python.log.ILogObserver) implementing objects "
|
||||
"instead."),
|
||||
DeprecationWarning,
|
||||
stacklevel=2)
|
||||
observers = [logger.LegacyLogObserverWrapper(self._observer)]
|
||||
|
||||
logger.globalLogBeginner.beginLoggingTo(observers)
|
||||
self._initialLog()
|
||||
|
||||
|
||||
def _initialLog(self):
|
||||
"""
|
||||
Print twistd start log message.
|
||||
"""
|
||||
from twisted.internet import reactor
|
||||
logger._loggerFor(self).info(
|
||||
"twistd {version} ({exe} {pyVersion}) starting up.",
|
||||
version=copyright.version, exe=sys.executable,
|
||||
pyVersion=runtime.shortPythonVersion())
|
||||
logger._loggerFor(self).info('reactor class: {reactor}.',
|
||||
reactor=qual(reactor.__class__))
|
||||
|
||||
|
||||
def _getLogObserver(self):
|
||||
"""
|
||||
Create a log observer to be added to the logging system before running
|
||||
this application.
|
||||
"""
|
||||
if self._logfilename == '-' or not self._logfilename:
|
||||
logFile = sys.stdout
|
||||
else:
|
||||
logFile = logfile.LogFile.fromFullPath(self._logfilename)
|
||||
return logger.textFileLogObserver(logFile)
|
||||
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Remove all log observers previously set up by L{AppLogger.start}.
|
||||
"""
|
||||
logger._loggerFor(self).info("Server Shut Down.")
|
||||
if self._observer is not None:
|
||||
logger.globalLogPublisher.removeObserver(self._observer)
|
||||
self._observer = None
|
||||
|
||||
|
||||
|
||||
def fixPdb():
|
||||
def do_stop(self, arg):
|
||||
self.clear_all_breaks()
|
||||
self.set_continue()
|
||||
from twisted.internet import reactor
|
||||
reactor.callLater(0, reactor.stop)
|
||||
return 1
|
||||
|
||||
|
||||
def help_stop(self):
|
||||
print("stop - Continue execution, then cleanly shutdown the twisted "
|
||||
"reactor.")
|
||||
|
||||
|
||||
def set_quit(self):
|
||||
os._exit(0)
|
||||
|
||||
pdb.Pdb.set_quit = set_quit
|
||||
pdb.Pdb.do_stop = do_stop
|
||||
pdb.Pdb.help_stop = help_stop
|
||||
|
||||
|
||||
|
||||
def runReactorWithLogging(config, oldstdout, oldstderr, profiler=None,
|
||||
reactor=None):
|
||||
"""
|
||||
Start the reactor, using profiling if specified by the configuration, and
|
||||
log any error happening in the process.
|
||||
|
||||
@param config: configuration of the twistd application.
|
||||
@type config: L{ServerOptions}
|
||||
|
||||
@param oldstdout: initial value of C{sys.stdout}.
|
||||
@type oldstdout: C{file}
|
||||
|
||||
@param oldstderr: initial value of C{sys.stderr}.
|
||||
@type oldstderr: C{file}
|
||||
|
||||
@param profiler: object used to run the reactor with profiling.
|
||||
@type profiler: L{AppProfiler}
|
||||
|
||||
@param reactor: The reactor to use. If L{None}, the global reactor will
|
||||
be used.
|
||||
"""
|
||||
if reactor is None:
|
||||
from twisted.internet import reactor
|
||||
try:
|
||||
if config['profile']:
|
||||
if profiler is not None:
|
||||
profiler.run(reactor)
|
||||
elif config['debug']:
|
||||
sys.stdout = oldstdout
|
||||
sys.stderr = oldstderr
|
||||
if runtime.platformType == 'posix':
|
||||
signal.signal(signal.SIGUSR2, lambda *args: pdb.set_trace())
|
||||
signal.signal(signal.SIGINT, lambda *args: pdb.set_trace())
|
||||
fixPdb()
|
||||
pdb.runcall(reactor.run)
|
||||
else:
|
||||
reactor.run()
|
||||
except:
|
||||
close = False
|
||||
if config['nodaemon']:
|
||||
file = oldstdout
|
||||
else:
|
||||
file = open("TWISTD-CRASH.log", "a")
|
||||
close = True
|
||||
try:
|
||||
traceback.print_exc(file=file)
|
||||
file.flush()
|
||||
finally:
|
||||
if close:
|
||||
file.close()
|
||||
|
||||
|
||||
|
||||
def getPassphrase(needed):
|
||||
if needed:
|
||||
return getpass.getpass('Passphrase: ')
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def getSavePassphrase(needed):
|
||||
if needed:
|
||||
return util.getPassword("Encryption passphrase: ")
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
|
||||
class ApplicationRunner(object):
|
||||
"""
|
||||
An object which helps running an application based on a config object.
|
||||
|
||||
Subclass me and implement preApplication and postApplication
|
||||
methods. postApplication generally will want to run the reactor
|
||||
after starting the application.
|
||||
|
||||
@ivar config: The config object, which provides a dict-like interface.
|
||||
|
||||
@ivar application: Available in postApplication, but not
|
||||
preApplication. This is the application object.
|
||||
|
||||
@ivar profilerFactory: Factory for creating a profiler object, able to
|
||||
profile the application if options are set accordingly.
|
||||
|
||||
@ivar profiler: Instance provided by C{profilerFactory}.
|
||||
|
||||
@ivar loggerFactory: Factory for creating object responsible for logging.
|
||||
|
||||
@ivar logger: Instance provided by C{loggerFactory}.
|
||||
"""
|
||||
profilerFactory = AppProfiler
|
||||
loggerFactory = AppLogger
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.profiler = self.profilerFactory(config)
|
||||
self.logger = self.loggerFactory(config)
|
||||
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Run the application.
|
||||
"""
|
||||
self.preApplication()
|
||||
self.application = self.createOrGetApplication()
|
||||
|
||||
self.logger.start(self.application)
|
||||
|
||||
self.postApplication()
|
||||
self.logger.stop()
|
||||
|
||||
|
||||
def startReactor(self, reactor, oldstdout, oldstderr):
|
||||
"""
|
||||
Run the reactor with the given configuration. Subclasses should
|
||||
probably call this from C{postApplication}.
|
||||
|
||||
@see: L{runReactorWithLogging}
|
||||
"""
|
||||
if reactor is None:
|
||||
from twisted.internet import reactor
|
||||
runReactorWithLogging(
|
||||
self.config, oldstdout, oldstderr, self.profiler, reactor)
|
||||
|
||||
if _ISupportsExitSignalCapturing.providedBy(reactor):
|
||||
self._exitSignal = reactor._exitSignal
|
||||
else:
|
||||
self._exitSignal = None
|
||||
|
||||
|
||||
def preApplication(self):
|
||||
"""
|
||||
Override in subclass.
|
||||
|
||||
This should set up any state necessary before loading and
|
||||
running the Application.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def postApplication(self):
|
||||
"""
|
||||
Override in subclass.
|
||||
|
||||
This will be called after the application has been loaded (so
|
||||
the C{application} attribute will be set). Generally this
|
||||
should start the application and run the reactor.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def createOrGetApplication(self):
|
||||
"""
|
||||
Create or load an Application based on the parameters found in the
|
||||
given L{ServerOptions} instance.
|
||||
|
||||
If a subcommand was used, the L{service.IServiceMaker} that it
|
||||
represents will be used to construct a service to be added to
|
||||
a newly-created Application.
|
||||
|
||||
Otherwise, an application will be loaded based on parameters in
|
||||
the config.
|
||||
"""
|
||||
if self.config.subCommand:
|
||||
# If a subcommand was given, it's our responsibility to create
|
||||
# the application, instead of load it from a file.
|
||||
|
||||
# loadedPlugins is set up by the ServerOptions.subCommands
|
||||
# property, which is iterated somewhere in the bowels of
|
||||
# usage.Options.
|
||||
plg = self.config.loadedPlugins[self.config.subCommand]
|
||||
ser = plg.makeService(self.config.subOptions)
|
||||
application = service.Application(plg.tapname)
|
||||
ser.setServiceParent(application)
|
||||
else:
|
||||
passphrase = getPassphrase(self.config['encrypted'])
|
||||
application = getApplication(self.config, passphrase)
|
||||
return application
|
||||
|
||||
|
||||
|
||||
def getApplication(config, passphrase):
|
||||
s = [(config[t], t)
|
||||
for t in ['python', 'source', 'file'] if config[t]][0]
|
||||
filename, style = s[0], {'file': 'pickle'}.get(s[1], s[1])
|
||||
try:
|
||||
log.msg("Loading %s..." % filename)
|
||||
application = service.loadApplication(filename, style, passphrase)
|
||||
log.msg("Loaded.")
|
||||
except Exception as e:
|
||||
s = "Failed to load application: %s" % e
|
||||
if isinstance(e, KeyError) and e.args[0] == "application":
|
||||
s += """
|
||||
Could not find 'application' in the file. To use 'twistd -y', your .tac
|
||||
file must create a suitable object (e.g., by calling service.Application())
|
||||
and store it in a variable named 'application'. twistd loads your .tac file
|
||||
and scans the global variables for one of this name.
|
||||
|
||||
Please read the 'Using Application' HOWTO for details.
|
||||
"""
|
||||
traceback.print_exc(file=log.logfile)
|
||||
log.msg(s)
|
||||
log.deferr()
|
||||
sys.exit('\n' + s + '\n')
|
||||
return application
|
||||
|
||||
|
||||
|
||||
def _reactorAction():
|
||||
return usage.CompleteList([r.shortName for r in
|
||||
reactors.getReactorTypes()])
|
||||
|
||||
|
||||
|
||||
@_oldStyle
|
||||
class ReactorSelectionMixin:
|
||||
"""
|
||||
Provides options for selecting a reactor to install.
|
||||
|
||||
If a reactor is installed, the short name which was used to locate it is
|
||||
saved as the value for the C{"reactor"} key.
|
||||
"""
|
||||
compData = usage.Completions(
|
||||
optActions={"reactor": _reactorAction})
|
||||
|
||||
messageOutput = sys.stdout
|
||||
_getReactorTypes = staticmethod(reactors.getReactorTypes)
|
||||
|
||||
|
||||
def opt_help_reactors(self):
|
||||
"""
|
||||
Display a list of possibly available reactor names.
|
||||
"""
|
||||
rcts = sorted(self._getReactorTypes(), key=attrgetter('shortName'))
|
||||
notWorkingReactors = ""
|
||||
for r in rcts:
|
||||
try:
|
||||
namedModule(r.moduleName)
|
||||
self.messageOutput.write(' %-4s\t%s\n' %
|
||||
(r.shortName, r.description))
|
||||
except ImportError as e:
|
||||
notWorkingReactors += (' !%-4s\t%s (%s)\n' %
|
||||
(r.shortName, r.description, e.args[0]))
|
||||
|
||||
if notWorkingReactors:
|
||||
self.messageOutput.write('\n')
|
||||
self.messageOutput.write(' reactors not available '
|
||||
'on this platform:\n\n')
|
||||
self.messageOutput.write(notWorkingReactors)
|
||||
raise SystemExit(0)
|
||||
|
||||
|
||||
def opt_reactor(self, shortName):
|
||||
"""
|
||||
Which reactor to use (see --help-reactors for a list of possibilities)
|
||||
"""
|
||||
# Actually actually actually install the reactor right at this very
|
||||
# moment, before any other code (for example, a sub-command plugin)
|
||||
# runs and accidentally imports and installs the default reactor.
|
||||
#
|
||||
# This could probably be improved somehow.
|
||||
try:
|
||||
installReactor(shortName)
|
||||
except NoSuchReactor:
|
||||
msg = ("The specified reactor does not exist: '%s'.\n"
|
||||
"See the list of available reactors with "
|
||||
"--help-reactors" % (shortName,))
|
||||
raise usage.UsageError(msg)
|
||||
except Exception as e:
|
||||
msg = ("The specified reactor cannot be used, failed with error: "
|
||||
"%s.\nSee the list of available reactors with "
|
||||
"--help-reactors" % (e,))
|
||||
raise usage.UsageError(msg)
|
||||
else:
|
||||
self["reactor"] = shortName
|
||||
opt_r = opt_reactor
|
||||
|
||||
|
||||
|
||||
class ServerOptions(usage.Options, ReactorSelectionMixin):
|
||||
|
||||
longdesc = ("twistd reads a twisted.application.service.Application out "
|
||||
"of a file and runs it.")
|
||||
|
||||
optFlags = [['savestats', None,
|
||||
"save the Stats object rather than the text output of "
|
||||
"the profiler."],
|
||||
['no_save', 'o', "do not save state on shutdown"],
|
||||
['encrypted', 'e',
|
||||
"The specified tap/aos file is encrypted."]]
|
||||
|
||||
optParameters = [['logfile', 'l', None,
|
||||
"log to a specified file, - for stdout"],
|
||||
['logger', None, None,
|
||||
"A fully-qualified name to a log observer factory to "
|
||||
"use for the initial log observer. Takes precedence "
|
||||
"over --logfile and --syslog (when available)."],
|
||||
['profile', 'p', None,
|
||||
"Run in profile mode, dumping results to specified "
|
||||
"file."],
|
||||
['profiler', None, "cprofile",
|
||||
"Name of the profiler to use (%s)." %
|
||||
", ".join(AppProfiler.profilers)],
|
||||
['file', 'f', 'twistd.tap',
|
||||
"read the given .tap file"],
|
||||
['python', 'y', None,
|
||||
"read an application from within a Python file "
|
||||
"(implies -o)"],
|
||||
['source', 's', None,
|
||||
"Read an application from a .tas file (AOT format)."],
|
||||
['rundir', 'd', '.',
|
||||
'Change to a supplied directory before running']]
|
||||
|
||||
compData = usage.Completions(
|
||||
mutuallyExclusive=[("file", "python", "source")],
|
||||
optActions={"file": usage.CompleteFiles("*.tap"),
|
||||
"python": usage.CompleteFiles("*.(tac|py)"),
|
||||
"source": usage.CompleteFiles("*.tas"),
|
||||
"rundir": usage.CompleteDirs()}
|
||||
)
|
||||
|
||||
_getPlugins = staticmethod(plugin.getPlugins)
|
||||
|
||||
def __init__(self, *a, **kw):
|
||||
self['debug'] = False
|
||||
if 'stdout' in kw:
|
||||
self.stdout = kw['stdout']
|
||||
else:
|
||||
self.stdout = sys.stdout
|
||||
usage.Options.__init__(self)
|
||||
|
||||
|
||||
def opt_debug(self):
|
||||
"""
|
||||
Run the application in the Python Debugger (implies nodaemon),
|
||||
sending SIGUSR2 will drop into debugger
|
||||
"""
|
||||
defer.setDebugging(True)
|
||||
failure.startDebugMode()
|
||||
self['debug'] = True
|
||||
opt_b = opt_debug
|
||||
|
||||
|
||||
def opt_spew(self):
|
||||
"""
|
||||
Print an insanely verbose log of everything that happens.
|
||||
Useful when debugging freezes or locks in complex code.
|
||||
"""
|
||||
sys.settrace(util.spewer)
|
||||
try:
|
||||
import threading
|
||||
except ImportError:
|
||||
return
|
||||
threading.settrace(util.spewer)
|
||||
|
||||
|
||||
def parseOptions(self, options=None):
|
||||
if options is None:
|
||||
options = sys.argv[1:] or ["--help"]
|
||||
usage.Options.parseOptions(self, options)
|
||||
|
||||
|
||||
def postOptions(self):
|
||||
if self.subCommand or self['python']:
|
||||
self['no_save'] = True
|
||||
if self['logger'] is not None:
|
||||
try:
|
||||
self['logger'] = namedAny(self['logger'])
|
||||
except Exception as e:
|
||||
raise usage.UsageError("Logger '%s' could not be imported: %s"
|
||||
% (self['logger'], e))
|
||||
|
||||
|
||||
def subCommands(self):
|
||||
plugins = self._getPlugins(service.IServiceMaker)
|
||||
self.loadedPlugins = {}
|
||||
for plug in sorted(plugins, key=attrgetter('tapname')):
|
||||
self.loadedPlugins[plug.tapname] = plug
|
||||
yield (plug.tapname,
|
||||
None,
|
||||
# Avoid resolving the options attribute right away, in case
|
||||
# it's a property with a non-trivial getter (eg, one which
|
||||
# imports modules).
|
||||
lambda plug=plug: plug.options(),
|
||||
plug.description)
|
||||
subCommands = property(subCommands)
|
||||
|
||||
|
||||
|
||||
def run(runApp, ServerOptions):
|
||||
config = ServerOptions()
|
||||
try:
|
||||
config.parseOptions()
|
||||
except usage.error as ue:
|
||||
print(config)
|
||||
print("%s: %s" % (sys.argv[0], ue))
|
||||
else:
|
||||
runApp(config)
|
||||
|
||||
|
||||
|
||||
def convertStyle(filein, typein, passphrase, fileout, typeout, encrypt):
|
||||
application = service.loadApplication(filein, typein, passphrase)
|
||||
sob.IPersistable(application).setStyle(typeout)
|
||||
passphrase = getSavePassphrase(encrypt)
|
||||
if passphrase:
|
||||
fileout = None
|
||||
sob.IPersistable(application).save(filename=fileout, passphrase=passphrase)
|
||||
|
||||
|
||||
|
||||
def startApplication(application, save):
|
||||
from twisted.internet import reactor
|
||||
service.IService(application).startService()
|
||||
if save:
|
||||
p = sob.IPersistable(application)
|
||||
reactor.addSystemEventTrigger('after', 'shutdown', p.save, 'shutdown')
|
||||
reactor.addSystemEventTrigger('before', 'shutdown',
|
||||
service.IService(application).stopService)
|
||||
|
||||
|
||||
|
||||
def _exitWithSignal(sig):
|
||||
"""
|
||||
Force the application to terminate with the specified signal by replacing
|
||||
the signal handler with the default and sending the signal to ourselves.
|
||||
|
||||
@param sig: Signal to use to terminate the process with C{os.kill}.
|
||||
@type sig: C{int}
|
||||
"""
|
||||
signal.signal(sig, signal.SIG_DFL)
|
||||
os.kill(os.getpid(), sig)
|
||||
1157
venv/lib/python3.9/site-packages/twisted/application/internet.py
Normal file
1157
venv/lib/python3.9/site-packages/twisted/application/internet.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,85 @@
|
|||
# -*- test-case-name: twisted.test.test_application -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Plugin-based system for enumerating available reactors and installing one of
|
||||
them.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division
|
||||
|
||||
from zope.interface import Interface, Attribute, implementer
|
||||
|
||||
from twisted.plugin import IPlugin, getPlugins
|
||||
from twisted.python.reflect import namedAny
|
||||
|
||||
|
||||
class IReactorInstaller(Interface):
|
||||
"""
|
||||
Definition of a reactor which can probably be installed.
|
||||
"""
|
||||
shortName = Attribute("""
|
||||
A brief string giving the user-facing name of this reactor.
|
||||
""")
|
||||
|
||||
description = Attribute("""
|
||||
A longer string giving a user-facing description of this reactor.
|
||||
""")
|
||||
|
||||
def install():
|
||||
"""
|
||||
Install this reactor.
|
||||
"""
|
||||
|
||||
# TODO - A method which provides a best-guess as to whether this reactor
|
||||
# can actually be used in the execution environment.
|
||||
|
||||
|
||||
|
||||
class NoSuchReactor(KeyError):
|
||||
"""
|
||||
Raised when an attempt is made to install a reactor which cannot be found.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@implementer(IPlugin, IReactorInstaller)
|
||||
class Reactor(object):
|
||||
"""
|
||||
@ivar moduleName: The fully-qualified Python name of the module of which
|
||||
the install callable is an attribute.
|
||||
"""
|
||||
def __init__(self, shortName, moduleName, description):
|
||||
self.shortName = shortName
|
||||
self.moduleName = moduleName
|
||||
self.description = description
|
||||
|
||||
|
||||
def install(self):
|
||||
namedAny(self.moduleName).install()
|
||||
|
||||
|
||||
|
||||
def getReactorTypes():
|
||||
"""
|
||||
Return an iterator of L{IReactorInstaller} plugins.
|
||||
"""
|
||||
return getPlugins(IReactorInstaller)
|
||||
|
||||
|
||||
|
||||
def installReactor(shortName):
|
||||
"""
|
||||
Install the reactor with the given C{shortName} attribute.
|
||||
|
||||
@raise NoSuchReactor: If no reactor is found with a matching C{shortName}.
|
||||
|
||||
@raise: anything that the specified reactor can raise when installed.
|
||||
"""
|
||||
for installer in getReactorTypes():
|
||||
if installer.shortName == shortName:
|
||||
installer.install()
|
||||
from twisted.internet import reactor
|
||||
return reactor
|
||||
raise NoSuchReactor(shortName)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# -*- test-case-name: twisted.application.runner.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Facilities for running a Twisted application.
|
||||
"""
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
# -*- test-case-name: twisted.application.runner.test.test_exit -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
System exit support.
|
||||
"""
|
||||
|
||||
from sys import stdout, stderr, exit as sysexit
|
||||
|
||||
from constantly import Values, ValueConstant
|
||||
|
||||
|
||||
|
||||
def exit(status, message=None):
|
||||
"""
|
||||
Exit the python interpreter with the given status and an optional message.
|
||||
|
||||
@param status: An exit status.
|
||||
@type status: L{int} or L{ValueConstant} from L{ExitStatus}.
|
||||
|
||||
@param message: An options message to print.
|
||||
@type status: L{str}
|
||||
"""
|
||||
if isinstance(status, ValueConstant):
|
||||
code = status.value
|
||||
else:
|
||||
code = int(status)
|
||||
|
||||
if message:
|
||||
if code == 0:
|
||||
out = stdout
|
||||
else:
|
||||
out = stderr
|
||||
out.write(message)
|
||||
out.write("\n")
|
||||
|
||||
sysexit(code)
|
||||
|
||||
|
||||
|
||||
try:
|
||||
import posix as Status
|
||||
except ImportError:
|
||||
class Status(object):
|
||||
"""
|
||||
Object to hang C{EX_*} values off of as a substitute for L{posix}.
|
||||
"""
|
||||
EX__BASE = 64
|
||||
|
||||
EX_OK = 0
|
||||
EX_USAGE = EX__BASE
|
||||
EX_DATAERR = EX__BASE + 1
|
||||
EX_NOINPUT = EX__BASE + 2
|
||||
EX_NOUSER = EX__BASE + 3
|
||||
EX_NOHOST = EX__BASE + 4
|
||||
EX_UNAVAILABLE = EX__BASE + 5
|
||||
EX_SOFTWARE = EX__BASE + 6
|
||||
EX_OSERR = EX__BASE + 7
|
||||
EX_OSFILE = EX__BASE + 8
|
||||
EX_CANTCREAT = EX__BASE + 9
|
||||
EX_IOERR = EX__BASE + 10
|
||||
EX_TEMPFAIL = EX__BASE + 11
|
||||
EX_PROTOCOL = EX__BASE + 12
|
||||
EX_NOPERM = EX__BASE + 13
|
||||
EX_CONFIG = EX__BASE + 14
|
||||
|
||||
|
||||
|
||||
class ExitStatus(Values):
|
||||
"""
|
||||
Standard exit status codes for system programs.
|
||||
|
||||
@cvar EX_OK: Successful termination.
|
||||
@type EX_OK: L{ValueConstant}
|
||||
|
||||
@cvar EX_USAGE: Command line usage error.
|
||||
@type EX_USAGE: L{ValueConstant}
|
||||
|
||||
@cvar EX_DATAERR: Data format error.
|
||||
@type EX_DATAERR: L{ValueConstant}
|
||||
|
||||
@cvar EX_NOINPUT: Cannot open input.
|
||||
@type EX_NOINPUT: L{ValueConstant}
|
||||
|
||||
@cvar EX_NOUSER: Addressee unknown.
|
||||
@type EX_NOUSER: L{ValueConstant}
|
||||
|
||||
@cvar EX_NOHOST: Host name unknown.
|
||||
@type EX_NOHOST: L{ValueConstant}
|
||||
|
||||
@cvar EX_UNAVAILABLE: Service unavailable.
|
||||
@type EX_UNAVAILABLE: L{ValueConstant}
|
||||
|
||||
@cvar EX_SOFTWARE: Internal software error.
|
||||
@type EX_SOFTWARE: L{ValueConstant}
|
||||
|
||||
@cvar EX_OSERR: System error (e.g., can't fork).
|
||||
@type EX_OSERR: L{ValueConstant}
|
||||
|
||||
@cvar EX_OSFILE: Critical OS file missing.
|
||||
@type EX_OSFILE: L{ValueConstant}
|
||||
|
||||
@cvar EX_CANTCREAT: Can't create (user) output file.
|
||||
@type EX_CANTCREAT: L{ValueConstant}
|
||||
|
||||
@cvar EX_IOERR: Input/output error.
|
||||
@type EX_IOERR: L{ValueConstant}
|
||||
|
||||
@cvar EX_TEMPFAIL: Temporary failure; the user is invited to retry.
|
||||
@type EX_TEMPFAIL: L{ValueConstant}
|
||||
|
||||
@cvar EX_PROTOCOL: Remote error in protocol.
|
||||
@type EX_PROTOCOL: L{ValueConstant}
|
||||
|
||||
@cvar EX_NOPERM: Permission denied.
|
||||
@type EX_NOPERM: L{ValueConstant}
|
||||
|
||||
@cvar EX_CONFIG: Configuration error.
|
||||
@type EX_CONFIG: L{ValueConstant}
|
||||
"""
|
||||
|
||||
EX_OK = ValueConstant(Status.EX_OK)
|
||||
EX_USAGE = ValueConstant(Status.EX_USAGE)
|
||||
EX_DATAERR = ValueConstant(Status.EX_DATAERR)
|
||||
EX_NOINPUT = ValueConstant(Status.EX_NOINPUT)
|
||||
EX_NOUSER = ValueConstant(Status.EX_NOUSER)
|
||||
EX_NOHOST = ValueConstant(Status.EX_NOHOST)
|
||||
EX_UNAVAILABLE = ValueConstant(Status.EX_UNAVAILABLE)
|
||||
EX_SOFTWARE = ValueConstant(Status.EX_SOFTWARE)
|
||||
EX_OSERR = ValueConstant(Status.EX_OSERR)
|
||||
EX_OSFILE = ValueConstant(Status.EX_OSFILE)
|
||||
EX_CANTCREAT = ValueConstant(Status.EX_CANTCREAT)
|
||||
EX_IOERR = ValueConstant(Status.EX_IOERR)
|
||||
EX_TEMPFAIL = ValueConstant(Status.EX_TEMPFAIL)
|
||||
EX_PROTOCOL = ValueConstant(Status.EX_PROTOCOL)
|
||||
EX_NOPERM = ValueConstant(Status.EX_NOPERM)
|
||||
EX_CONFIG = ValueConstant(Status.EX_CONFIG)
|
||||
|
|
@ -0,0 +1,303 @@
|
|||
# -*- test-case-name: twisted.application.runner.test.test_pidfile -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
PID file.
|
||||
"""
|
||||
|
||||
import errno
|
||||
from os import getpid, kill, name as SYSTEM_NAME
|
||||
|
||||
from zope.interface import Interface, implementer
|
||||
|
||||
from twisted.logger import Logger
|
||||
|
||||
|
||||
|
||||
class IPIDFile(Interface):
|
||||
"""
|
||||
Manages a file that remembers a process ID.
|
||||
"""
|
||||
|
||||
def read():
|
||||
"""
|
||||
Read the process ID stored in this PID file.
|
||||
|
||||
@return: The contained process ID.
|
||||
@rtype: L{int}
|
||||
|
||||
@raise NoPIDFound: If this PID file does not exist.
|
||||
@raise EnvironmentError: If this PID file cannot be read.
|
||||
@raise ValueError: If this PID file's content is invalid.
|
||||
"""
|
||||
|
||||
|
||||
def writeRunningPID():
|
||||
"""
|
||||
Store the PID of the current process in this PID file.
|
||||
|
||||
@raise EnvironmentError: If this PID file cannot be written.
|
||||
"""
|
||||
|
||||
|
||||
def remove():
|
||||
"""
|
||||
Remove this PID file.
|
||||
|
||||
@raise EnvironmentError: If this PID file cannot be removed.
|
||||
"""
|
||||
|
||||
|
||||
def isRunning():
|
||||
"""
|
||||
Determine whether there is a running process corresponding to the PID
|
||||
in this PID file.
|
||||
|
||||
@return: True if this PID file contains a PID and a process with that
|
||||
PID is currently running; false otherwise.
|
||||
@rtype: L{bool}
|
||||
|
||||
@raise EnvironmentError: If this PID file cannot be read.
|
||||
@raise InvalidPIDFileError: If this PID file's content is invalid.
|
||||
@raise StalePIDFileError: If this PID file's content refers to a PID
|
||||
for which there is no corresponding running process.
|
||||
"""
|
||||
|
||||
|
||||
def __enter__():
|
||||
"""
|
||||
Enter a context using this PIDFile.
|
||||
|
||||
Writes the PID file with the PID of the running process.
|
||||
|
||||
@raise AlreadyRunningError: A process corresponding to the PID in this
|
||||
PID file is already running.
|
||||
"""
|
||||
|
||||
|
||||
def __exit__(excType, excValue, traceback):
|
||||
"""
|
||||
Exit a context using this PIDFile.
|
||||
|
||||
Removes the PID file.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@implementer(IPIDFile)
|
||||
class PIDFile(object):
|
||||
"""
|
||||
Concrete implementation of L{IPIDFile} based on C{IFilePath}.
|
||||
|
||||
This implementation is presently not supported on non-POSIX platforms.
|
||||
Specifically, calling L{PIDFile.isRunning} will raise
|
||||
L{NotImplementedError}.
|
||||
"""
|
||||
|
||||
_log = Logger()
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _format(pid):
|
||||
"""
|
||||
Format a PID file's content.
|
||||
|
||||
@param pid: A process ID.
|
||||
@type pid: int
|
||||
|
||||
@return: Formatted PID file contents.
|
||||
@rtype: L{bytes}
|
||||
"""
|
||||
return u"{}\n".format(int(pid)).encode("utf-8")
|
||||
|
||||
|
||||
def __init__(self, filePath):
|
||||
"""
|
||||
@param filePath: The path to the PID file on disk.
|
||||
@type filePath: L{IFilePath}
|
||||
"""
|
||||
self.filePath = filePath
|
||||
|
||||
|
||||
def read(self):
|
||||
pidString = b""
|
||||
try:
|
||||
with self.filePath.open() as fh:
|
||||
for pidString in fh:
|
||||
break
|
||||
except OSError as e:
|
||||
if e.errno == errno.ENOENT: # No such file
|
||||
raise NoPIDFound("PID file does not exist")
|
||||
raise
|
||||
|
||||
try:
|
||||
return int(pidString)
|
||||
except ValueError:
|
||||
raise InvalidPIDFileError(
|
||||
"non-integer PID value in PID file: {!r}".format(pidString)
|
||||
)
|
||||
|
||||
|
||||
def _write(self, pid):
|
||||
"""
|
||||
Store a PID in this PID file.
|
||||
|
||||
@param pid: A PID to store.
|
||||
@type pid: L{int}
|
||||
|
||||
@raise EnvironmentError: If this PID file cannot be written.
|
||||
"""
|
||||
self.filePath.setContent(self._format(pid=pid))
|
||||
|
||||
|
||||
def writeRunningPID(self):
|
||||
self._write(getpid())
|
||||
|
||||
|
||||
def remove(self):
|
||||
self.filePath.remove()
|
||||
|
||||
|
||||
def isRunning(self):
|
||||
try:
|
||||
pid = self.read()
|
||||
except NoPIDFound:
|
||||
return False
|
||||
|
||||
if SYSTEM_NAME == "posix":
|
||||
return self._pidIsRunningPOSIX(pid)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"isRunning is not implemented on {}".format(SYSTEM_NAME)
|
||||
)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _pidIsRunningPOSIX(pid):
|
||||
"""
|
||||
POSIX implementation for running process check.
|
||||
|
||||
Determine whether there is a running process corresponding to the given
|
||||
PID.
|
||||
|
||||
@return: True if the given PID is currently running; false otherwise.
|
||||
@rtype: L{bool}
|
||||
|
||||
@raise EnvironmentError: If this PID file cannot be read.
|
||||
@raise InvalidPIDFileError: If this PID file's content is invalid.
|
||||
@raise StalePIDFileError: If this PID file's content refers to a PID
|
||||
for which there is no corresponding running process.
|
||||
"""
|
||||
try:
|
||||
kill(pid, 0)
|
||||
except OSError as e:
|
||||
if e.errno == errno.ESRCH: # No such process
|
||||
raise StalePIDFileError(
|
||||
"PID file refers to non-existing process"
|
||||
)
|
||||
elif e.errno == errno.EPERM: # Not permitted to kill
|
||||
return True
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def __enter__(self):
|
||||
try:
|
||||
if self.isRunning():
|
||||
raise AlreadyRunningError()
|
||||
except StalePIDFileError:
|
||||
self._log.info("Replacing stale PID file: {log_source}")
|
||||
self.writeRunningPID()
|
||||
return self
|
||||
|
||||
|
||||
def __exit__(self, excType, excValue, traceback):
|
||||
self.remove()
|
||||
|
||||
|
||||
|
||||
@implementer(IPIDFile)
|
||||
class NonePIDFile(object):
|
||||
"""
|
||||
PID file implementation that does nothing.
|
||||
|
||||
This is meant to be used as a "active None" object in place of a PID file
|
||||
when no PID file is desired.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
def read(self):
|
||||
raise NoPIDFound("PID file does not exist")
|
||||
|
||||
|
||||
def _write(self, pid):
|
||||
"""
|
||||
Store a PID in this PID file.
|
||||
|
||||
@param pid: A PID to store.
|
||||
@type pid: L{int}
|
||||
|
||||
@raise EnvironmentError: If this PID file cannot be written.
|
||||
|
||||
@note: This implementation always raises an L{EnvironmentError}.
|
||||
"""
|
||||
raise OSError(errno.EPERM, "Operation not permitted")
|
||||
|
||||
|
||||
def writeRunningPID(self):
|
||||
self._write(0)
|
||||
|
||||
|
||||
def remove(self):
|
||||
raise OSError(errno.ENOENT, "No such file or directory")
|
||||
|
||||
|
||||
def isRunning(self):
|
||||
return False
|
||||
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
|
||||
def __exit__(self, excType, excValue, traceback):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
nonePIDFile = NonePIDFile()
|
||||
|
||||
|
||||
|
||||
class AlreadyRunningError(Exception):
|
||||
"""
|
||||
Process is already running.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class InvalidPIDFileError(Exception):
|
||||
"""
|
||||
PID file contents are invalid.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class StalePIDFileError(Exception):
|
||||
"""
|
||||
PID file contents are valid, but there is no process with the referenced
|
||||
PID.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class NoPIDFound(Exception):
|
||||
"""
|
||||
No PID found in PID file.
|
||||
"""
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
# -*- test-case-name: twisted.application.runner.test.test_runner -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Twisted application runner.
|
||||
"""
|
||||
|
||||
from sys import stderr
|
||||
from signal import SIGTERM
|
||||
from os import kill
|
||||
|
||||
from attr import attrib, attrs, Factory
|
||||
|
||||
from twisted.logger import (
|
||||
globalLogBeginner, textFileLogObserver,
|
||||
FilteringLogObserver, LogLevelFilterPredicate,
|
||||
LogLevel, Logger,
|
||||
)
|
||||
|
||||
from ._exit import exit, ExitStatus
|
||||
from ._pidfile import nonePIDFile, AlreadyRunningError, InvalidPIDFileError
|
||||
|
||||
|
||||
|
||||
@attrs(frozen=True)
|
||||
class Runner(object):
|
||||
"""
|
||||
Twisted application runner.
|
||||
|
||||
@cvar _log: The logger attached to this class.
|
||||
@type _log: L{Logger}
|
||||
|
||||
@ivar _reactor: The reactor to start and run the application in.
|
||||
@type _reactor: L{IReactorCore}
|
||||
|
||||
@ivar _pidFile: The file to store the running process ID in.
|
||||
@type _pidFile: L{IPIDFile}
|
||||
|
||||
@ivar _kill: Whether this runner should kill an existing running
|
||||
instance of the application.
|
||||
@type _kill: L{bool}
|
||||
|
||||
@ivar _defaultLogLevel: The default log level to start the logging
|
||||
system with.
|
||||
@type _defaultLogLevel: L{constantly.NamedConstant} from L{LogLevel}
|
||||
|
||||
@ivar _logFile: A file stream to write logging output to.
|
||||
@type _logFile: writable file-like object
|
||||
|
||||
@ivar _fileLogObserverFactory: A factory for the file log observer to
|
||||
use when starting the logging system.
|
||||
@type _pidFile: callable that takes a single writable file-like object
|
||||
argument and returns a L{twisted.logger.FileLogObserver}
|
||||
|
||||
@ivar _whenRunning: Hook to call after the reactor is running;
|
||||
this is where the application code that relies on the reactor gets
|
||||
called.
|
||||
@type _whenRunning: callable that takes the keyword arguments specified
|
||||
by C{whenRunningArguments}
|
||||
|
||||
@ivar _whenRunningArguments: Keyword arguments to pass to
|
||||
C{whenRunning} when it is called.
|
||||
@type _whenRunningArguments: L{dict}
|
||||
|
||||
@ivar _reactorExited: Hook to call after the reactor exits.
|
||||
@type _reactorExited: callable that takes the keyword arguments
|
||||
specified by C{reactorExitedArguments}
|
||||
|
||||
@ivar _reactorExitedArguments: Keyword arguments to pass to
|
||||
C{reactorExited} when it is called.
|
||||
@type _reactorExitedArguments: L{dict}
|
||||
"""
|
||||
|
||||
_log = Logger()
|
||||
|
||||
_reactor = attrib()
|
||||
_pidFile = attrib(default=nonePIDFile)
|
||||
_kill = attrib(default=False)
|
||||
_defaultLogLevel = attrib(default=LogLevel.info)
|
||||
_logFile = attrib(default=stderr)
|
||||
_fileLogObserverFactory = attrib(default=textFileLogObserver)
|
||||
_whenRunning = attrib(default=lambda **_: None)
|
||||
_whenRunningArguments = attrib(default=Factory(dict))
|
||||
_reactorExited = attrib(default=lambda **_: None)
|
||||
_reactorExitedArguments = attrib(default=Factory(dict))
|
||||
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Run this command.
|
||||
"""
|
||||
pidFile = self._pidFile
|
||||
|
||||
self.killIfRequested()
|
||||
|
||||
try:
|
||||
with pidFile:
|
||||
self.startLogging()
|
||||
self.startReactor()
|
||||
self.reactorExited()
|
||||
|
||||
except AlreadyRunningError:
|
||||
exit(ExitStatus.EX_CONFIG, "Already running.")
|
||||
return # When testing, patched exit doesn't exit
|
||||
|
||||
|
||||
def killIfRequested(self):
|
||||
"""
|
||||
If C{self._kill} is true, attempt to kill a running instance of the
|
||||
application.
|
||||
"""
|
||||
pidFile = self._pidFile
|
||||
|
||||
if self._kill:
|
||||
if pidFile is nonePIDFile:
|
||||
exit(ExitStatus.EX_USAGE, "No PID file specified.")
|
||||
return # When testing, patched exit doesn't exit
|
||||
|
||||
try:
|
||||
pid = pidFile.read()
|
||||
except EnvironmentError:
|
||||
exit(ExitStatus.EX_IOERR, "Unable to read PID file.")
|
||||
return # When testing, patched exit doesn't exit
|
||||
except InvalidPIDFileError:
|
||||
exit(ExitStatus.EX_DATAERR, "Invalid PID file.")
|
||||
return # When testing, patched exit doesn't exit
|
||||
|
||||
self.startLogging()
|
||||
self._log.info("Terminating process: {pid}", pid=pid)
|
||||
|
||||
kill(pid, SIGTERM)
|
||||
|
||||
exit(ExitStatus.EX_OK)
|
||||
return # When testing, patched exit doesn't exit
|
||||
|
||||
|
||||
def startLogging(self):
|
||||
"""
|
||||
Start the L{twisted.logger} logging system.
|
||||
"""
|
||||
logFile = self._logFile
|
||||
|
||||
fileLogObserverFactory = self._fileLogObserverFactory
|
||||
|
||||
fileLogObserver = fileLogObserverFactory(logFile)
|
||||
|
||||
logLevelPredicate = LogLevelFilterPredicate(
|
||||
defaultLogLevel=self._defaultLogLevel
|
||||
)
|
||||
|
||||
filteringObserver = FilteringLogObserver(
|
||||
fileLogObserver, [logLevelPredicate]
|
||||
)
|
||||
|
||||
globalLogBeginner.beginLoggingTo([filteringObserver])
|
||||
|
||||
|
||||
def startReactor(self):
|
||||
"""
|
||||
Register C{self._whenRunning} with the reactor so that it is called
|
||||
once the reactor is running, then start the reactor.
|
||||
"""
|
||||
self._reactor.callWhenRunning(self.whenRunning)
|
||||
|
||||
self._log.info("Starting reactor...")
|
||||
self._reactor.run()
|
||||
|
||||
|
||||
def whenRunning(self):
|
||||
"""
|
||||
Call C{self._whenRunning} with C{self._whenRunningArguments}.
|
||||
|
||||
@note: This method is called after the reactor starts running.
|
||||
"""
|
||||
self._whenRunning(**self._whenRunningArguments)
|
||||
|
||||
|
||||
def reactorExited(self):
|
||||
"""
|
||||
Call C{self._reactorExited} with C{self._reactorExitedArguments}.
|
||||
|
||||
@note: This method is called after the reactor exits.
|
||||
"""
|
||||
self._reactorExited(**self._reactorExitedArguments)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# -*- test-case-name: twisted.application.runner.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.application.runner}.
|
||||
"""
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.application.runner._exit}.
|
||||
"""
|
||||
|
||||
from twisted.python.compat import NativeStringIO
|
||||
from ...runner import _exit
|
||||
from .._exit import exit, ExitStatus
|
||||
|
||||
import twisted.trial.unittest
|
||||
|
||||
|
||||
|
||||
class ExitTests(twisted.trial.unittest.TestCase):
|
||||
"""
|
||||
Tests for L{exit}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.exit = DummyExit()
|
||||
self.patch(_exit, "sysexit", self.exit)
|
||||
|
||||
|
||||
def test_exitStatusInt(self):
|
||||
"""
|
||||
L{exit} given an L{int} status code will pass it to L{sys.exit}.
|
||||
"""
|
||||
status = 1234
|
||||
exit(status)
|
||||
self.assertEqual(self.exit.arg, status)
|
||||
|
||||
|
||||
def test_exitStatusStringNotInt(self):
|
||||
"""
|
||||
L{exit} given a L{str} status code that isn't a string integer raises
|
||||
L{ValueError}.
|
||||
"""
|
||||
self.assertRaises(ValueError, exit, "foo")
|
||||
|
||||
|
||||
def test_exitStatusStringInt(self):
|
||||
"""
|
||||
L{exit} given a L{str} status code that is a string integer passes the
|
||||
corresponding L{int} to L{sys.exit}.
|
||||
"""
|
||||
exit("1234")
|
||||
self.assertEqual(self.exit.arg, 1234)
|
||||
|
||||
|
||||
def test_exitConstant(self):
|
||||
"""
|
||||
L{exit} given a L{ValueConstant} status code passes the corresponding
|
||||
value to L{sys.exit}.
|
||||
"""
|
||||
status = ExitStatus.EX_CONFIG
|
||||
exit(status)
|
||||
self.assertEqual(self.exit.arg, status.value)
|
||||
|
||||
|
||||
def test_exitMessageZero(self):
|
||||
"""
|
||||
L{exit} given a status code of zero (C{0}) writes the given message to
|
||||
standard output.
|
||||
"""
|
||||
out = NativeStringIO()
|
||||
self.patch(_exit, "stdout", out)
|
||||
|
||||
message = "Hello, world."
|
||||
exit(0, message)
|
||||
|
||||
self.assertEqual(out.getvalue(), message + "\n")
|
||||
|
||||
|
||||
def test_exitMessageNonZero(self):
|
||||
"""
|
||||
L{exit} given a non-zero status code writes the given message to
|
||||
standard error.
|
||||
"""
|
||||
out = NativeStringIO()
|
||||
self.patch(_exit, "stderr", out)
|
||||
|
||||
message = "Hello, world."
|
||||
exit(64, message)
|
||||
|
||||
self.assertEqual(out.getvalue(), message + "\n")
|
||||
|
||||
|
||||
|
||||
class DummyExit(object):
|
||||
"""
|
||||
Stub for L{sys.exit} that remembers whether it's been called and, if it
|
||||
has, what argument it was given.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.exited = False
|
||||
|
||||
|
||||
def __call__(self, arg=None):
|
||||
assert not self.exited
|
||||
|
||||
self.arg = arg
|
||||
self.exited = True
|
||||
|
|
@ -0,0 +1,476 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.application.runner._pidfile}.
|
||||
"""
|
||||
|
||||
from functools import wraps
|
||||
import errno
|
||||
from os import getpid, name as SYSTEM_NAME
|
||||
from io import BytesIO
|
||||
|
||||
from zope.interface import implementer
|
||||
from zope.interface.verify import verifyObject
|
||||
|
||||
from twisted.python.filepath import IFilePath
|
||||
from twisted.python.runtime import platform
|
||||
|
||||
from ...runner import _pidfile
|
||||
from .._pidfile import (
|
||||
IPIDFile, PIDFile, NonePIDFile,
|
||||
AlreadyRunningError, InvalidPIDFileError, StalePIDFileError,
|
||||
NoPIDFound,
|
||||
)
|
||||
|
||||
import twisted.trial.unittest
|
||||
from twisted.trial.unittest import SkipTest
|
||||
|
||||
|
||||
def ifPlatformSupported(f):
|
||||
"""
|
||||
Decorator for tests that are not expected to work on all platforms.
|
||||
|
||||
Calling L{PIDFile.isRunning} currently raises L{NotImplementedError} on
|
||||
non-POSIX platforms.
|
||||
|
||||
On an unsupported platform, we expect to see any test that calls
|
||||
L{PIDFile.isRunning} to raise either L{NotImplementedError}, L{SkipTest},
|
||||
or C{self.failureException}.
|
||||
(C{self.failureException} may occur in a test that checks for a specific
|
||||
exception but it gets NotImplementedError instead.)
|
||||
|
||||
@param f: The test method to decorate.
|
||||
@type f: method
|
||||
|
||||
@return: The wrapped callable.
|
||||
"""
|
||||
@wraps(f)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
supported = platform.getType() == "posix"
|
||||
|
||||
if supported:
|
||||
return f(self, *args, **kwargs)
|
||||
else:
|
||||
e = self.assertRaises(
|
||||
(NotImplementedError, SkipTest, self.failureException),
|
||||
f, self, *args, **kwargs
|
||||
)
|
||||
if isinstance(e, NotImplementedError):
|
||||
self.assertTrue(
|
||||
str(e).startswith("isRunning is not implemented on ")
|
||||
)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
|
||||
class PIDFileTests(twisted.trial.unittest.TestCase):
|
||||
"""
|
||||
Tests for L{PIDFile}.
|
||||
"""
|
||||
|
||||
def test_interface(self):
|
||||
"""
|
||||
L{PIDFile} conforms to L{IPIDFile}.
|
||||
"""
|
||||
pidFile = PIDFile(DummyFilePath())
|
||||
verifyObject(IPIDFile, pidFile)
|
||||
|
||||
|
||||
def test_formatWithPID(self):
|
||||
"""
|
||||
L{PIDFile._format} returns the expected format when given a PID.
|
||||
"""
|
||||
self.assertEqual(PIDFile._format(pid=1337), b"1337\n")
|
||||
|
||||
|
||||
def test_readWithPID(self):
|
||||
"""
|
||||
L{PIDFile.read} returns the PID from the given file path.
|
||||
"""
|
||||
pid = 1337
|
||||
|
||||
pidFile = PIDFile(DummyFilePath(PIDFile._format(pid=pid)))
|
||||
|
||||
self.assertEqual(pid, pidFile.read())
|
||||
|
||||
|
||||
def test_readEmptyPID(self):
|
||||
"""
|
||||
L{PIDFile.read} raises L{InvalidPIDFileError} when given an empty file
|
||||
path.
|
||||
"""
|
||||
pidValue = b""
|
||||
pidFile = PIDFile(DummyFilePath(b""))
|
||||
|
||||
e = self.assertRaises(InvalidPIDFileError, pidFile.read)
|
||||
self.assertEqual(
|
||||
str(e),
|
||||
"non-integer PID value in PID file: {!r}".format(pidValue)
|
||||
)
|
||||
|
||||
|
||||
def test_readWithBogusPID(self):
|
||||
"""
|
||||
L{PIDFile.read} raises L{InvalidPIDFileError} when given an empty file
|
||||
path.
|
||||
"""
|
||||
pidValue = b"$foo!"
|
||||
pidFile = PIDFile(DummyFilePath(pidValue))
|
||||
|
||||
e = self.assertRaises(InvalidPIDFileError, pidFile.read)
|
||||
self.assertEqual(
|
||||
str(e),
|
||||
"non-integer PID value in PID file: {!r}".format(pidValue)
|
||||
)
|
||||
|
||||
|
||||
def test_readDoesntExist(self):
|
||||
"""
|
||||
L{PIDFile.read} raises L{NoPIDFound} when given a non-existing file
|
||||
path.
|
||||
"""
|
||||
pidFile = PIDFile(DummyFilePath())
|
||||
|
||||
e = self.assertRaises(NoPIDFound, pidFile.read)
|
||||
self.assertEqual(str(e), "PID file does not exist")
|
||||
|
||||
|
||||
def test_readOpenRaisesOSErrorNotENOENT(self):
|
||||
"""
|
||||
L{PIDFile.read} re-raises L{OSError} if the associated C{errno} is
|
||||
anything other than L{errno.ENOENT}.
|
||||
"""
|
||||
def oops(mode="r"):
|
||||
raise OSError(errno.EIO, "I/O error")
|
||||
|
||||
self.patch(DummyFilePath, "open", oops)
|
||||
|
||||
pidFile = PIDFile(DummyFilePath())
|
||||
|
||||
error = self.assertRaises(OSError, pidFile.read)
|
||||
self.assertEqual(error.errno, errno.EIO)
|
||||
|
||||
|
||||
def test_writePID(self):
|
||||
"""
|
||||
L{PIDFile._write} stores the given PID.
|
||||
"""
|
||||
pid = 1995
|
||||
|
||||
pidFile = PIDFile(DummyFilePath())
|
||||
pidFile._write(pid)
|
||||
|
||||
self.assertEqual(pidFile.read(), pid)
|
||||
|
||||
|
||||
def test_writePIDInvalid(self):
|
||||
"""
|
||||
L{PIDFile._write} raises L{ValueError} when given an invalid PID.
|
||||
"""
|
||||
pidFile = PIDFile(DummyFilePath())
|
||||
|
||||
self.assertRaises(ValueError, pidFile._write, u"burp")
|
||||
|
||||
|
||||
def test_writeRunningPID(self):
|
||||
"""
|
||||
L{PIDFile.writeRunningPID} stores the PID for the current process.
|
||||
"""
|
||||
pidFile = PIDFile(DummyFilePath())
|
||||
pidFile.writeRunningPID()
|
||||
|
||||
self.assertEqual(pidFile.read(), getpid())
|
||||
|
||||
|
||||
def test_remove(self):
|
||||
"""
|
||||
L{PIDFile.remove} removes the PID file.
|
||||
"""
|
||||
pidFile = PIDFile(DummyFilePath(b""))
|
||||
self.assertTrue(pidFile.filePath.exists())
|
||||
|
||||
pidFile.remove()
|
||||
self.assertFalse(pidFile.filePath.exists())
|
||||
|
||||
|
||||
@ifPlatformSupported
|
||||
def test_isRunningDoesExist(self):
|
||||
"""
|
||||
L{PIDFile.isRunning} returns true for a process that does exist.
|
||||
"""
|
||||
pidFile = PIDFile(DummyFilePath())
|
||||
pidFile._write(1337)
|
||||
|
||||
def kill(pid, signal):
|
||||
return # Don't actually kill anything
|
||||
|
||||
self.patch(_pidfile, "kill", kill)
|
||||
|
||||
self.assertTrue(pidFile.isRunning())
|
||||
|
||||
|
||||
@ifPlatformSupported
|
||||
def test_isRunningThis(self):
|
||||
"""
|
||||
L{PIDFile.isRunning} returns true for this process (which is running).
|
||||
|
||||
@note: This differs from L{PIDFileTests.test_isRunningDoesExist} in
|
||||
that it actually invokes the C{kill} system call, which is useful for
|
||||
testing of our chosen method for probing the existence of a process.
|
||||
"""
|
||||
pidFile = PIDFile(DummyFilePath())
|
||||
pidFile.writeRunningPID()
|
||||
|
||||
self.assertTrue(pidFile.isRunning())
|
||||
|
||||
|
||||
@ifPlatformSupported
|
||||
def test_isRunningDoesNotExist(self):
|
||||
"""
|
||||
L{PIDFile.isRunning} raises L{StalePIDFileError} for a process that
|
||||
does not exist (errno=ESRCH).
|
||||
"""
|
||||
pidFile = PIDFile(DummyFilePath())
|
||||
pidFile._write(1337)
|
||||
|
||||
def kill(pid, signal):
|
||||
raise OSError(errno.ESRCH, "No such process")
|
||||
|
||||
self.patch(_pidfile, "kill", kill)
|
||||
|
||||
self.assertRaises(StalePIDFileError, pidFile.isRunning)
|
||||
|
||||
|
||||
@ifPlatformSupported
|
||||
def test_isRunningNotAllowed(self):
|
||||
"""
|
||||
L{PIDFile.isRunning} returns true for a process that we are not allowed
|
||||
to kill (errno=EPERM).
|
||||
"""
|
||||
pidFile = PIDFile(DummyFilePath())
|
||||
pidFile._write(1337)
|
||||
|
||||
def kill(pid, signal):
|
||||
raise OSError(errno.EPERM, "Operation not permitted")
|
||||
|
||||
self.patch(_pidfile, "kill", kill)
|
||||
|
||||
self.assertTrue(pidFile.isRunning())
|
||||
|
||||
|
||||
@ifPlatformSupported
|
||||
def test_isRunningInit(self):
|
||||
"""
|
||||
L{PIDFile.isRunning} returns true for a process that we are not allowed
|
||||
to kill (errno=EPERM).
|
||||
|
||||
@note: This differs from L{PIDFileTests.test_isRunningNotAllowed} in
|
||||
that it actually invokes the C{kill} system call, which is useful for
|
||||
testing of our chosen method for probing the existence of a process
|
||||
that we are not allowed to kill.
|
||||
|
||||
@note: In this case, we try killing C{init}, which is process #1 on
|
||||
POSIX systems, so this test is not portable. C{init} should always be
|
||||
running and should not be killable by non-root users.
|
||||
"""
|
||||
if SYSTEM_NAME != "posix":
|
||||
raise SkipTest("This test assumes POSIX")
|
||||
|
||||
pidFile = PIDFile(DummyFilePath())
|
||||
pidFile._write(1) # PID 1 is init on POSIX systems
|
||||
|
||||
self.assertTrue(pidFile.isRunning())
|
||||
|
||||
|
||||
@ifPlatformSupported
|
||||
def test_isRunningUnknownErrno(self):
|
||||
"""
|
||||
L{PIDFile.isRunning} re-raises L{OSError} if the attached C{errno}
|
||||
value from L{os.kill} is not an expected one.
|
||||
"""
|
||||
pidFile = PIDFile(DummyFilePath())
|
||||
pidFile.writeRunningPID()
|
||||
|
||||
def kill(pid, signal):
|
||||
raise OSError(errno.EEXIST, "File exists")
|
||||
|
||||
self.patch(_pidfile, "kill", kill)
|
||||
|
||||
self.assertRaises(OSError, pidFile.isRunning)
|
||||
|
||||
|
||||
def test_isRunningNoPIDFile(self):
|
||||
"""
|
||||
L{PIDFile.isRunning} returns false if the PID file doesn't exist.
|
||||
"""
|
||||
pidFile = PIDFile(DummyFilePath())
|
||||
|
||||
self.assertFalse(pidFile.isRunning())
|
||||
|
||||
|
||||
def test_contextManager(self):
|
||||
"""
|
||||
When used as a context manager, a L{PIDFile} will store the current pid
|
||||
on entry, then removes the PID file on exit.
|
||||
"""
|
||||
pidFile = PIDFile(DummyFilePath())
|
||||
self.assertFalse(pidFile.filePath.exists())
|
||||
|
||||
with pidFile:
|
||||
self.assertTrue(pidFile.filePath.exists())
|
||||
self.assertEqual(pidFile.read(), getpid())
|
||||
|
||||
self.assertFalse(pidFile.filePath.exists())
|
||||
|
||||
|
||||
@ifPlatformSupported
|
||||
def test_contextManagerDoesntExist(self):
|
||||
"""
|
||||
When used as a context manager, a L{PIDFile} will replace the
|
||||
underlying PIDFile rather than raising L{AlreadyRunningError} if the
|
||||
contained PID file exists but refers to a non-running PID.
|
||||
"""
|
||||
pidFile = PIDFile(DummyFilePath())
|
||||
pidFile._write(1337)
|
||||
|
||||
def kill(pid, signal):
|
||||
raise OSError(errno.ESRCH, "No such process")
|
||||
|
||||
self.patch(_pidfile, "kill", kill)
|
||||
|
||||
e = self.assertRaises(StalePIDFileError, pidFile.isRunning)
|
||||
self.assertEqual(str(e), "PID file refers to non-existing process")
|
||||
|
||||
with pidFile:
|
||||
self.assertEqual(pidFile.read(), getpid())
|
||||
|
||||
|
||||
@ifPlatformSupported
|
||||
def test_contextManagerAlreadyRunning(self):
|
||||
"""
|
||||
When used as a context manager, a L{PIDFile} will raise
|
||||
L{AlreadyRunningError} if the there is already a running process with
|
||||
the contained PID.
|
||||
"""
|
||||
pidFile = PIDFile(DummyFilePath())
|
||||
pidFile._write(1337)
|
||||
|
||||
def kill(pid, signal):
|
||||
return # Don't actually kill anything
|
||||
|
||||
self.patch(_pidfile, "kill", kill)
|
||||
|
||||
self.assertTrue(pidFile.isRunning())
|
||||
|
||||
self.assertRaises(AlreadyRunningError, pidFile.__enter__)
|
||||
|
||||
|
||||
|
||||
class NonePIDFileTests(twisted.trial.unittest.TestCase):
|
||||
"""
|
||||
Tests for L{NonePIDFile}.
|
||||
"""
|
||||
|
||||
def test_interface(self):
|
||||
"""
|
||||
L{NonePIDFile} conforms to L{IPIDFile}.
|
||||
"""
|
||||
pidFile = NonePIDFile()
|
||||
verifyObject(IPIDFile, pidFile)
|
||||
|
||||
|
||||
def test_read(self):
|
||||
"""
|
||||
L{NonePIDFile.read} raises L{NoPIDFound}.
|
||||
"""
|
||||
pidFile = NonePIDFile()
|
||||
|
||||
e = self.assertRaises(NoPIDFound, pidFile.read)
|
||||
self.assertEqual(str(e), "PID file does not exist")
|
||||
|
||||
|
||||
def test_write(self):
|
||||
"""
|
||||
L{NonePIDFile._write} raises L{OSError} with an errno of L{errno.EPERM}.
|
||||
"""
|
||||
pidFile = NonePIDFile()
|
||||
|
||||
error = self.assertRaises(OSError, pidFile._write, 0)
|
||||
self.assertEqual(error.errno, errno.EPERM)
|
||||
|
||||
|
||||
def test_writeRunningPID(self):
|
||||
"""
|
||||
L{NonePIDFile.writeRunningPID} raises L{OSError} with an errno of
|
||||
L{errno.EPERM}.
|
||||
"""
|
||||
pidFile = NonePIDFile()
|
||||
|
||||
error = self.assertRaises(OSError, pidFile.writeRunningPID)
|
||||
self.assertEqual(error.errno, errno.EPERM)
|
||||
|
||||
|
||||
def test_remove(self):
|
||||
"""
|
||||
L{NonePIDFile.remove} raises L{OSError} with an errno of L{errno.EPERM}.
|
||||
"""
|
||||
pidFile = NonePIDFile()
|
||||
|
||||
error = self.assertRaises(OSError, pidFile.remove)
|
||||
self.assertEqual(error.errno, errno.ENOENT)
|
||||
|
||||
|
||||
def test_isRunning(self):
|
||||
"""
|
||||
L{NonePIDFile.isRunning} returns L{False}.
|
||||
"""
|
||||
pidFile = NonePIDFile()
|
||||
|
||||
self.assertEqual(pidFile.isRunning(), False)
|
||||
|
||||
|
||||
def test_contextManager(self):
|
||||
"""
|
||||
When used as a context manager, a L{NonePIDFile} doesn't raise, despite
|
||||
not existing.
|
||||
"""
|
||||
pidFile = NonePIDFile()
|
||||
|
||||
with pidFile:
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@implementer(IFilePath)
|
||||
class DummyFilePath(object):
|
||||
"""
|
||||
In-memory L{IFilePath}.
|
||||
"""
|
||||
|
||||
def __init__(self, content=None):
|
||||
self.setContent(content)
|
||||
|
||||
|
||||
def open(self, mode="r"):
|
||||
if not self._exists:
|
||||
raise OSError(errno.ENOENT, "No such file or directory")
|
||||
return BytesIO(self.getContent())
|
||||
|
||||
|
||||
def setContent(self, content):
|
||||
self._exists = content is not None
|
||||
self._content = content
|
||||
|
||||
|
||||
def getContent(self):
|
||||
return self._content
|
||||
|
||||
|
||||
def remove(self):
|
||||
self.setContent(None)
|
||||
|
||||
|
||||
def exists(self):
|
||||
return self._exists
|
||||
|
|
@ -0,0 +1,460 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.application.runner._runner}.
|
||||
"""
|
||||
|
||||
from signal import SIGTERM
|
||||
from io import BytesIO
|
||||
import errno
|
||||
|
||||
from attr import attrib, attrs, Factory
|
||||
|
||||
from twisted.logger import (
|
||||
LogLevel, LogPublisher, LogBeginner,
|
||||
FileLogObserver, FilteringLogObserver, LogLevelFilterPredicate,
|
||||
)
|
||||
from twisted.internet.testing import MemoryReactor
|
||||
|
||||
from ...runner import _runner
|
||||
from .._exit import ExitStatus
|
||||
from .._pidfile import PIDFile, NonePIDFile
|
||||
from .._runner import Runner
|
||||
from .test_pidfile import DummyFilePath
|
||||
|
||||
import twisted.trial.unittest
|
||||
|
||||
|
||||
|
||||
class RunnerTests(twisted.trial.unittest.TestCase):
|
||||
"""
|
||||
Tests for L{Runner}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
# Patch exit and kill so we can capture usage and prevent actual exits
|
||||
# and kills.
|
||||
|
||||
self.exit = DummyExit()
|
||||
self.kill = DummyKill()
|
||||
|
||||
self.patch(_runner, "exit", self.exit)
|
||||
self.patch(_runner, "kill", self.kill)
|
||||
|
||||
# Patch getpid so we get a known result
|
||||
|
||||
self.pid = 1337
|
||||
self.pidFileContent = u"{}\n".format(self.pid).encode("utf-8")
|
||||
|
||||
# Patch globalLogBeginner so that we aren't trying to install multiple
|
||||
# global log observers.
|
||||
|
||||
self.stdout = BytesIO()
|
||||
self.stderr = BytesIO()
|
||||
self.stdio = DummyStandardIO(self.stdout, self.stderr)
|
||||
self.warnings = DummyWarningsModule()
|
||||
|
||||
self.globalLogPublisher = LogPublisher()
|
||||
self.globalLogBeginner = LogBeginner(
|
||||
self.globalLogPublisher,
|
||||
self.stdio.stderr, self.stdio,
|
||||
self.warnings,
|
||||
)
|
||||
|
||||
self.patch(_runner, "stderr", self.stderr)
|
||||
self.patch(_runner, "globalLogBeginner", self.globalLogBeginner)
|
||||
|
||||
|
||||
def test_runInOrder(self):
|
||||
"""
|
||||
L{Runner.run} calls the expected methods in order.
|
||||
"""
|
||||
runner = DummyRunner(reactor=MemoryReactor())
|
||||
runner.run()
|
||||
|
||||
self.assertEqual(
|
||||
runner.calledMethods,
|
||||
[
|
||||
"killIfRequested",
|
||||
"startLogging",
|
||||
"startReactor",
|
||||
"reactorExited",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_runUsesPIDFile(self):
|
||||
"""
|
||||
L{Runner.run} uses the provided PID file.
|
||||
"""
|
||||
pidFile = DummyPIDFile()
|
||||
|
||||
runner = Runner(reactor=MemoryReactor(), pidFile=pidFile)
|
||||
|
||||
self.assertFalse(pidFile.entered)
|
||||
self.assertFalse(pidFile.exited)
|
||||
|
||||
runner.run()
|
||||
|
||||
self.assertTrue(pidFile.entered)
|
||||
self.assertTrue(pidFile.exited)
|
||||
|
||||
|
||||
def test_runAlreadyRunning(self):
|
||||
"""
|
||||
L{Runner.run} exits with L{ExitStatus.EX_USAGE} and the expected
|
||||
message if a process is already running that corresponds to the given
|
||||
PID file.
|
||||
"""
|
||||
pidFile = PIDFile(DummyFilePath(self.pidFileContent))
|
||||
pidFile.isRunning = lambda: True
|
||||
|
||||
runner = Runner(reactor=MemoryReactor(), pidFile=pidFile)
|
||||
runner.run()
|
||||
|
||||
self.assertEqual(self.exit.status, ExitStatus.EX_CONFIG)
|
||||
self.assertEqual(self.exit.message, "Already running.")
|
||||
|
||||
|
||||
def test_killNotRequested(self):
|
||||
"""
|
||||
L{Runner.killIfRequested} when C{kill} is false doesn't exit and
|
||||
doesn't indiscriminately murder anyone.
|
||||
"""
|
||||
runner = Runner(reactor=MemoryReactor())
|
||||
runner.killIfRequested()
|
||||
|
||||
self.assertEqual(self.kill.calls, [])
|
||||
self.assertFalse(self.exit.exited)
|
||||
|
||||
|
||||
def test_killRequestedWithoutPIDFile(self):
|
||||
"""
|
||||
L{Runner.killIfRequested} when C{kill} is true but C{pidFile} is
|
||||
L{nonePIDFile} exits with L{ExitStatus.EX_USAGE} and the expected
|
||||
message; and also doesn't indiscriminately murder anyone.
|
||||
"""
|
||||
runner = Runner(reactor=MemoryReactor(), kill=True)
|
||||
runner.killIfRequested()
|
||||
|
||||
self.assertEqual(self.kill.calls, [])
|
||||
self.assertEqual(self.exit.status, ExitStatus.EX_USAGE)
|
||||
self.assertEqual(self.exit.message, "No PID file specified.")
|
||||
|
||||
|
||||
def test_killRequestedWithPIDFile(self):
|
||||
"""
|
||||
L{Runner.killIfRequested} when C{kill} is true and given a C{pidFile}
|
||||
performs a targeted killing of the appropriate process.
|
||||
"""
|
||||
pidFile = PIDFile(DummyFilePath(self.pidFileContent))
|
||||
runner = Runner(reactor=MemoryReactor(), kill=True, pidFile=pidFile)
|
||||
runner.killIfRequested()
|
||||
|
||||
self.assertEqual(self.kill.calls, [(self.pid, SIGTERM)])
|
||||
self.assertEqual(self.exit.status, ExitStatus.EX_OK)
|
||||
self.assertIdentical(self.exit.message, None)
|
||||
|
||||
|
||||
def test_killRequestedWithPIDFileCantRead(self):
|
||||
"""
|
||||
L{Runner.killIfRequested} when C{kill} is true and given a C{pidFile}
|
||||
that it can't read exits with L{ExitStatus.EX_IOERR}.
|
||||
"""
|
||||
pidFile = PIDFile(DummyFilePath(None))
|
||||
|
||||
def read():
|
||||
raise OSError(errno.EACCES, "Permission denied")
|
||||
|
||||
pidFile.read = read
|
||||
|
||||
runner = Runner(reactor=MemoryReactor(), kill=True, pidFile=pidFile)
|
||||
runner.killIfRequested()
|
||||
|
||||
self.assertEqual(self.exit.status, ExitStatus.EX_IOERR)
|
||||
self.assertEqual(self.exit.message, "Unable to read PID file.")
|
||||
|
||||
|
||||
def test_killRequestedWithPIDFileEmpty(self):
|
||||
"""
|
||||
L{Runner.killIfRequested} when C{kill} is true and given a C{pidFile}
|
||||
containing no value exits with L{ExitStatus.EX_DATAERR}.
|
||||
"""
|
||||
pidFile = PIDFile(DummyFilePath(b""))
|
||||
runner = Runner(reactor=MemoryReactor(), kill=True, pidFile=pidFile)
|
||||
runner.killIfRequested()
|
||||
|
||||
self.assertEqual(self.exit.status, ExitStatus.EX_DATAERR)
|
||||
self.assertEqual(self.exit.message, "Invalid PID file.")
|
||||
|
||||
|
||||
def test_killRequestedWithPIDFileNotAnInt(self):
|
||||
"""
|
||||
L{Runner.killIfRequested} when C{kill} is true and given a C{pidFile}
|
||||
containing a non-integer value exits with L{ExitStatus.EX_DATAERR}.
|
||||
"""
|
||||
pidFile = PIDFile(DummyFilePath(b"** totally not a number, dude **"))
|
||||
runner = Runner(reactor=MemoryReactor(), kill=True, pidFile=pidFile)
|
||||
runner.killIfRequested()
|
||||
|
||||
self.assertEqual(self.exit.status, ExitStatus.EX_DATAERR)
|
||||
self.assertEqual(self.exit.message, "Invalid PID file.")
|
||||
|
||||
|
||||
def test_startLogging(self):
|
||||
"""
|
||||
L{Runner.startLogging} sets up a filtering observer with a log level
|
||||
predicate set to the given log level that contains a file observer of
|
||||
the given type which writes to the given file.
|
||||
"""
|
||||
logFile = BytesIO()
|
||||
|
||||
# Patch the log beginner so that we don't try to start the already
|
||||
# running (started by trial) logging system.
|
||||
|
||||
class LogBeginner(object):
|
||||
def beginLoggingTo(self, observers):
|
||||
LogBeginner.observers = observers
|
||||
|
||||
self.patch(_runner, "globalLogBeginner", LogBeginner())
|
||||
|
||||
# Patch FilteringLogObserver so we can capture its arguments
|
||||
|
||||
class MockFilteringLogObserver(FilteringLogObserver):
|
||||
def __init__(
|
||||
self, observer, predicates,
|
||||
negativeObserver=lambda event: None
|
||||
):
|
||||
MockFilteringLogObserver.observer = observer
|
||||
MockFilteringLogObserver.predicates = predicates
|
||||
FilteringLogObserver.__init__(
|
||||
self, observer, predicates, negativeObserver
|
||||
)
|
||||
|
||||
self.patch(_runner, "FilteringLogObserver", MockFilteringLogObserver)
|
||||
|
||||
# Patch FileLogObserver so we can capture its arguments
|
||||
|
||||
class MockFileLogObserver(FileLogObserver):
|
||||
def __init__(self, outFile):
|
||||
MockFileLogObserver.outFile = outFile
|
||||
FileLogObserver.__init__(self, outFile, str)
|
||||
|
||||
# Start logging
|
||||
runner = Runner(
|
||||
reactor=MemoryReactor(),
|
||||
defaultLogLevel=LogLevel.critical,
|
||||
logFile=logFile,
|
||||
fileLogObserverFactory=MockFileLogObserver,
|
||||
)
|
||||
runner.startLogging()
|
||||
|
||||
# Check for a filtering observer
|
||||
self.assertEqual(len(LogBeginner.observers), 1)
|
||||
self.assertIsInstance(LogBeginner.observers[0], FilteringLogObserver)
|
||||
|
||||
# Check log level predicate with the correct default log level
|
||||
self.assertEqual(len(MockFilteringLogObserver.predicates), 1)
|
||||
self.assertIsInstance(
|
||||
MockFilteringLogObserver.predicates[0],
|
||||
LogLevelFilterPredicate
|
||||
)
|
||||
self.assertIdentical(
|
||||
MockFilteringLogObserver.predicates[0].defaultLogLevel,
|
||||
LogLevel.critical
|
||||
)
|
||||
|
||||
# Check for a file observer attached to the filtering observer
|
||||
self.assertIsInstance(
|
||||
MockFilteringLogObserver.observer, MockFileLogObserver
|
||||
)
|
||||
|
||||
# Check for the file we gave it
|
||||
self.assertIdentical(
|
||||
MockFilteringLogObserver.observer.outFile, logFile
|
||||
)
|
||||
|
||||
|
||||
def test_startReactorWithReactor(self):
|
||||
"""
|
||||
L{Runner.startReactor} with the C{reactor} argument runs the given
|
||||
reactor.
|
||||
"""
|
||||
reactor = MemoryReactor()
|
||||
runner = Runner(reactor=reactor)
|
||||
runner.startReactor()
|
||||
|
||||
self.assertTrue(reactor.hasRun)
|
||||
|
||||
|
||||
def test_startReactorWhenRunning(self):
|
||||
"""
|
||||
L{Runner.startReactor} ensures that C{whenRunning} is called with
|
||||
C{whenRunningArguments} when the reactor is running.
|
||||
"""
|
||||
self._testHook("whenRunning", "startReactor")
|
||||
|
||||
|
||||
def test_whenRunningWithArguments(self):
|
||||
"""
|
||||
L{Runner.whenRunning} calls C{whenRunning} with
|
||||
C{whenRunningArguments}.
|
||||
"""
|
||||
self._testHook("whenRunning")
|
||||
|
||||
|
||||
def test_reactorExitedWithArguments(self):
|
||||
"""
|
||||
L{Runner.whenRunning} calls C{reactorExited} with
|
||||
C{reactorExitedArguments}.
|
||||
"""
|
||||
self._testHook("reactorExited")
|
||||
|
||||
|
||||
def _testHook(self, methodName, callerName=None):
|
||||
"""
|
||||
Verify that the named hook is run with the expected arguments as
|
||||
specified by the arguments used to create the L{Runner}, when the
|
||||
specified caller is invoked.
|
||||
|
||||
@param methodName: The name of the hook to verify.
|
||||
@type methodName: L{str}
|
||||
|
||||
@param callerName: The name of the method that is expected to cause the
|
||||
hook to be called.
|
||||
If C{None}, use the L{Runner} method with the same name as the
|
||||
hook.
|
||||
@type callerName: L{str}
|
||||
"""
|
||||
if callerName is None:
|
||||
callerName = methodName
|
||||
|
||||
arguments = dict(a=object(), b=object(), c=object())
|
||||
argumentsSeen = []
|
||||
|
||||
def hook(**arguments):
|
||||
argumentsSeen.append(arguments)
|
||||
|
||||
runnerArguments = {
|
||||
methodName: hook,
|
||||
"{}Arguments".format(methodName): arguments.copy(),
|
||||
}
|
||||
runner = Runner(reactor=MemoryReactor(), **runnerArguments)
|
||||
|
||||
hookCaller = getattr(runner, callerName)
|
||||
hookCaller()
|
||||
|
||||
self.assertEqual(len(argumentsSeen), 1)
|
||||
self.assertEqual(argumentsSeen[0], arguments)
|
||||
|
||||
|
||||
|
||||
@attrs(frozen=True)
|
||||
class DummyRunner(Runner):
|
||||
"""
|
||||
Stub for L{Runner}.
|
||||
|
||||
Keep track of calls to some methods without actually doing anything.
|
||||
"""
|
||||
|
||||
calledMethods = attrib(default=Factory(list))
|
||||
|
||||
|
||||
def killIfRequested(self):
|
||||
self.calledMethods.append("killIfRequested")
|
||||
|
||||
|
||||
def startLogging(self):
|
||||
self.calledMethods.append("startLogging")
|
||||
|
||||
|
||||
def startReactor(self):
|
||||
self.calledMethods.append("startReactor")
|
||||
|
||||
|
||||
def reactorExited(self):
|
||||
self.calledMethods.append("reactorExited")
|
||||
|
||||
|
||||
|
||||
class DummyPIDFile(NonePIDFile):
|
||||
"""
|
||||
Stub for L{PIDFile}.
|
||||
|
||||
Tracks context manager entry/exit without doing anything.
|
||||
"""
|
||||
def __init__(self):
|
||||
NonePIDFile.__init__(self)
|
||||
|
||||
self.entered = False
|
||||
self.exited = False
|
||||
|
||||
|
||||
def __enter__(self):
|
||||
self.entered = True
|
||||
return self
|
||||
|
||||
|
||||
def __exit__(self, excType, excValue, traceback):
|
||||
self.exited = True
|
||||
|
||||
|
||||
|
||||
class DummyExit(object):
|
||||
"""
|
||||
Stub for L{exit} that remembers whether it's been called and, if it has,
|
||||
what arguments it was given.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.exited = False
|
||||
|
||||
|
||||
def __call__(self, status, message=None):
|
||||
assert not self.exited
|
||||
|
||||
self.status = status
|
||||
self.message = message
|
||||
self.exited = True
|
||||
|
||||
|
||||
|
||||
class DummyKill(object):
|
||||
"""
|
||||
Stub for L{os.kill} that remembers whether it's been called and, if it has,
|
||||
what arguments it was given.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
|
||||
def __call__(self, pid, sig):
|
||||
self.calls.append((pid, sig))
|
||||
|
||||
|
||||
|
||||
class DummyStandardIO(object):
|
||||
"""
|
||||
Stub for L{sys} which provides L{BytesIO} streams as stdout and stderr.
|
||||
"""
|
||||
|
||||
def __init__(self, stdout, stderr):
|
||||
self.stdout = stdout
|
||||
self.stderr = stderr
|
||||
|
||||
|
||||
|
||||
class DummyWarningsModule(object):
|
||||
"""
|
||||
Stub for L{warnings} which provides a C{showwarning} method that is a no-op.
|
||||
"""
|
||||
|
||||
def showwarning(*args, **kwargs):
|
||||
"""
|
||||
Do nothing.
|
||||
|
||||
@param args: ignored.
|
||||
@param kwargs: ignored.
|
||||
"""
|
||||
424
venv/lib/python3.9/site-packages/twisted/application/service.py
Normal file
424
venv/lib/python3.9/site-packages/twisted/application/service.py
Normal file
|
|
@ -0,0 +1,424 @@
|
|||
# -*- test-case-name: twisted.application.test.test_service -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Service architecture for Twisted.
|
||||
|
||||
Services are arranged in a hierarchy. At the leafs of the hierarchy,
|
||||
the services which actually interact with the outside world are started.
|
||||
Services can be named or anonymous -- usually, they will be named if
|
||||
there is need to access them through the hierarchy (from a parent or
|
||||
a sibling).
|
||||
|
||||
Maintainer: Moshe Zadka
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division
|
||||
|
||||
from zope.interface import implementer, Interface, Attribute
|
||||
|
||||
from twisted.persisted import sob
|
||||
from twisted.python.reflect import namedAny
|
||||
from twisted.python import components
|
||||
from twisted.python._oldstyle import _oldStyle
|
||||
from twisted.internet import defer
|
||||
from twisted.plugin import IPlugin
|
||||
|
||||
|
||||
class IServiceMaker(Interface):
|
||||
"""
|
||||
An object which can be used to construct services in a flexible
|
||||
way.
|
||||
|
||||
This interface should most often be implemented along with
|
||||
L{twisted.plugin.IPlugin}, and will most often be used by the
|
||||
'twistd' command.
|
||||
"""
|
||||
tapname = Attribute(
|
||||
"A short string naming this Twisted plugin, for example 'web' or "
|
||||
"'pencil'. This name will be used as the subcommand of 'twistd'.")
|
||||
|
||||
description = Attribute(
|
||||
"A brief summary of the features provided by this "
|
||||
"Twisted application plugin.")
|
||||
|
||||
options = Attribute(
|
||||
"A C{twisted.python.usage.Options} subclass defining the "
|
||||
"configuration options for this application.")
|
||||
|
||||
|
||||
def makeService(options):
|
||||
"""
|
||||
Create and return an object providing
|
||||
L{twisted.application.service.IService}.
|
||||
|
||||
@param options: A mapping (typically a C{dict} or
|
||||
L{twisted.python.usage.Options} instance) of configuration
|
||||
options to desired configuration values.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@implementer(IPlugin, IServiceMaker)
|
||||
class ServiceMaker(object):
|
||||
"""
|
||||
Utility class to simplify the definition of L{IServiceMaker} plugins.
|
||||
"""
|
||||
def __init__(self, name, module, description, tapname):
|
||||
self.name = name
|
||||
self.module = module
|
||||
self.description = description
|
||||
self.tapname = tapname
|
||||
|
||||
|
||||
def options():
|
||||
def get(self):
|
||||
return namedAny(self.module).Options
|
||||
return get,
|
||||
options = property(*options())
|
||||
|
||||
|
||||
def makeService():
|
||||
def get(self):
|
||||
return namedAny(self.module).makeService
|
||||
return get,
|
||||
makeService = property(*makeService())
|
||||
|
||||
|
||||
|
||||
class IService(Interface):
|
||||
"""
|
||||
A service.
|
||||
|
||||
Run start-up and shut-down code at the appropriate times.
|
||||
"""
|
||||
|
||||
name = Attribute(
|
||||
"A C{str} which is the name of the service or C{None}.")
|
||||
|
||||
running = Attribute(
|
||||
"A C{boolean} which indicates whether the service is running.")
|
||||
|
||||
parent = Attribute(
|
||||
"An C{IServiceCollection} which is the parent or C{None}.")
|
||||
|
||||
def setName(name):
|
||||
"""
|
||||
Set the name of the service.
|
||||
|
||||
@type name: C{str}
|
||||
@raise RuntimeError: Raised if the service already has a parent.
|
||||
"""
|
||||
|
||||
def setServiceParent(parent):
|
||||
"""
|
||||
Set the parent of the service. This method is responsible for setting
|
||||
the C{parent} attribute on this service (the child service).
|
||||
|
||||
@type parent: L{IServiceCollection}
|
||||
@raise RuntimeError: Raised if the service already has a parent
|
||||
or if the service has a name and the parent already has a child
|
||||
by that name.
|
||||
"""
|
||||
|
||||
def disownServiceParent():
|
||||
"""
|
||||
Use this API to remove an L{IService} from an L{IServiceCollection}.
|
||||
|
||||
This method is used symmetrically with L{setServiceParent} in that it
|
||||
sets the C{parent} attribute on the child.
|
||||
|
||||
@rtype: L{Deferred<defer.Deferred>}
|
||||
@return: a L{Deferred<defer.Deferred>} which is triggered when the
|
||||
service has finished shutting down. If shutting down is immediate,
|
||||
a value can be returned (usually, L{None}).
|
||||
"""
|
||||
|
||||
def startService():
|
||||
"""
|
||||
Start the service.
|
||||
"""
|
||||
|
||||
def stopService():
|
||||
"""
|
||||
Stop the service.
|
||||
|
||||
@rtype: L{Deferred<defer.Deferred>}
|
||||
@return: a L{Deferred<defer.Deferred>} which is triggered when the
|
||||
service has finished shutting down. If shutting down is immediate,
|
||||
a value can be returned (usually, L{None}).
|
||||
"""
|
||||
|
||||
def privilegedStartService():
|
||||
"""
|
||||
Do preparation work for starting the service.
|
||||
|
||||
Here things which should be done before changing directory,
|
||||
root or shedding privileges are done.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@implementer(IService)
|
||||
class Service(object):
|
||||
"""
|
||||
Base class for services.
|
||||
|
||||
Most services should inherit from this class. It handles the
|
||||
book-keeping responsibilities of starting and stopping, as well
|
||||
as not serializing this book-keeping information.
|
||||
"""
|
||||
|
||||
running = 0
|
||||
name = None
|
||||
parent = None
|
||||
|
||||
def __getstate__(self):
|
||||
dict = self.__dict__.copy()
|
||||
if "running" in dict:
|
||||
del dict['running']
|
||||
return dict
|
||||
|
||||
def setName(self, name):
|
||||
if self.parent is not None:
|
||||
raise RuntimeError("cannot change name when parent exists")
|
||||
self.name = name
|
||||
|
||||
def setServiceParent(self, parent):
|
||||
if self.parent is not None:
|
||||
self.disownServiceParent()
|
||||
parent = IServiceCollection(parent, parent)
|
||||
self.parent = parent
|
||||
self.parent.addService(self)
|
||||
|
||||
def disownServiceParent(self):
|
||||
d = self.parent.removeService(self)
|
||||
self.parent = None
|
||||
return d
|
||||
|
||||
def privilegedStartService(self):
|
||||
pass
|
||||
|
||||
def startService(self):
|
||||
self.running = 1
|
||||
|
||||
def stopService(self):
|
||||
self.running = 0
|
||||
|
||||
|
||||
|
||||
class IServiceCollection(Interface):
|
||||
"""
|
||||
Collection of services.
|
||||
|
||||
Contain several services, and manage their start-up/shut-down.
|
||||
Services can be accessed by name if they have a name, and it
|
||||
is always possible to iterate over them.
|
||||
"""
|
||||
|
||||
def getServiceNamed(name):
|
||||
"""
|
||||
Get the child service with a given name.
|
||||
|
||||
@type name: C{str}
|
||||
@rtype: L{IService}
|
||||
@raise KeyError: Raised if the service has no child with the
|
||||
given name.
|
||||
"""
|
||||
|
||||
def __iter__():
|
||||
"""
|
||||
Get an iterator over all child services.
|
||||
"""
|
||||
|
||||
def addService(service):
|
||||
"""
|
||||
Add a child service.
|
||||
|
||||
Only implementations of L{IService.setServiceParent} should use this
|
||||
method.
|
||||
|
||||
@type service: L{IService}
|
||||
@raise RuntimeError: Raised if the service has a child with
|
||||
the given name.
|
||||
"""
|
||||
|
||||
def removeService(service):
|
||||
"""
|
||||
Remove a child service.
|
||||
|
||||
Only implementations of L{IService.disownServiceParent} should
|
||||
use this method.
|
||||
|
||||
@type service: L{IService}
|
||||
@raise ValueError: Raised if the given service is not a child.
|
||||
@rtype: L{Deferred<defer.Deferred>}
|
||||
@return: a L{Deferred<defer.Deferred>} which is triggered when the
|
||||
service has finished shutting down. If shutting down is immediate,
|
||||
a value can be returned (usually, L{None}).
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@implementer(IServiceCollection)
|
||||
class MultiService(Service):
|
||||
"""
|
||||
Straightforward Service Container.
|
||||
|
||||
Hold a collection of services, and manage them in a simplistic
|
||||
way. No service will wait for another, but this object itself
|
||||
will not finish shutting down until all of its child services
|
||||
will finish.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.services = []
|
||||
self.namedServices = {}
|
||||
self.parent = None
|
||||
|
||||
def privilegedStartService(self):
|
||||
Service.privilegedStartService(self)
|
||||
for service in self:
|
||||
service.privilegedStartService()
|
||||
|
||||
def startService(self):
|
||||
Service.startService(self)
|
||||
for service in self:
|
||||
service.startService()
|
||||
|
||||
def stopService(self):
|
||||
Service.stopService(self)
|
||||
l = []
|
||||
services = list(self)
|
||||
services.reverse()
|
||||
for service in services:
|
||||
l.append(defer.maybeDeferred(service.stopService))
|
||||
return defer.DeferredList(l)
|
||||
|
||||
def getServiceNamed(self, name):
|
||||
return self.namedServices[name]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.services)
|
||||
|
||||
def addService(self, service):
|
||||
if service.name is not None:
|
||||
if service.name in self.namedServices:
|
||||
raise RuntimeError("cannot have two services with same name"
|
||||
" '%s'" % service.name)
|
||||
self.namedServices[service.name] = service
|
||||
self.services.append(service)
|
||||
if self.running:
|
||||
# It may be too late for that, but we will do our best
|
||||
service.privilegedStartService()
|
||||
service.startService()
|
||||
|
||||
def removeService(self, service):
|
||||
if service.name:
|
||||
del self.namedServices[service.name]
|
||||
self.services.remove(service)
|
||||
if self.running:
|
||||
# Returning this so as not to lose information from the
|
||||
# MultiService.stopService deferred.
|
||||
return service.stopService()
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
|
||||
class IProcess(Interface):
|
||||
"""
|
||||
Process running parameters.
|
||||
|
||||
Represents parameters for how processes should be run.
|
||||
"""
|
||||
processName = Attribute(
|
||||
"""
|
||||
A C{str} giving the name the process should have in ps (or L{None}
|
||||
to leave the name alone).
|
||||
""")
|
||||
|
||||
uid = Attribute(
|
||||
"""
|
||||
An C{int} giving the user id as which the process should run (or
|
||||
L{None} to leave the UID alone).
|
||||
""")
|
||||
|
||||
gid = Attribute(
|
||||
"""
|
||||
An C{int} giving the group id as which the process should run (or
|
||||
L{None} to leave the GID alone).
|
||||
""")
|
||||
|
||||
|
||||
|
||||
@implementer(IProcess)
|
||||
@_oldStyle
|
||||
class Process:
|
||||
"""
|
||||
Process running parameters.
|
||||
|
||||
Sets up uid/gid in the constructor, and has a default
|
||||
of L{None} as C{processName}.
|
||||
"""
|
||||
processName = None
|
||||
|
||||
def __init__(self, uid=None, gid=None):
|
||||
"""
|
||||
Set uid and gid.
|
||||
|
||||
@param uid: The user ID as whom to execute the process. If
|
||||
this is L{None}, no attempt will be made to change the UID.
|
||||
|
||||
@param gid: The group ID as whom to execute the process. If
|
||||
this is L{None}, no attempt will be made to change the GID.
|
||||
"""
|
||||
self.uid = uid
|
||||
self.gid = gid
|
||||
|
||||
|
||||
|
||||
def Application(name, uid=None, gid=None):
|
||||
"""
|
||||
Return a compound class.
|
||||
|
||||
Return an object supporting the L{IService}, L{IServiceCollection},
|
||||
L{IProcess} and L{sob.IPersistable} interfaces, with the given
|
||||
parameters. Always access the return value by explicit casting to
|
||||
one of the interfaces.
|
||||
"""
|
||||
ret = components.Componentized()
|
||||
availableComponents = [MultiService(), Process(uid, gid),
|
||||
sob.Persistent(ret, name)]
|
||||
|
||||
for comp in availableComponents:
|
||||
ret.addComponent(comp, ignoreClass=1)
|
||||
IService(ret).setName(name)
|
||||
return ret
|
||||
|
||||
|
||||
|
||||
def loadApplication(filename, kind, passphrase=None):
|
||||
"""
|
||||
Load Application from a given file.
|
||||
|
||||
The serialization format it was saved in should be given as
|
||||
C{kind}, and is one of C{pickle}, C{source}, C{xml} or C{python}. If
|
||||
C{passphrase} is given, the application was encrypted with the
|
||||
given passphrase.
|
||||
|
||||
@type filename: C{str}
|
||||
@type kind: C{str}
|
||||
@type passphrase: C{str}
|
||||
"""
|
||||
if kind == 'python':
|
||||
application = sob.loadValueFromFile(filename, 'application')
|
||||
else:
|
||||
application = sob.load(filename, kind)
|
||||
return application
|
||||
|
||||
|
||||
__all__ = ['IServiceMaker', 'IService', 'Service',
|
||||
'IServiceCollection', 'MultiService',
|
||||
'IProcess', 'Process', 'Application', 'loadApplication']
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
# -*- test-case-name: twisted.test.test_strports -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Construct listening port services from a simple string description.
|
||||
|
||||
@see: L{twisted.internet.endpoints.serverFromString}
|
||||
@see: L{twisted.internet.endpoints.clientFromString}
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division
|
||||
|
||||
from twisted.application.internet import StreamServerEndpointService
|
||||
from twisted.internet import endpoints
|
||||
|
||||
|
||||
def service(description, factory, reactor=None):
|
||||
"""
|
||||
Return the service corresponding to a description.
|
||||
|
||||
@param description: The description of the listening port, in the syntax
|
||||
described by L{twisted.internet.endpoints.serverFromString}.
|
||||
@type description: C{str}
|
||||
|
||||
@param factory: The protocol factory which will build protocols for
|
||||
connections to this service.
|
||||
@type factory: L{twisted.internet.interfaces.IProtocolFactory}
|
||||
|
||||
@rtype: C{twisted.application.service.IService}
|
||||
@return: the service corresponding to a description of a reliable stream
|
||||
server.
|
||||
|
||||
@see: L{twisted.internet.endpoints.serverFromString}
|
||||
"""
|
||||
if reactor is None:
|
||||
from twisted.internet import reactor
|
||||
|
||||
svc = StreamServerEndpointService(
|
||||
endpoints.serverFromString(reactor, description), factory)
|
||||
svc._raiseSynchronously = True
|
||||
return svc
|
||||
|
||||
|
||||
|
||||
def listen(description, factory):
|
||||
"""
|
||||
Listen on a port corresponding to a description.
|
||||
|
||||
@param description: The description of the connecting port, in the syntax
|
||||
described by L{twisted.internet.endpoints.serverFromString}.
|
||||
@type description: L{str}
|
||||
|
||||
@param factory: The protocol factory which will build protocols on
|
||||
connection.
|
||||
@type factory: L{twisted.internet.interfaces.IProtocolFactory}
|
||||
|
||||
@rtype: L{twisted.internet.interfaces.IListeningPort}
|
||||
@return: the port corresponding to a description of a reliable virtual
|
||||
circuit server.
|
||||
|
||||
@see: L{twisted.internet.endpoints.serverFromString}
|
||||
"""
|
||||
from twisted.internet import reactor
|
||||
name, args, kw = endpoints._parseServer(description, factory)
|
||||
return getattr(reactor, 'listen' + name)(*args, **kw)
|
||||
|
||||
|
||||
|
||||
__all__ = ['service', 'listen']
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.internet.application}.
|
||||
"""
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,188 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.application.service}.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division
|
||||
|
||||
from zope.interface import implementer
|
||||
from zope.interface.exceptions import BrokenImplementation
|
||||
from zope.interface.verify import verifyObject
|
||||
|
||||
from twisted.persisted.sob import IPersistable
|
||||
from twisted.application.service import Application, IProcess
|
||||
from twisted.application.service import IService, IServiceCollection
|
||||
from twisted.application.service import Service
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
|
||||
@implementer(IService)
|
||||
class AlmostService(object):
|
||||
"""
|
||||
Implement IService in a way that can fail.
|
||||
|
||||
In general, classes should maintain invariants that adhere
|
||||
to the interfaces that they claim to implement --
|
||||
otherwise, it is a bug.
|
||||
|
||||
This is a buggy class -- the IService implementation is fragile,
|
||||
and several methods will break it. These bugs are intentional,
|
||||
as the tests trigger them -- and then check that the class,
|
||||
indeed, no longer complies with the interface (IService)
|
||||
that it claims to comply with.
|
||||
|
||||
Since the verification will, by definition, only fail on buggy classes --
|
||||
in other words, those which do not actually support the interface they
|
||||
claim to support, we have to write a buggy class to properly verify
|
||||
the interface.
|
||||
"""
|
||||
|
||||
def __init__(self, name, parent, running):
|
||||
self.name = name
|
||||
self.parent = parent
|
||||
self.running = running
|
||||
|
||||
|
||||
def makeInvalidByDeletingName(self):
|
||||
"""
|
||||
Probably not a wise method to call.
|
||||
|
||||
This method removes the :code:`name` attribute,
|
||||
which has to exist in IService classes.
|
||||
"""
|
||||
del self.name
|
||||
|
||||
|
||||
def makeInvalidByDeletingParent(self):
|
||||
"""
|
||||
Probably not a wise method to call.
|
||||
|
||||
This method removes the :code:`parent` attribute,
|
||||
which has to exist in IService classes.
|
||||
"""
|
||||
del self.parent
|
||||
|
||||
|
||||
def makeInvalidByDeletingRunning(self):
|
||||
"""
|
||||
Probably not a wise method to call.
|
||||
|
||||
This method removes the :code:`running` attribute,
|
||||
which has to exist in IService classes.
|
||||
"""
|
||||
del self.running
|
||||
|
||||
|
||||
def setName(self, name):
|
||||
"""
|
||||
See L{twisted.application.service.IService}.
|
||||
|
||||
@param name: ignored
|
||||
"""
|
||||
|
||||
|
||||
def setServiceParent(self, parent):
|
||||
"""
|
||||
See L{twisted.application.service.IService}.
|
||||
|
||||
@param parent: ignored
|
||||
"""
|
||||
|
||||
|
||||
def disownServiceParent(self):
|
||||
"""
|
||||
See L{twisted.application.service.IService}.
|
||||
"""
|
||||
|
||||
|
||||
def privilegedStartService(self):
|
||||
"""
|
||||
See L{twisted.application.service.IService}.
|
||||
"""
|
||||
|
||||
|
||||
def startService(self):
|
||||
"""
|
||||
See L{twisted.application.service.IService}.
|
||||
"""
|
||||
|
||||
|
||||
def stopService(self):
|
||||
"""
|
||||
See L{twisted.application.service.IService}.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class ServiceInterfaceTests(TestCase):
|
||||
"""
|
||||
Tests for L{twisted.application.service.IService} implementation.
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
Build something that implements IService.
|
||||
"""
|
||||
self.almostService = AlmostService(parent=None, running=False,
|
||||
name=None)
|
||||
|
||||
|
||||
def test_realService(self):
|
||||
"""
|
||||
Service implements IService.
|
||||
"""
|
||||
myService = Service()
|
||||
verifyObject(IService, myService)
|
||||
|
||||
|
||||
def test_hasAll(self):
|
||||
"""
|
||||
AlmostService implements IService.
|
||||
"""
|
||||
verifyObject(IService, self.almostService)
|
||||
|
||||
|
||||
def test_noName(self):
|
||||
"""
|
||||
AlmostService with no name does not implement IService.
|
||||
"""
|
||||
self.almostService.makeInvalidByDeletingName()
|
||||
with self.assertRaises(BrokenImplementation):
|
||||
verifyObject(IService, self.almostService)
|
||||
|
||||
|
||||
def test_noParent(self):
|
||||
"""
|
||||
AlmostService with no parent does not implement IService.
|
||||
"""
|
||||
self.almostService.makeInvalidByDeletingParent()
|
||||
with self.assertRaises(BrokenImplementation):
|
||||
verifyObject(IService, self.almostService)
|
||||
|
||||
|
||||
def test_noRunning(self):
|
||||
"""
|
||||
AlmostService with no running does not implement IService.
|
||||
"""
|
||||
self.almostService.makeInvalidByDeletingRunning()
|
||||
with self.assertRaises(BrokenImplementation):
|
||||
verifyObject(IService, self.almostService)
|
||||
|
||||
|
||||
|
||||
class ApplicationTests(TestCase):
|
||||
"""
|
||||
Tests for L{twisted.application.service.Application}.
|
||||
"""
|
||||
def test_applicationComponents(self):
|
||||
"""
|
||||
Check L{twisted.application.service.Application} instantiation.
|
||||
"""
|
||||
app = Application('app-name')
|
||||
|
||||
self.assertTrue(verifyObject(IService, IService(app)))
|
||||
self.assertTrue(
|
||||
verifyObject(IServiceCollection, IServiceCollection(app)))
|
||||
self.assertTrue(verifyObject(IProcess, IProcess(app)))
|
||||
self.assertTrue(verifyObject(IPersistable, IPersistable(app)))
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# -*- test-case-name: twisted.application.twist.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
C{twist} command line tool.
|
||||
"""
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
# -*- test-case-name: twisted.application.twist.test.test_options -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Command line options for C{twist}.
|
||||
"""
|
||||
|
||||
from sys import stdout, stderr
|
||||
from textwrap import dedent
|
||||
|
||||
from twisted.copyright import version
|
||||
from twisted.python.usage import Options, UsageError
|
||||
from twisted.logger import (
|
||||
LogLevel, InvalidLogLevelError,
|
||||
textFileLogObserver, jsonFileLogObserver,
|
||||
)
|
||||
from twisted.plugin import getPlugins
|
||||
|
||||
from ..reactors import installReactor, NoSuchReactor, getReactorTypes
|
||||
from ..runner._exit import exit, ExitStatus
|
||||
from ..service import IServiceMaker
|
||||
|
||||
openFile = open
|
||||
|
||||
|
||||
|
||||
class TwistOptions(Options):
|
||||
"""
|
||||
Command line options for C{twist}.
|
||||
"""
|
||||
|
||||
defaultReactorName = "default"
|
||||
defaultLogLevel = LogLevel.info
|
||||
|
||||
|
||||
def __init__(self):
|
||||
Options.__init__(self)
|
||||
|
||||
self["reactorName"] = self.defaultReactorName
|
||||
self["logLevel"] = self.defaultLogLevel
|
||||
self["logFile"] = stdout
|
||||
|
||||
|
||||
def getSynopsis(self):
|
||||
return "{} plugin [plugin_options]".format(
|
||||
Options.getSynopsis(self)
|
||||
)
|
||||
|
||||
|
||||
def opt_version(self):
|
||||
"""
|
||||
Print version and exit.
|
||||
"""
|
||||
exit(ExitStatus.EX_OK, "{}".format(version))
|
||||
|
||||
|
||||
def opt_reactor(self, name):
|
||||
"""
|
||||
The name of the reactor to use.
|
||||
(options: {options})
|
||||
"""
|
||||
# Actually actually actually install the reactor right at this very
|
||||
# moment, before any other code (for example, a sub-command plugin)
|
||||
# runs and accidentally imports and installs the default reactor.
|
||||
try:
|
||||
self["reactor"] = self.installReactor(name)
|
||||
except NoSuchReactor:
|
||||
raise UsageError("Unknown reactor: {}".format(name))
|
||||
else:
|
||||
self["reactorName"] = name
|
||||
|
||||
opt_reactor.__doc__ = dedent(opt_reactor.__doc__).format(
|
||||
options=", ".join(
|
||||
'"{}"'.format(rt.shortName) for rt in getReactorTypes()
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def installReactor(self, name):
|
||||
"""
|
||||
Install the reactor.
|
||||
"""
|
||||
if name == self.defaultReactorName:
|
||||
from twisted.internet import reactor
|
||||
return reactor
|
||||
else:
|
||||
return installReactor(name)
|
||||
|
||||
|
||||
def opt_log_level(self, levelName):
|
||||
"""
|
||||
Set default log level.
|
||||
(options: {options}; default: "{default}")
|
||||
"""
|
||||
try:
|
||||
self["logLevel"] = LogLevel.levelWithName(levelName)
|
||||
except InvalidLogLevelError:
|
||||
raise UsageError("Invalid log level: {}".format(levelName))
|
||||
|
||||
opt_log_level.__doc__ = dedent(opt_log_level.__doc__).format(
|
||||
options=", ".join(
|
||||
'"{}"'.format(l.name) for l in LogLevel.iterconstants()
|
||||
),
|
||||
default=defaultLogLevel.name,
|
||||
)
|
||||
|
||||
|
||||
def opt_log_file(self, fileName):
|
||||
"""
|
||||
Log to file. ("-" for stdout, "+" for stderr; default: "-")
|
||||
"""
|
||||
if fileName == "-":
|
||||
self["logFile"] = stdout
|
||||
return
|
||||
|
||||
if fileName == "+":
|
||||
self["logFile"] = stderr
|
||||
return
|
||||
|
||||
try:
|
||||
self["logFile"] = openFile(fileName, "a")
|
||||
except EnvironmentError as e:
|
||||
exit(
|
||||
ExitStatus.EX_IOERR,
|
||||
"Unable to open log file {!r}: {}".format(fileName, e)
|
||||
)
|
||||
|
||||
|
||||
def opt_log_format(self, format):
|
||||
"""
|
||||
Log file format.
|
||||
(options: "text", "json"; default: "text" if the log file is a tty,
|
||||
otherwise "json")
|
||||
"""
|
||||
format = format.lower()
|
||||
|
||||
if format == "text":
|
||||
self["fileLogObserverFactory"] = textFileLogObserver
|
||||
elif format == "json":
|
||||
self["fileLogObserverFactory"] = jsonFileLogObserver
|
||||
else:
|
||||
raise UsageError("Invalid log format: {}".format(format))
|
||||
self["logFormat"] = format
|
||||
|
||||
opt_log_format.__doc__ = dedent(opt_log_format.__doc__)
|
||||
|
||||
|
||||
def selectDefaultLogObserver(self):
|
||||
"""
|
||||
Set C{fileLogObserverFactory} to the default appropriate for the
|
||||
chosen C{logFile}.
|
||||
"""
|
||||
if "fileLogObserverFactory" not in self:
|
||||
logFile = self["logFile"]
|
||||
|
||||
if hasattr(logFile, "isatty") and logFile.isatty():
|
||||
self["fileLogObserverFactory"] = textFileLogObserver
|
||||
self["logFormat"] = "text"
|
||||
else:
|
||||
self["fileLogObserverFactory"] = jsonFileLogObserver
|
||||
self["logFormat"] = "json"
|
||||
|
||||
|
||||
def parseOptions(self, options=None):
|
||||
self.selectDefaultLogObserver()
|
||||
|
||||
Options.parseOptions(self, options=options)
|
||||
|
||||
if "reactor" not in self:
|
||||
self["reactor"] = self.installReactor(self["reactorName"])
|
||||
|
||||
|
||||
@property
|
||||
def plugins(self):
|
||||
if "plugins" not in self:
|
||||
plugins = {}
|
||||
for plugin in getPlugins(IServiceMaker):
|
||||
plugins[plugin.tapname] = plugin
|
||||
self["plugins"] = plugins
|
||||
|
||||
return self["plugins"]
|
||||
|
||||
|
||||
@property
|
||||
def subCommands(self):
|
||||
plugins = self.plugins
|
||||
for name in sorted(plugins):
|
||||
plugin = plugins[name]
|
||||
yield (
|
||||
plugin.tapname,
|
||||
None,
|
||||
# Avoid resolving the options attribute right away, in case
|
||||
# it's a property with a non-trivial getter (eg, one which
|
||||
# imports modules).
|
||||
lambda plugin=plugin: plugin.options(),
|
||||
plugin.description,
|
||||
)
|
||||
|
||||
|
||||
def postOptions(self):
|
||||
Options.postOptions(self)
|
||||
|
||||
if self.subCommand is None:
|
||||
raise UsageError("No plugin specified.")
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
# -*- test-case-name: twisted.application.twist.test.test_twist -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Run a Twisted application.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from twisted.python.usage import UsageError
|
||||
from ..service import Application, IService
|
||||
from ..runner._exit import exit, ExitStatus
|
||||
from ..runner._runner import Runner
|
||||
from ._options import TwistOptions
|
||||
from twisted.application.app import _exitWithSignal
|
||||
from twisted.internet.interfaces import _ISupportsExitSignalCapturing
|
||||
|
||||
|
||||
|
||||
class Twist(object):
|
||||
"""
|
||||
Run a Twisted application.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def options(argv):
|
||||
"""
|
||||
Parse command line options.
|
||||
|
||||
@param argv: Command line arguments.
|
||||
@type argv: L{list}
|
||||
|
||||
@return: The parsed options.
|
||||
@rtype: L{TwistOptions}
|
||||
"""
|
||||
options = TwistOptions()
|
||||
|
||||
try:
|
||||
options.parseOptions(argv[1:])
|
||||
except UsageError as e:
|
||||
exit(ExitStatus.EX_USAGE, "Error: {}\n\n{}".format(e, options))
|
||||
|
||||
return options
|
||||
|
||||
|
||||
@staticmethod
|
||||
def service(plugin, options):
|
||||
"""
|
||||
Create the application service.
|
||||
|
||||
@param plugin: The name of the plugin that implements the service
|
||||
application to run.
|
||||
@type plugin: L{str}
|
||||
|
||||
@param options: Options to pass to the application.
|
||||
@type options: L{twisted.python.usage.Options}
|
||||
|
||||
@return: The created application service.
|
||||
@rtype: L{IService}
|
||||
"""
|
||||
service = plugin.makeService(options)
|
||||
application = Application(plugin.tapname)
|
||||
service.setServiceParent(application)
|
||||
|
||||
return IService(application)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def startService(reactor, service):
|
||||
"""
|
||||
Start the application service.
|
||||
|
||||
@param reactor: The reactor to run the service with.
|
||||
@type reactor: L{twisted.internet.interfaces.IReactorCore}
|
||||
|
||||
@param service: The application service to run.
|
||||
@type service: L{IService}
|
||||
"""
|
||||
service.startService()
|
||||
|
||||
# Ask the reactor to stop the service before shutting down
|
||||
reactor.addSystemEventTrigger(
|
||||
"before", "shutdown", service.stopService
|
||||
)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def run(twistOptions):
|
||||
"""
|
||||
Run the application service.
|
||||
|
||||
@param twistOptions: Command line options to convert to runner
|
||||
arguments.
|
||||
@type twistOptions: L{TwistOptions}
|
||||
"""
|
||||
runner = Runner(
|
||||
reactor=twistOptions["reactor"],
|
||||
defaultLogLevel=twistOptions["logLevel"],
|
||||
logFile=twistOptions["logFile"],
|
||||
fileLogObserverFactory=twistOptions["fileLogObserverFactory"],
|
||||
)
|
||||
runner.run()
|
||||
reactor = twistOptions["reactor"]
|
||||
if _ISupportsExitSignalCapturing.providedBy(reactor):
|
||||
if reactor._exitSignal is not None:
|
||||
_exitWithSignal(reactor._exitSignal)
|
||||
|
||||
|
||||
@classmethod
|
||||
def main(cls, argv=sys.argv):
|
||||
"""
|
||||
Executable entry point for L{Twist}.
|
||||
Processes options and run a twisted reactor with a service.
|
||||
|
||||
@param argv: Command line arguments.
|
||||
@type argv: L{list}
|
||||
"""
|
||||
options = cls.options(argv)
|
||||
|
||||
reactor = options["reactor"]
|
||||
service = cls.service(
|
||||
plugin=options.plugins[options.subCommand],
|
||||
options=options.subOptions,
|
||||
)
|
||||
|
||||
cls.startService(reactor, service)
|
||||
cls.run(options)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# -*- test-case-name: twisted.application.twist.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.application.twist}.
|
||||
"""
|
||||
|
|
@ -0,0 +1,385 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.application.twist._options}.
|
||||
"""
|
||||
|
||||
from sys import stdout, stderr
|
||||
|
||||
from twisted.internet import reactor
|
||||
from twisted.copyright import version
|
||||
from twisted.python.usage import UsageError
|
||||
from twisted.logger import LogLevel, textFileLogObserver, jsonFileLogObserver
|
||||
from twisted.test.proto_helpers import MemoryReactor
|
||||
from ...reactors import NoSuchReactor
|
||||
from ...service import ServiceMaker
|
||||
from ...runner._exit import ExitStatus
|
||||
from ...runner.test.test_runner import DummyExit
|
||||
from ...twist import _options
|
||||
from .._options import TwistOptions
|
||||
|
||||
import twisted.trial.unittest
|
||||
|
||||
|
||||
|
||||
class OptionsTests(twisted.trial.unittest.TestCase):
|
||||
"""
|
||||
Tests for L{TwistOptions}.
|
||||
"""
|
||||
|
||||
def patchExit(self):
|
||||
"""
|
||||
Patch L{_twist.exit} so we can capture usage and prevent actual exits.
|
||||
"""
|
||||
self.exit = DummyExit()
|
||||
self.patch(_options, "exit", self.exit)
|
||||
|
||||
|
||||
def patchOpen(self):
|
||||
"""
|
||||
Patch L{_options.open} so we can capture usage and prevent actual opens.
|
||||
"""
|
||||
self.opened = []
|
||||
|
||||
def fakeOpen(name, mode=None):
|
||||
if name == "nocanopen":
|
||||
raise IOError(None, None, name)
|
||||
|
||||
self.opened.append((name, mode))
|
||||
return NotImplemented
|
||||
|
||||
self.patch(_options, "openFile", fakeOpen)
|
||||
|
||||
|
||||
def patchInstallReactor(self):
|
||||
"""
|
||||
Patch C{_options.installReactor} so we can capture usage and prevent
|
||||
actual installs.
|
||||
"""
|
||||
self.installedReactors = {}
|
||||
|
||||
def installReactor(name):
|
||||
if name != "fusion":
|
||||
raise NoSuchReactor()
|
||||
|
||||
reactor = MemoryReactor()
|
||||
self.installedReactors[name] = reactor
|
||||
return reactor
|
||||
|
||||
self.patch(_options, "installReactor", installReactor)
|
||||
|
||||
|
||||
def test_synopsis(self):
|
||||
"""
|
||||
L{TwistOptions.getSynopsis} appends arguments.
|
||||
"""
|
||||
options = TwistOptions()
|
||||
|
||||
self.assertTrue(
|
||||
options.getSynopsis().endswith(" plugin [plugin_options]")
|
||||
)
|
||||
|
||||
|
||||
def test_version(self):
|
||||
"""
|
||||
L{TwistOptions.opt_version} exits with L{ExitStatus.EX_OK} and prints
|
||||
the version.
|
||||
"""
|
||||
self.patchExit()
|
||||
|
||||
options = TwistOptions()
|
||||
options.opt_version()
|
||||
|
||||
self.assertEquals(self.exit.status, ExitStatus.EX_OK)
|
||||
self.assertEquals(self.exit.message, version)
|
||||
|
||||
|
||||
def test_reactor(self):
|
||||
"""
|
||||
L{TwistOptions.installReactor} installs the chosen reactor and sets
|
||||
the reactor name.
|
||||
"""
|
||||
self.patchInstallReactor()
|
||||
|
||||
options = TwistOptions()
|
||||
options.opt_reactor("fusion")
|
||||
|
||||
self.assertEqual(set(self.installedReactors), set(["fusion"]))
|
||||
self.assertEquals(options["reactorName"], "fusion")
|
||||
|
||||
|
||||
def test_installCorrectReactor(self):
|
||||
"""
|
||||
L{TwistOptions.installReactor} installs the chosen reactor after the
|
||||
command line options have been parsed.
|
||||
"""
|
||||
self.patchInstallReactor()
|
||||
|
||||
options = TwistOptions()
|
||||
options.subCommand = "test-subcommand"
|
||||
options.parseOptions(["--reactor=fusion"])
|
||||
|
||||
self.assertEqual(set(self.installedReactors), set(["fusion"]))
|
||||
|
||||
|
||||
def test_installReactorBogus(self):
|
||||
"""
|
||||
L{TwistOptions.installReactor} raises UsageError if an unknown reactor
|
||||
is specified.
|
||||
"""
|
||||
self.patchInstallReactor()
|
||||
|
||||
options = TwistOptions()
|
||||
self.assertRaises(UsageError, options.opt_reactor, "coal")
|
||||
|
||||
|
||||
def test_installReactorDefault(self):
|
||||
"""
|
||||
L{TwistOptions.installReactor} returns the currently installed reactor
|
||||
when the default reactor name is specified.
|
||||
"""
|
||||
options = TwistOptions()
|
||||
self.assertIdentical(reactor, options.installReactor('default'))
|
||||
|
||||
|
||||
def test_logLevelValid(self):
|
||||
"""
|
||||
L{TwistOptions.opt_log_level} sets the corresponding log level.
|
||||
"""
|
||||
options = TwistOptions()
|
||||
options.opt_log_level("warn")
|
||||
|
||||
self.assertIdentical(options["logLevel"], LogLevel.warn)
|
||||
|
||||
|
||||
def test_logLevelInvalid(self):
|
||||
"""
|
||||
L{TwistOptions.opt_log_level} with an invalid log level name raises
|
||||
UsageError.
|
||||
"""
|
||||
options = TwistOptions()
|
||||
|
||||
self.assertRaises(UsageError, options.opt_log_level, "cheese")
|
||||
|
||||
|
||||
def _testLogFile(self, name, expectedStream):
|
||||
"""
|
||||
Set log file name and check the selected output stream.
|
||||
|
||||
@param name: The name of the file.
|
||||
@param expectedStream: The expected stream.
|
||||
"""
|
||||
options = TwistOptions()
|
||||
options.opt_log_file(name)
|
||||
|
||||
self.assertIdentical(options["logFile"], expectedStream)
|
||||
|
||||
|
||||
def test_logFileStdout(self):
|
||||
"""
|
||||
L{TwistOptions.opt_log_file} given C{"-"} as a file name uses stdout.
|
||||
"""
|
||||
self._testLogFile("-", stdout)
|
||||
|
||||
|
||||
def test_logFileStderr(self):
|
||||
"""
|
||||
L{TwistOptions.opt_log_file} given C{"+"} as a file name uses stderr.
|
||||
"""
|
||||
self._testLogFile("+", stderr)
|
||||
|
||||
|
||||
def test_logFileNamed(self):
|
||||
"""
|
||||
L{TwistOptions.opt_log_file} opens the given file name in append mode.
|
||||
"""
|
||||
self.patchOpen()
|
||||
|
||||
options = TwistOptions()
|
||||
options.opt_log_file("mylog")
|
||||
|
||||
self.assertEqual([("mylog", "a")], self.opened)
|
||||
|
||||
|
||||
def test_logFileCantOpen(self):
|
||||
"""
|
||||
L{TwistOptions.opt_log_file} exits with L{ExitStatus.EX_IOERR} if
|
||||
unable to open the log file due to an L{EnvironmentError}.
|
||||
"""
|
||||
self.patchExit()
|
||||
self.patchOpen()
|
||||
|
||||
options = TwistOptions()
|
||||
options.opt_log_file("nocanopen")
|
||||
|
||||
self.assertEquals(self.exit.status, ExitStatus.EX_IOERR)
|
||||
self.assertTrue(
|
||||
self.exit.message.startswith(
|
||||
"Unable to open log file 'nocanopen': "
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _testLogFormat(self, format, expectedObserver):
|
||||
"""
|
||||
Set log file format and check the selected observer.
|
||||
|
||||
@param format: The format of the file.
|
||||
@param expectedObserver: The expected observer.
|
||||
"""
|
||||
options = TwistOptions()
|
||||
options.opt_log_format(format)
|
||||
|
||||
self.assertIdentical(
|
||||
options["fileLogObserverFactory"], expectedObserver
|
||||
)
|
||||
self.assertEqual(options["logFormat"], format)
|
||||
|
||||
|
||||
def test_logFormatText(self):
|
||||
"""
|
||||
L{TwistOptions.opt_log_format} given C{"text"} uses a
|
||||
L{textFileLogObserver}.
|
||||
"""
|
||||
self._testLogFormat("text", textFileLogObserver)
|
||||
|
||||
|
||||
def test_logFormatJSON(self):
|
||||
"""
|
||||
L{TwistOptions.opt_log_format} given C{"text"} uses a
|
||||
L{textFileLogObserver}.
|
||||
"""
|
||||
self._testLogFormat("json", jsonFileLogObserver)
|
||||
|
||||
|
||||
def test_logFormatInvalid(self):
|
||||
"""
|
||||
L{TwistOptions.opt_log_format} given an invalid format name raises
|
||||
L{UsageError}.
|
||||
"""
|
||||
options = TwistOptions()
|
||||
|
||||
self.assertRaises(UsageError, options.opt_log_format, "frommage")
|
||||
|
||||
|
||||
def test_selectDefaultLogObserverNoOverride(self):
|
||||
"""
|
||||
L{TwistOptions.selectDefaultLogObserver} will not override an already
|
||||
selected observer.
|
||||
"""
|
||||
self.patchOpen()
|
||||
|
||||
options = TwistOptions()
|
||||
options.opt_log_format("text") # Ask for text
|
||||
options.opt_log_file("queso") # File, not a tty
|
||||
options.selectDefaultLogObserver()
|
||||
|
||||
# Because we didn't select a file that is a tty, the default is JSON,
|
||||
# but since we asked for text, we should get text.
|
||||
self.assertIdentical(
|
||||
options["fileLogObserverFactory"], textFileLogObserver
|
||||
)
|
||||
self.assertEqual(options["logFormat"], "text")
|
||||
|
||||
|
||||
def test_selectDefaultLogObserverDefaultWithTTY(self):
|
||||
"""
|
||||
L{TwistOptions.selectDefaultLogObserver} will not override an already
|
||||
selected observer.
|
||||
"""
|
||||
class TTYFile(object):
|
||||
def isatty(self):
|
||||
return True
|
||||
|
||||
# stdout may not be a tty, so let's make sure it thinks it is
|
||||
self.patch(_options, "stdout", TTYFile())
|
||||
|
||||
options = TwistOptions()
|
||||
options.opt_log_file("-") # stdout, a tty
|
||||
options.selectDefaultLogObserver()
|
||||
|
||||
self.assertIdentical(
|
||||
options["fileLogObserverFactory"], textFileLogObserver
|
||||
)
|
||||
self.assertEqual(options["logFormat"], "text")
|
||||
|
||||
|
||||
def test_selectDefaultLogObserverDefaultWithoutTTY(self):
|
||||
"""
|
||||
L{TwistOptions.selectDefaultLogObserver} will not override an already
|
||||
selected observer.
|
||||
"""
|
||||
self.patchOpen()
|
||||
|
||||
options = TwistOptions()
|
||||
options.opt_log_file("queso") # File, not a tty
|
||||
options.selectDefaultLogObserver()
|
||||
|
||||
self.assertIdentical(
|
||||
options["fileLogObserverFactory"], jsonFileLogObserver
|
||||
)
|
||||
self.assertEqual(options["logFormat"], "json")
|
||||
|
||||
|
||||
def test_pluginsType(self):
|
||||
"""
|
||||
L{TwistOptions.plugins} is a mapping of available plug-ins.
|
||||
"""
|
||||
options = TwistOptions()
|
||||
plugins = options.plugins
|
||||
|
||||
for name in plugins:
|
||||
self.assertIsInstance(name, str)
|
||||
self.assertIsInstance(plugins[name], ServiceMaker)
|
||||
|
||||
|
||||
def test_pluginsIncludeWeb(self):
|
||||
"""
|
||||
L{TwistOptions.plugins} includes a C{"web"} plug-in.
|
||||
This is an attempt to verify that something we expect to be in the list
|
||||
is in there without enumerating all of the built-in plug-ins.
|
||||
"""
|
||||
options = TwistOptions()
|
||||
|
||||
self.assertIn("web", options.plugins)
|
||||
|
||||
|
||||
def test_subCommandsType(self):
|
||||
"""
|
||||
L{TwistOptions.subCommands} is an iterable of tuples as expected by
|
||||
L{twisted.python.usage.Options}.
|
||||
"""
|
||||
options = TwistOptions()
|
||||
|
||||
for name, shortcut, parser, doc in options.subCommands:
|
||||
self.assertIsInstance(name, str)
|
||||
self.assertIdentical(shortcut, None)
|
||||
self.assertTrue(callable(parser))
|
||||
self.assertIsInstance(doc, str)
|
||||
|
||||
|
||||
def test_subCommandsIncludeWeb(self):
|
||||
"""
|
||||
L{TwistOptions.subCommands} includes a sub-command for every plug-in.
|
||||
"""
|
||||
options = TwistOptions()
|
||||
|
||||
plugins = set(options.plugins)
|
||||
subCommands = set(
|
||||
name for name, shortcut, parser, doc in options.subCommands
|
||||
)
|
||||
|
||||
self.assertEqual(subCommands, plugins)
|
||||
|
||||
|
||||
def test_postOptionsNoSubCommand(self):
|
||||
"""
|
||||
L{TwistOptions.postOptions} raises L{UsageError} is it has no
|
||||
sub-command.
|
||||
"""
|
||||
self.patchInstallReactor()
|
||||
|
||||
options = TwistOptions()
|
||||
|
||||
self.assertRaises(UsageError, options.postOptions)
|
||||
|
|
@ -0,0 +1,269 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.application.twist._twist}.
|
||||
"""
|
||||
|
||||
from sys import stdout
|
||||
|
||||
from twisted.logger import LogLevel, jsonFileLogObserver
|
||||
from twisted.test.proto_helpers import MemoryReactor
|
||||
from ...service import IService, MultiService
|
||||
from ...runner._exit import ExitStatus
|
||||
from ...runner._runner import Runner
|
||||
from ...runner.test.test_runner import DummyExit
|
||||
from ...twist import _twist
|
||||
from .._options import TwistOptions
|
||||
from .._twist import Twist
|
||||
from twisted.test.test_twistd import SignalCapturingMemoryReactor
|
||||
|
||||
import twisted.trial.unittest
|
||||
|
||||
|
||||
|
||||
class TwistTests(twisted.trial.unittest.TestCase):
|
||||
"""
|
||||
Tests for L{Twist}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.patchInstallReactor()
|
||||
|
||||
|
||||
def patchExit(self):
|
||||
"""
|
||||
Patch L{_twist.exit} so we can capture usage and prevent actual exits.
|
||||
"""
|
||||
self.exit = DummyExit()
|
||||
self.patch(_twist, "exit", self.exit)
|
||||
|
||||
|
||||
def patchInstallReactor(self):
|
||||
"""
|
||||
Patch C{_options.installReactor} so we can capture usage and prevent
|
||||
actual installs.
|
||||
"""
|
||||
self.installedReactors = {}
|
||||
|
||||
def installReactor(_, name):
|
||||
reactor = MemoryReactor()
|
||||
self.installedReactors[name] = reactor
|
||||
return reactor
|
||||
|
||||
self.patch(TwistOptions, "installReactor", installReactor)
|
||||
|
||||
|
||||
def patchStartService(self):
|
||||
"""
|
||||
Patch L{MultiService.startService} so we can capture usage and prevent
|
||||
actual starts.
|
||||
"""
|
||||
self.serviceStarts = []
|
||||
|
||||
def startService(service):
|
||||
self.serviceStarts.append(service)
|
||||
|
||||
self.patch(MultiService, "startService", startService)
|
||||
|
||||
|
||||
def test_optionsValidArguments(self):
|
||||
"""
|
||||
L{Twist.options} given valid arguments returns options.
|
||||
"""
|
||||
options = Twist.options(["twist", "web"])
|
||||
|
||||
self.assertIsInstance(options, TwistOptions)
|
||||
|
||||
|
||||
def test_optionsInvalidArguments(self):
|
||||
"""
|
||||
L{Twist.options} given invalid arguments exits with
|
||||
L{ExitStatus.EX_USAGE} and an error/usage message.
|
||||
"""
|
||||
self.patchExit()
|
||||
|
||||
Twist.options(["twist", "--bogus-bagels"])
|
||||
|
||||
self.assertIdentical(self.exit.status, ExitStatus.EX_USAGE)
|
||||
self.assertTrue(self.exit.message.startswith("Error: "))
|
||||
self.assertTrue(self.exit.message.endswith(
|
||||
"\n\n{}".format(TwistOptions())
|
||||
))
|
||||
|
||||
|
||||
def test_service(self):
|
||||
"""
|
||||
L{Twist.service} returns an L{IService}.
|
||||
"""
|
||||
options = Twist.options(["twist", "web"]) # web should exist
|
||||
service = Twist.service(options.plugins["web"], options.subOptions)
|
||||
self.assertTrue(IService.providedBy(service))
|
||||
|
||||
|
||||
def test_startService(self):
|
||||
"""
|
||||
L{Twist.startService} starts the service and registers a trigger to
|
||||
stop the service when the reactor shuts down.
|
||||
"""
|
||||
options = Twist.options(["twist", "web"])
|
||||
|
||||
reactor = options["reactor"]
|
||||
service = Twist.service(
|
||||
plugin=options.plugins[options.subCommand],
|
||||
options=options.subOptions,
|
||||
)
|
||||
|
||||
self.patchStartService()
|
||||
|
||||
Twist.startService(reactor, service)
|
||||
|
||||
self.assertEqual(self.serviceStarts, [service])
|
||||
self.assertEqual(
|
||||
reactor.triggers["before"]["shutdown"],
|
||||
[(service.stopService, (), {})]
|
||||
)
|
||||
|
||||
|
||||
def test_run(self):
|
||||
"""
|
||||
L{Twist.run} runs the runner with arguments corresponding to the given
|
||||
options.
|
||||
"""
|
||||
argsSeen = []
|
||||
|
||||
self.patch(
|
||||
Runner, "__init__", lambda self, **args: argsSeen.append(args)
|
||||
)
|
||||
self.patch(
|
||||
Runner, "run", lambda self: None
|
||||
)
|
||||
|
||||
twistOptions = Twist.options([
|
||||
"twist", "--reactor=default", "--log-format=json", "web"
|
||||
])
|
||||
Twist.run(twistOptions)
|
||||
|
||||
self.assertEqual(len(argsSeen), 1)
|
||||
self.assertEqual(
|
||||
argsSeen[0],
|
||||
dict(
|
||||
reactor=self.installedReactors["default"],
|
||||
defaultLogLevel=LogLevel.info,
|
||||
logFile=stdout,
|
||||
fileLogObserverFactory=jsonFileLogObserver,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_main(self):
|
||||
"""
|
||||
L{Twist.main} runs the runner with arguments corresponding to the given
|
||||
command line arguments.
|
||||
"""
|
||||
self.patchStartService()
|
||||
|
||||
runners = []
|
||||
|
||||
class Runner(object):
|
||||
def __init__(self, **kwargs):
|
||||
self.args = kwargs
|
||||
self.runs = 0
|
||||
runners.append(self)
|
||||
|
||||
def run(self):
|
||||
self.runs += 1
|
||||
|
||||
self.patch(_twist, "Runner", Runner)
|
||||
|
||||
Twist.main([
|
||||
"twist", "--reactor=default", "--log-format=json", "web"
|
||||
])
|
||||
|
||||
self.assertEqual(len(self.serviceStarts), 1)
|
||||
self.assertEqual(len(runners), 1)
|
||||
self.assertEqual(
|
||||
runners[0].args,
|
||||
dict(
|
||||
reactor=self.installedReactors["default"],
|
||||
defaultLogLevel=LogLevel.info,
|
||||
logFile=stdout,
|
||||
fileLogObserverFactory=jsonFileLogObserver,
|
||||
)
|
||||
)
|
||||
self.assertEqual(runners[0].runs, 1)
|
||||
|
||||
|
||||
|
||||
class TwistExitTests(twisted.trial.unittest.TestCase):
|
||||
"""
|
||||
Tests to verify that the Twist script takes the expected actions related
|
||||
to signals and the reactor.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.exitWithSignalCalled = False
|
||||
|
||||
def fakeExitWithSignal(sig):
|
||||
"""
|
||||
Fake to capture whether L{twisted.application._exitWithSignal
|
||||
was called.
|
||||
|
||||
@param sig: Signal value
|
||||
@type sig: C{int}
|
||||
"""
|
||||
self.exitWithSignalCalled = True
|
||||
|
||||
self.patch(_twist, '_exitWithSignal', fakeExitWithSignal)
|
||||
|
||||
def startLogging(_):
|
||||
"""
|
||||
Prevent Runner from adding new log observers or other
|
||||
tests outside this module will fail.
|
||||
|
||||
@param _: Unused self param
|
||||
"""
|
||||
|
||||
self.patch(Runner, 'startLogging', startLogging)
|
||||
|
||||
|
||||
def test_twistReactorDoesntExitWithSignal(self):
|
||||
"""
|
||||
_exitWithSignal is not called if the reactor's _exitSignal attribute
|
||||
is zero.
|
||||
"""
|
||||
reactor = SignalCapturingMemoryReactor()
|
||||
reactor._exitSignal = None
|
||||
options = TwistOptions()
|
||||
options["reactor"] = reactor
|
||||
options["fileLogObserverFactory"] = jsonFileLogObserver
|
||||
|
||||
Twist.run(options)
|
||||
self.assertFalse(self.exitWithSignalCalled)
|
||||
|
||||
|
||||
def test_twistReactorHasNoExitSignalAttr(self):
|
||||
"""
|
||||
_exitWithSignal is not called if the runner's reactor does not
|
||||
implement L{twisted.internet.interfaces._ISupportsExitSignalCapturing}
|
||||
"""
|
||||
reactor = MemoryReactor()
|
||||
options = TwistOptions()
|
||||
options["reactor"] = reactor
|
||||
options["fileLogObserverFactory"] = jsonFileLogObserver
|
||||
Twist.run(options)
|
||||
self.assertFalse(self.exitWithSignalCalled)
|
||||
|
||||
|
||||
def test_twistReactorExitsWithSignal(self):
|
||||
"""
|
||||
_exitWithSignal is called if the runner's reactor exits due
|
||||
to a signal.
|
||||
"""
|
||||
reactor = SignalCapturingMemoryReactor()
|
||||
reactor._exitSignal = 2
|
||||
options = TwistOptions()
|
||||
options["reactor"] = reactor
|
||||
options["fileLogObserverFactory"] = jsonFileLogObserver
|
||||
Twist.run(options)
|
||||
self.assertTrue(self.exitWithSignalCalled)
|
||||
Loading…
Add table
Add a link
Reference in a new issue