Ausgabe der neuen DB Einträge

This commit is contained in:
hubobel 2022-01-02 21:50:48 +01:00
parent bad48e1627
commit cfbbb9ee3d
2399 changed files with 843193 additions and 43 deletions

View file

@ -0,0 +1,6 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Configuration objects for Twisted Applications.
"""

View 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)

File diff suppressed because it is too large Load diff

View file

@ -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)

View file

@ -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.
"""

View file

@ -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)

View file

@ -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.
"""

View 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)

View file

@ -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}.
"""

View file

@ -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

View file

@ -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

View file

@ -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.
"""

View 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']

View file

@ -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']

View file

@ -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

View file

@ -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)))

View file

@ -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.
"""

View file

@ -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.")

View file

@ -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)

View file

@ -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}.
"""

View file

@ -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)

View file

@ -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)