okay fine

This commit is contained in:
pacnpal
2024-11-03 17:47:26 +00:00
parent 01c6004a79
commit 27eb239e97
10020 changed files with 1935769 additions and 2364 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,596 @@
# -*- test-case-name: twisted.application.test.test_internet,twisted.test.test_application,twisted.test.test_cooperator -*-
"""
Implementation of L{twisted.application.internet.ClientService}, particularly
its U{automat <https://automat.readthedocs.org/>} state machine.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from random import random as _goodEnoughRandom
from typing import Callable, Optional, Protocol as TypingProtocol, TypeVar, Union
from zope.interface import implementer
from automat import TypeMachineBuilder, pep614
from twisted.application.service import Service
from twisted.internet.defer import (
CancelledError,
Deferred,
fail,
maybeDeferred,
succeed,
)
from twisted.internet.interfaces import (
IAddress,
IDelayedCall,
IProtocol,
IProtocolFactory,
IReactorTime,
IStreamClientEndpoint,
ITransport,
)
from twisted.logger import Logger
from twisted.python.failure import Failure
T = TypeVar("T")
def _maybeGlobalReactor(maybeReactor: Optional[T]) -> T:
"""
@return: the argument, or the global reactor if the argument is L{None}.
"""
if maybeReactor is None:
from twisted.internet import reactor
return reactor # type:ignore[return-value]
else:
return maybeReactor
class _Client(TypingProtocol):
def start(self) -> None:
"""
Start this L{ClientService}, initiating the connection retry loop.
"""
def stop(self) -> Deferred[None]:
"""
Stop trying to connect and disconnect any current connection.
@return: a L{Deferred} that fires when all outstanding connections are
closed and all in-progress connection attempts halted.
"""
def _connectionMade(self, protocol: _ReconnectingProtocolProxy) -> None:
"""
A connection has been made.
@param protocol: The protocol of the connection.
"""
def _connectionFailed(self, failure: Failure) -> None:
"""
Deliver connection failures to any L{ClientService.whenConnected}
L{Deferred}s that have met their failAfterFailures threshold.
@param failure: the Failure to fire the L{Deferred}s with.
"""
def _reconnect(self, failure: Optional[Failure] = None) -> None:
"""
The wait between connection attempts is done.
"""
def _clientDisconnected(self, failure: Optional[Failure] = None) -> None:
"""
The current connection has been disconnected.
"""
def whenConnected(
self, /, failAfterFailures: Optional[int] = None
) -> Deferred[IProtocol]:
"""
Retrieve the currently-connected L{Protocol}, or the next one to
connect.
@param failAfterFailures: number of connection failures after which the
Deferred will deliver a Failure (None means the Deferred will only
fail if/when the service is stopped). Set this to 1 to make the
very first connection failure signal an error. Use 2 to allow one
failure but signal an error if the subsequent retry then fails.
@return: a Deferred that fires with a protocol produced by the factory
passed to C{__init__}. It may:
- fire with L{IProtocol}
- fail with L{CancelledError} when the service is stopped
- fail with e.g.
L{DNSLookupError<twisted.internet.error.DNSLookupError>} or
L{ConnectionRefusedError<twisted.internet.error.ConnectionRefusedError>}
when the number of consecutive failed connection attempts
equals the value of "failAfterFailures"
"""
@implementer(IProtocol)
class _ReconnectingProtocolProxy:
"""
A proxy for a Protocol to provide connectionLost notification to a client
connection service, in support of reconnecting when connections are lost.
"""
def __init__(
self, protocol: IProtocol, lostNotification: Callable[[Failure], None]
) -> None:
"""
Create a L{_ReconnectingProtocolProxy}.
@param protocol: the application-provided L{interfaces.IProtocol}
provider.
@type protocol: provider of L{interfaces.IProtocol} which may
additionally provide L{interfaces.IHalfCloseableProtocol} and
L{interfaces.IFileDescriptorReceiver}.
@param lostNotification: a 1-argument callable to invoke with the
C{reason} when the connection is lost.
"""
self._protocol = protocol
self._lostNotification = lostNotification
def makeConnection(self, transport: ITransport) -> None:
self._transport = transport
self._protocol.makeConnection(transport)
def connectionLost(self, reason: Failure) -> None:
"""
The connection was lost. Relay this information.
@param reason: The reason the connection was lost.
@return: the underlying protocol's result
"""
try:
return self._protocol.connectionLost(reason)
finally:
self._lostNotification(reason)
def __getattr__(self, item: str) -> object:
return getattr(self._protocol, item)
def __repr__(self) -> str:
return f"<{self.__class__.__name__} wrapping {self._protocol!r}>"
@implementer(IProtocolFactory)
class _DisconnectFactory:
"""
A L{_DisconnectFactory} is a proxy for L{IProtocolFactory} that catches
C{connectionLost} notifications and relays them.
"""
def __init__(
self,
protocolFactory: IProtocolFactory,
protocolDisconnected: Callable[[Failure], None],
) -> None:
self._protocolFactory = protocolFactory
self._protocolDisconnected = protocolDisconnected
def buildProtocol(self, addr: IAddress) -> Optional[IProtocol]:
"""
Create a L{_ReconnectingProtocolProxy} with the disconnect-notification
callback we were called with.
@param addr: The address the connection is coming from.
@return: a L{_ReconnectingProtocolProxy} for a protocol produced by
C{self._protocolFactory}
"""
built = self._protocolFactory.buildProtocol(addr)
if built is None:
return None
return _ReconnectingProtocolProxy(built, self._protocolDisconnected)
def __getattr__(self, item: str) -> object:
return getattr(self._protocolFactory, item)
def __repr__(self) -> str:
return "<{} wrapping {!r}>".format(
self.__class__.__name__, self._protocolFactory
)
def _deinterface(o: object) -> None:
"""
Remove the special runtime attributes set by L{implementer} so that a class
can proxy through those attributes with C{__getattr__} and thereby forward
optionally-provided interfaces by the delegated class.
"""
for zopeSpecial in ["__providedBy__", "__provides__", "__implemented__"]:
delattr(o, zopeSpecial)
_deinterface(_DisconnectFactory)
_deinterface(_ReconnectingProtocolProxy)
@dataclass
class _Core:
"""
Shared core for ClientService state machine.
"""
# required parameters
endpoint: IStreamClientEndpoint
factory: IProtocolFactory
timeoutForAttempt: Callable[[int], float]
clock: IReactorTime
prepareConnection: Optional[Callable[[IProtocol], object]]
# internal state
stopWaiters: list[Deferred[None]] = field(default_factory=list)
awaitingConnected: list[tuple[Deferred[IProtocol], Optional[int]]] = field(
default_factory=list
)
failedAttempts: int = 0
log: Logger = Logger()
def waitForStop(self) -> Deferred[None]:
self.stopWaiters.append(Deferred())
return self.stopWaiters[-1]
def unawait(self, value: Union[IProtocol, Failure]) -> None:
self.awaitingConnected, waiting = [], self.awaitingConnected
for w, remaining in waiting:
w.callback(value)
def cancelConnectWaiters(self) -> None:
self.unawait(Failure(CancelledError()))
def finishStopping(self) -> None:
self.stopWaiters, waiting = [], self.stopWaiters
for w in waiting:
w.callback(None)
def makeMachine() -> Callable[[_Core], _Client]:
machine = TypeMachineBuilder(_Client, _Core)
def waitForRetry(
c: _Client, s: _Core, failure: Optional[Failure] = None
) -> IDelayedCall:
s.failedAttempts += 1
delay = s.timeoutForAttempt(s.failedAttempts)
s.log.info(
"Scheduling retry {attempt} to connect {endpoint} in {delay} seconds.",
attempt=s.failedAttempts,
endpoint=s.endpoint,
delay=delay,
)
return s.clock.callLater(delay, c._reconnect)
def rememberConnection(
c: _Client, s: _Core, protocol: _ReconnectingProtocolProxy
) -> _ReconnectingProtocolProxy:
s.failedAttempts = 0
s.unawait(protocol._protocol)
return protocol
def attemptConnection(
c: _Client, s: _Core, failure: Optional[Failure] = None
) -> Deferred[_ReconnectingProtocolProxy]:
factoryProxy = _DisconnectFactory(s.factory, c._clientDisconnected)
connecting: Deferred[IProtocol] = s.endpoint.connect(factoryProxy)
def prepare(
protocol: _ReconnectingProtocolProxy,
) -> Deferred[_ReconnectingProtocolProxy]:
if s.prepareConnection is not None:
return maybeDeferred(s.prepareConnection, protocol).addCallback(
lambda _: protocol
)
return succeed(protocol)
# endpoint.connect() is actually generic on the type of the protocol,
# but this is not expressible via zope.interface, so we have to cast
# https://github.com/Shoobx/mypy-zope/issues/95
connectingProxy: Deferred[_ReconnectingProtocolProxy]
connectingProxy = connecting # type:ignore[assignment]
(
connectingProxy.addCallback(prepare)
.addCallback(c._connectionMade)
.addErrback(c._connectionFailed)
)
return connectingProxy
# States:
Init = machine.state("Init")
Connecting = machine.state("Connecting", attemptConnection)
Stopped = machine.state("Stopped")
Waiting = machine.state("Waiting", waitForRetry)
Connected = machine.state("Connected", rememberConnection)
Disconnecting = machine.state("Disconnecting")
Restarting = machine.state("Restarting")
Stopped = machine.state("Stopped")
# Behavior-less state transitions:
Init.upon(_Client.start).to(Connecting).returns(None)
Connecting.upon(_Client.start).loop().returns(None)
Connecting.upon(_Client._connectionMade).to(Connected).returns(None)
Waiting.upon(_Client.start).loop().returns(None)
Waiting.upon(_Client._reconnect).to(Connecting).returns(None)
Connected.upon(_Client._connectionFailed).to(Waiting).returns(None)
Connected.upon(_Client.start).loop().returns(None)
Connected.upon(_Client._clientDisconnected).to(Waiting).returns(None)
Disconnecting.upon(_Client.start).to(Restarting).returns(None)
Restarting.upon(_Client.start).to(Restarting).returns(None)
Stopped.upon(_Client.start).to(Connecting).returns(None)
# Behavior-full state transitions:
@pep614(Init.upon(_Client.stop).to(Stopped))
@pep614(Stopped.upon(_Client.stop).to(Stopped))
def immediateStop(c: _Client, s: _Core) -> Deferred[None]:
return succeed(None)
@pep614(Connecting.upon(_Client.stop).to(Disconnecting))
def connectingStop(
c: _Client, s: _Core, attempt: Deferred[_ReconnectingProtocolProxy]
) -> Deferred[None]:
waited = s.waitForStop()
attempt.cancel()
return waited
@pep614(Connecting.upon(_Client._connectionFailed, nodata=True).to(Waiting))
def failedWhenConnecting(c: _Client, s: _Core, failure: Failure) -> None:
ready = []
notReady: list[tuple[Deferred[IProtocol], Optional[int]]] = []
for w, remaining in s.awaitingConnected:
if remaining is None:
notReady.append((w, remaining))
elif remaining <= 1:
ready.append(w)
else:
notReady.append((w, remaining - 1))
s.awaitingConnected = notReady
for w in ready:
w.callback(failure)
@pep614(Waiting.upon(_Client.stop).to(Stopped))
def stop(c: _Client, s: _Core, futureRetry: IDelayedCall) -> Deferred[None]:
waited = s.waitForStop()
s.cancelConnectWaiters()
futureRetry.cancel()
s.finishStopping()
return waited
@pep614(Connected.upon(_Client.stop).to(Disconnecting))
def stopWhileConnected(
c: _Client, s: _Core, protocol: _ReconnectingProtocolProxy
) -> Deferred[None]:
waited = s.waitForStop()
protocol._transport.loseConnection()
return waited
@pep614(Connected.upon(_Client.whenConnected).loop())
def whenConnectedWhenConnected(
c: _Client,
s: _Core,
protocol: _ReconnectingProtocolProxy,
failAfterFailures: Optional[int] = None,
) -> Deferred[IProtocol]:
return succeed(protocol._protocol)
@pep614(Disconnecting.upon(_Client.stop).loop())
@pep614(Restarting.upon(_Client.stop).to(Disconnecting))
def discoStop(c: _Client, s: _Core) -> Deferred[None]:
return s.waitForStop()
@pep614(Disconnecting.upon(_Client._connectionFailed).to(Stopped))
@pep614(Disconnecting.upon(_Client._clientDisconnected).to(Stopped))
def disconnectingFinished(
c: _Client, s: _Core, failure: Optional[Failure] = None
) -> None:
s.cancelConnectWaiters()
s.finishStopping()
@pep614(Connecting.upon(_Client.whenConnected, nodata=True).loop())
@pep614(Waiting.upon(_Client.whenConnected, nodata=True).loop())
@pep614(Init.upon(_Client.whenConnected).to(Init))
@pep614(Restarting.upon(_Client.whenConnected).to(Restarting))
@pep614(Disconnecting.upon(_Client.whenConnected).to(Disconnecting))
def awaitingConnection(
c: _Client, s: _Core, failAfterFailures: Optional[int] = None
) -> Deferred[IProtocol]:
result: Deferred[IProtocol] = Deferred()
s.awaitingConnected.append((result, failAfterFailures))
return result
@pep614(Restarting.upon(_Client._clientDisconnected).to(Connecting))
def restartDone(c: _Client, s: _Core, failure: Optional[Failure] = None) -> None:
s.finishStopping()
@pep614(Stopped.upon(_Client.whenConnected).to(Stopped))
def notGoingToConnect(
c: _Client, s: _Core, failAfterFailures: Optional[int] = None
) -> Deferred[IProtocol]:
return fail(CancelledError())
return machine.build()
def backoffPolicy(
initialDelay: float = 1.0,
maxDelay: float = 60.0,
factor: float = 1.5,
jitter: Callable[[], float] = _goodEnoughRandom,
) -> Callable[[int], float]:
"""
A timeout policy for L{ClientService} which computes an exponential backoff
interval with configurable parameters.
@since: 16.1.0
@param initialDelay: Delay for the first reconnection attempt (default
1.0s).
@type initialDelay: L{float}
@param maxDelay: Maximum number of seconds between connection attempts
(default 60 seconds, or one minute). Note that this value is before
jitter is applied, so the actual maximum possible delay is this value
plus the maximum possible result of C{jitter()}.
@type maxDelay: L{float}
@param factor: A multiplicative factor by which the delay grows on each
failed reattempt. Default: 1.5.
@type factor: L{float}
@param jitter: A 0-argument callable that introduces noise into the delay.
By default, C{random.random}, i.e. a pseudorandom floating-point value
between zero and one.
@type jitter: 0-argument callable returning L{float}
@return: a 1-argument callable that, given an attempt count, returns a
floating point number; the number of seconds to delay.
@rtype: see L{ClientService.__init__}'s C{retryPolicy} argument.
"""
def policy(attempt: int) -> float:
try:
delay = min(initialDelay * (factor ** min(100, attempt)), maxDelay)
except OverflowError:
delay = maxDelay
return delay + jitter()
return policy
_defaultPolicy = backoffPolicy()
ClientMachine = makeMachine()
class ClientService(Service):
"""
A L{ClientService} maintains a single outgoing connection to a client
endpoint, reconnecting after a configurable timeout when a connection
fails, either before or after connecting.
@since: 16.1.0
"""
_log = Logger()
def __init__(
self,
endpoint: IStreamClientEndpoint,
factory: IProtocolFactory,
retryPolicy: Optional[Callable[[int], float]] = None,
clock: Optional[IReactorTime] = None,
prepareConnection: Optional[Callable[[IProtocol], object]] = None,
):
"""
@param endpoint: A L{stream client endpoint
<interfaces.IStreamClientEndpoint>} provider which will be used to
connect when the service starts.
@param factory: A L{protocol factory <interfaces.IProtocolFactory>}
which will be used to create clients for the endpoint.
@param retryPolicy: A policy configuring how long L{ClientService} will
wait between attempts to connect to C{endpoint}; a callable taking
(the number of failed connection attempts made in a row (L{int}))
and returning the number of seconds to wait before making another
attempt.
@param clock: The clock used to schedule reconnection. It's mainly
useful to be parametrized in tests. If the factory is serialized,
this attribute will not be serialized, and the default value (the
reactor) will be restored when deserialized.
@param prepareConnection: A single argument L{callable} that may return
a L{Deferred}. It will be called once with the L{protocol
<interfaces.IProtocol>} each time a new connection is made. It may
call methods on the protocol to prepare it for use (e.g.
authenticate) or validate it (check its health).
The C{prepareConnection} callable may raise an exception or return
a L{Deferred} which fails to reject the connection. A rejected
connection is not used to fire an L{Deferred} returned by
L{whenConnected}. Instead, L{ClientService} handles the failure
and continues as if the connection attempt were a failure
(incrementing the counter passed to C{retryPolicy}).
L{Deferred}s returned by L{whenConnected} will not fire until any
L{Deferred} returned by the C{prepareConnection} callable fire.
Otherwise its successful return value is consumed, but ignored.
Present Since Twisted 18.7.0
"""
clock = _maybeGlobalReactor(clock)
retryPolicy = _defaultPolicy if retryPolicy is None else retryPolicy
self._machine: _Client = ClientMachine(
_Core(
endpoint,
factory,
retryPolicy,
clock,
prepareConnection=prepareConnection,
log=self._log,
)
)
def whenConnected(
self, failAfterFailures: Optional[int] = None
) -> Deferred[IProtocol]:
"""
Retrieve the currently-connected L{Protocol}, or the next one to
connect.
@param failAfterFailures: number of connection failures after which
the Deferred will deliver a Failure (None means the Deferred will
only fail if/when the service is stopped). Set this to 1 to make
the very first connection failure signal an error. Use 2 to
allow one failure but signal an error if the subsequent retry
then fails.
@type failAfterFailures: L{int} or None
@return: a Deferred that fires with a protocol produced by the
factory passed to C{__init__}
@rtype: L{Deferred} that may:
- fire with L{IProtocol}
- fail with L{CancelledError} when the service is stopped
- fail with e.g.
L{DNSLookupError<twisted.internet.error.DNSLookupError>} or
L{ConnectionRefusedError<twisted.internet.error.ConnectionRefusedError>}
when the number of consecutive failed connection attempts
equals the value of "failAfterFailures"
"""
return self._machine.whenConnected(failAfterFailures)
def startService(self) -> None:
"""
Start this L{ClientService}, initiating the connection retry loop.
"""
if self.running:
self._log.warn("Duplicate ClientService.startService {log_source}")
return
super().startService()
self._machine.start()
def stopService(self) -> Deferred[None]:
"""
Stop attempting to reconnect and close any existing connections.
@return: a L{Deferred} that fires when all outstanding connections are
closed and all in-progress connection attempts halted.
"""
super().stopService()
return self._machine.stop()

View File

@@ -0,0 +1,706 @@
# -*- test-case-name: twisted.test.test_application,twisted.test.test_twistd -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
import getpass
import os
import pdb
import signal
import sys
import traceback
import warnings
from operator import attrgetter
from twisted import copyright, logger, plugin
from twisted.application import reactors, service
# Expose the new implementation of installReactor at the old location.
from twisted.application.reactors import NoSuchReactor, installReactor
from twisted.internet import defer
from twisted.internet.interfaces import _ISupportsExitSignalCapturing
from twisted.persisted import sob
from twisted.python import failure, log, logfile, runtime, usage, util
from twisted.python.reflect import namedAny, namedModule, qual
class _BasicProfiler:
"""
@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 = f"Failed to import module {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:
"""
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(f"Unsupported profiler name: {self.profiler}")
class AppLogger:
"""
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 BaseException:
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:
"""
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()])
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(f" {r.shortName:<4}\t{r.description}\n")
except ImportError as e:
notWorkingReactors += " !{:<4}\t{} ({})\n".format(
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 '{}' could not be imported: {}".format(self["logger"], e)
)
@property
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,
)
def run(runApp, ServerOptions):
config = ServerOptions()
try:
config.parseOptions()
except usage.error as ue:
commstr = " ".join(sys.argv[0:2])
print(config)
print(f"{commstr}: {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)

View File

@@ -0,0 +1,427 @@
# -*- test-case-name: twisted.application.test.test_internet,twisted.test.test_application,twisted.test.test_cooperator -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Reactor-based Services
Here are services to run clients, servers and periodic services using
the reactor.
If you want to run a server service, L{StreamServerEndpointService} defines a
service that can wrap an arbitrary L{IStreamServerEndpoint
<twisted.internet.interfaces.IStreamServerEndpoint>}
as an L{IService}. See also L{twisted.application.strports.service} for
constructing one of these directly from a descriptive string.
Additionally, this module (dynamically) defines various Service subclasses that
let you represent clients and servers in a Service hierarchy. Endpoints APIs
should be preferred for stream server services, but since those APIs do not yet
exist for clients or datagram services, many of these are still useful.
They are as follows::
TCPServer, TCPClient,
UNIXServer, UNIXClient,
SSLServer, SSLClient,
UDPServer,
UNIXDatagramServer, UNIXDatagramClient,
MulticastServer
These classes take arbitrary arguments in their constructors and pass
them straight on to their respective reactor.listenXXX or
reactor.connectXXX calls.
For example, the following service starts a web server on port 8080:
C{TCPServer(8080, server.Site(r))}. See the documentation for the
reactor.listen/connect* methods for more information.
"""
from typing import List
from twisted.application import service
from twisted.internet import task
from twisted.internet.defer import CancelledError
from twisted.python import log
from ._client_service import ClientService, _maybeGlobalReactor, backoffPolicy
class _VolatileDataService(service.Service):
volatile: List[str] = []
def __getstate__(self):
d = service.Service.__getstate__(self)
for attr in self.volatile:
if attr in d:
del d[attr]
return d
class _AbstractServer(_VolatileDataService):
"""
@cvar volatile: list of attribute to remove from pickling.
@type volatile: C{list}
@ivar method: the type of method to call on the reactor, one of B{TCP},
B{UDP}, B{SSL} or B{UNIX}.
@type method: C{str}
@ivar reactor: the current running reactor.
@type reactor: a provider of C{IReactorTCP}, C{IReactorUDP},
C{IReactorSSL} or C{IReactorUnix}.
@ivar _port: instance of port set when the service is started.
@type _port: a provider of L{twisted.internet.interfaces.IListeningPort}.
"""
volatile = ["_port"]
method: str = ""
reactor = None
_port = None
def __init__(self, *args, **kwargs):
self.args = args
if "reactor" in kwargs:
self.reactor = kwargs.pop("reactor")
self.kwargs = kwargs
def privilegedStartService(self):
service.Service.privilegedStartService(self)
self._port = self._getPort()
def startService(self):
service.Service.startService(self)
if self._port is None:
self._port = self._getPort()
def stopService(self):
service.Service.stopService(self)
# TODO: if startup failed, should shutdown skip stopListening?
# _port won't exist
if self._port is not None:
d = self._port.stopListening()
del self._port
return d
def _getPort(self):
"""
Wrapper around the appropriate listen method of the reactor.
@return: the port object returned by the listen method.
@rtype: an object providing
L{twisted.internet.interfaces.IListeningPort}.
"""
return getattr(
_maybeGlobalReactor(self.reactor),
"listen{}".format(
self.method,
),
)(*self.args, **self.kwargs)
class _AbstractClient(_VolatileDataService):
"""
@cvar volatile: list of attribute to remove from pickling.
@type volatile: C{list}
@ivar method: the type of method to call on the reactor, one of B{TCP},
B{UDP}, B{SSL} or B{UNIX}.
@type method: C{str}
@ivar reactor: the current running reactor.
@type reactor: a provider of C{IReactorTCP}, C{IReactorUDP},
C{IReactorSSL} or C{IReactorUnix}.
@ivar _connection: instance of connection set when the service is started.
@type _connection: a provider of L{twisted.internet.interfaces.IConnector}.
"""
volatile = ["_connection"]
method: str = ""
reactor = None
_connection = None
def __init__(self, *args, **kwargs):
self.args = args
if "reactor" in kwargs:
self.reactor = kwargs.pop("reactor")
self.kwargs = kwargs
def startService(self):
service.Service.startService(self)
self._connection = self._getConnection()
def stopService(self):
service.Service.stopService(self)
if self._connection is not None:
self._connection.disconnect()
del self._connection
def _getConnection(self):
"""
Wrapper around the appropriate connect method of the reactor.
@return: the port object returned by the connect method.
@rtype: an object providing L{twisted.internet.interfaces.IConnector}.
"""
return getattr(_maybeGlobalReactor(self.reactor), f"connect{self.method}")(
*self.args, **self.kwargs
)
_clientDoc = """Connect to {tran}
Call reactor.connect{tran} when the service starts, with the
arguments given to the constructor.
"""
_serverDoc = """Serve {tran} clients
Call reactor.listen{tran} when the service starts, with the
arguments given to the constructor. When the service stops,
stop listening. See twisted.internet.interfaces for documentation
on arguments to the reactor method.
"""
class TCPServer(_AbstractServer):
__doc__ = _serverDoc.format(tran="TCP")
method = "TCP"
class TCPClient(_AbstractClient):
__doc__ = _clientDoc.format(tran="TCP")
method = "TCP"
class UNIXServer(_AbstractServer):
__doc__ = _serverDoc.format(tran="UNIX")
method = "UNIX"
class UNIXClient(_AbstractClient):
__doc__ = _clientDoc.format(tran="UNIX")
method = "UNIX"
class SSLServer(_AbstractServer):
__doc__ = _serverDoc.format(tran="SSL")
method = "SSL"
class SSLClient(_AbstractClient):
__doc__ = _clientDoc.format(tran="SSL")
method = "SSL"
class UDPServer(_AbstractServer):
__doc__ = _serverDoc.format(tran="UDP")
method = "UDP"
class UNIXDatagramServer(_AbstractServer):
__doc__ = _serverDoc.format(tran="UNIXDatagram")
method = "UNIXDatagram"
class UNIXDatagramClient(_AbstractClient):
__doc__ = _clientDoc.format(tran="UNIXDatagram")
method = "UNIXDatagram"
class MulticastServer(_AbstractServer):
__doc__ = _serverDoc.format(tran="Multicast")
method = "Multicast"
class TimerService(_VolatileDataService):
"""
Service to periodically call a function
Every C{step} seconds call the given function with the given arguments.
The service starts the calls when it starts, and cancels them
when it stops.
@ivar clock: Source of time. This defaults to L{None} which is
causes L{twisted.internet.reactor} to be used.
Feel free to set this to something else, but it probably ought to be
set *before* calling L{startService}.
@type clock: L{IReactorTime<twisted.internet.interfaces.IReactorTime>}
@ivar call: Function and arguments to call periodically.
@type call: L{tuple} of C{(callable, args, kwargs)}
"""
volatile = ["_loop", "_loopFinished"]
def __init__(self, step, callable, *args, **kwargs):
"""
@param step: The number of seconds between calls.
@type step: L{float}
@param callable: Function to call
@type callable: L{callable}
@param args: Positional arguments to pass to function
@param kwargs: Keyword arguments to pass to function
"""
self.step = step
self.call = (callable, args, kwargs)
self.clock = None
def startService(self):
service.Service.startService(self)
callable, args, kwargs = self.call
# we have to make a new LoopingCall each time we're started, because
# an active LoopingCall remains active when serialized. If
# LoopingCall were a _VolatileDataService, we wouldn't need to do
# this.
self._loop = task.LoopingCall(callable, *args, **kwargs)
self._loop.clock = _maybeGlobalReactor(self.clock)
self._loopFinished = self._loop.start(self.step, now=True)
self._loopFinished.addErrback(self._failed)
def _failed(self, why):
# make a note that the LoopingCall is no longer looping, so we don't
# try to shut it down a second time in stopService. I think this
# should be in LoopingCall. -warner
self._loop.running = False
log.err(why)
def stopService(self):
"""
Stop the service.
@rtype: L{Deferred<defer.Deferred>}
@return: a L{Deferred<defer.Deferred>} which is fired when the
currently running call (if any) is finished.
"""
if self._loop.running:
self._loop.stop()
self._loopFinished.addCallback(lambda _: service.Service.stopService(self))
return self._loopFinished
class CooperatorService(service.Service):
"""
Simple L{service.IService} which starts and stops a L{twisted.internet.task.Cooperator}.
"""
def __init__(self):
self.coop = task.Cooperator(started=False)
def coiterate(self, iterator):
return self.coop.coiterate(iterator)
def startService(self):
self.coop.start()
def stopService(self):
self.coop.stop()
class StreamServerEndpointService(service.Service):
"""
A L{StreamServerEndpointService} is an L{IService} which runs a server on a
listening port described by an L{IStreamServerEndpoint
<twisted.internet.interfaces.IStreamServerEndpoint>}.
@ivar factory: A server factory which will be used to listen on the
endpoint.
@ivar endpoint: An L{IStreamServerEndpoint
<twisted.internet.interfaces.IStreamServerEndpoint>} provider
which will be used to listen when the service starts.
@ivar _waitingForPort: a Deferred, if C{listen} has yet been invoked on the
endpoint, otherwise None.
@ivar _raiseSynchronously: Defines error-handling behavior for the case
where C{listen(...)} raises an exception before C{startService} or
C{privilegedStartService} have completed.
@type _raiseSynchronously: C{bool}
@since: 10.2
"""
_raiseSynchronously = False
def __init__(self, endpoint, factory):
self.endpoint = endpoint
self.factory = factory
self._waitingForPort = None
def privilegedStartService(self):
"""
Start listening on the endpoint.
"""
service.Service.privilegedStartService(self)
self._waitingForPort = self.endpoint.listen(self.factory)
raisedNow = []
def handleIt(err):
if self._raiseSynchronously:
raisedNow.append(err)
elif not err.check(CancelledError):
log.err(err)
self._waitingForPort.addErrback(handleIt)
if raisedNow:
raisedNow[0].raiseException()
self._raiseSynchronously = False
def startService(self):
"""
Start listening on the endpoint, unless L{privilegedStartService} got
around to it already.
"""
service.Service.startService(self)
if self._waitingForPort is None:
self.privilegedStartService()
def stopService(self):
"""
Stop listening on the port if it is already listening, otherwise,
cancel the attempt to listen.
@return: a L{Deferred<twisted.internet.defer.Deferred>} which fires
with L{None} when the port has stopped listening.
"""
self._waitingForPort.cancel()
def stopIt(port):
if port is not None:
return port.stopListening()
d = self._waitingForPort.addCallback(stopIt)
def stop(passthrough):
self.running = False
return passthrough
d.addBoth(stop)
return d
__all__ = [
"TimerService",
"CooperatorService",
"MulticastServer",
"StreamServerEndpointService",
"UDPServer",
"ClientService",
"TCPServer",
"TCPClient",
"UNIXServer",
"UNIXClient",
"SSLServer",
"SSLClient",
"UNIXDatagramServer",
"UNIXDatagramClient",
"ClientService",
"backoffPolicy",
]

View File

@@ -0,0 +1,87 @@
# -*- 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 typing import Iterable, cast
from zope.interface import Attribute, Interface, implementer
from twisted.internet.interfaces import IReactorCore
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() -> None:
"""
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:
"""
@ivar moduleName: The fully-qualified Python name of the module of which
the install callable is an attribute.
"""
def __init__(self, shortName: str, moduleName: str, description: str):
self.shortName = shortName
self.moduleName = moduleName
self.description = description
def install(self) -> None:
namedAny(self.moduleName).install()
def getReactorTypes() -> Iterable[IReactorInstaller]:
"""
Return an iterator of L{IReactorInstaller} plugins.
"""
return getPlugins(IReactorInstaller)
def installReactor(shortName: str) -> IReactorCore:
"""
Install the reactor with the given C{shortName} attribute.
@raise NoSuchReactor: If no reactor is found with a matching C{shortName}.
@raise Exception: 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 cast(IReactorCore, 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,99 @@
# -*- test-case-name: twisted.application.runner.test.test_exit -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
System exit support.
"""
import typing
from enum import IntEnum
from sys import exit as sysexit, stderr, stdout
from typing import Union
try:
import posix as Status
except ImportError:
class Status: # type: ignore[no-redef]
"""
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(IntEnum):
"""
Standard exit status codes for system programs.
@cvar EX_OK: Successful termination.
@cvar EX_USAGE: Command line usage error.
@cvar EX_DATAERR: Data format error.
@cvar EX_NOINPUT: Cannot open input.
@cvar EX_NOUSER: Addressee unknown.
@cvar EX_NOHOST: Host name unknown.
@cvar EX_UNAVAILABLE: Service unavailable.
@cvar EX_SOFTWARE: Internal software error.
@cvar EX_OSERR: System error (e.g., can't fork).
@cvar EX_OSFILE: Critical OS file missing.
@cvar EX_CANTCREAT: Can't create (user) output file.
@cvar EX_IOERR: Input/output error.
@cvar EX_TEMPFAIL: Temporary failure; the user is invited to retry.
@cvar EX_PROTOCOL: Remote error in protocol.
@cvar EX_NOPERM: Permission denied.
@cvar EX_CONFIG: Configuration error.
"""
EX_OK = Status.EX_OK
EX_USAGE = Status.EX_USAGE
EX_DATAERR = Status.EX_DATAERR
EX_NOINPUT = Status.EX_NOINPUT
EX_NOUSER = Status.EX_NOUSER
EX_NOHOST = Status.EX_NOHOST
EX_UNAVAILABLE = Status.EX_UNAVAILABLE
EX_SOFTWARE = Status.EX_SOFTWARE
EX_OSERR = Status.EX_OSERR
EX_OSFILE = Status.EX_OSFILE
EX_CANTCREAT = Status.EX_CANTCREAT
EX_IOERR = Status.EX_IOERR
EX_TEMPFAIL = Status.EX_TEMPFAIL
EX_PROTOCOL = Status.EX_PROTOCOL
EX_NOPERM = Status.EX_NOPERM
EX_CONFIG = Status.EX_CONFIG
def exit(status: Union[int, ExitStatus], message: str = "") -> "typing.NoReturn":
"""
Exit the python interpreter with the given status and an optional message.
@param status: An exit status. An appropriate value from L{ExitStatus} is
recommended.
@param message: An optional message to print.
"""
if message:
if status == ExitStatus.EX_OK:
out = stdout
else:
out = stderr
out.write(message)
out.write("\n")
sysexit(status)

View File

@@ -0,0 +1,282 @@
# -*- test-case-name: twisted.application.runner.test.test_pidfile -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
PID file.
"""
from __future__ import annotations
import errno
from os import getpid, kill, name as SYSTEM_NAME
from types import TracebackType
from typing import Any, Optional, Type
from zope.interface import Interface, implementer
from twisted.logger import Logger
from twisted.python.filepath import FilePath
class IPIDFile(Interface):
"""
Manages a file that remembers a process ID.
"""
def read() -> int:
"""
Read the process ID stored in this PID file.
@return: The contained process ID.
@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() -> None:
"""
Store the PID of the current process in this PID file.
@raise EnvironmentError: If this PID file cannot be written.
"""
def remove() -> None:
"""
Remove this PID file.
@raise EnvironmentError: If this PID file cannot be removed.
"""
def isRunning() -> bool:
"""
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.
@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__() -> "IPIDFile":
"""
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: Optional[Type[BaseException]],
excValue: Optional[BaseException],
traceback: Optional[TracebackType],
) -> Optional[bool]:
"""
Exit a context using this PIDFile.
Removes the PID file.
"""
@implementer(IPIDFile)
class PIDFile:
"""
Concrete implementation of L{IPIDFile}.
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: int) -> bytes:
"""
Format a PID file's content.
@param pid: A process ID.
@return: Formatted PID file contents.
"""
return f"{int(pid)}\n".encode()
def __init__(self, filePath: FilePath[Any]) -> None:
"""
@param filePath: The path to the PID file on disk.
"""
self.filePath = filePath
def read(self) -> int:
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(
f"non-integer PID value in PID file: {pidString!r}"
)
def _write(self, pid: int) -> None:
"""
Store a PID in this PID file.
@param pid: A PID to store.
@raise EnvironmentError: If this PID file cannot be written.
"""
self.filePath.setContent(self._format(pid=pid))
def writeRunningPID(self) -> None:
self._write(getpid())
def remove(self) -> None:
self.filePath.remove()
def isRunning(self) -> bool:
try:
pid = self.read()
except NoPIDFound:
return False
if SYSTEM_NAME == "posix":
return self._pidIsRunningPOSIX(pid)
else:
raise NotImplementedError(f"isRunning is not implemented on {SYSTEM_NAME}")
@staticmethod
def _pidIsRunningPOSIX(pid: int) -> bool:
"""
POSIX implementation for running process check.
Determine whether there is a running process corresponding to the given
PID.
@param pid: The PID to check.
@return: True if the given PID is currently running; false otherwise.
@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) -> "PIDFile":
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: Optional[Type[BaseException]],
excValue: Optional[BaseException],
traceback: Optional[TracebackType],
) -> None:
self.remove()
return None
@implementer(IPIDFile)
class NonePIDFile:
"""
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) -> None:
pass
def read(self) -> int:
raise NoPIDFound("PID file does not exist")
def _write(self, pid: int) -> None:
"""
Store a PID in this PID file.
@param pid: A PID to store.
@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) -> None:
self._write(0)
def remove(self) -> None:
raise OSError(errno.ENOENT, "No such file or directory")
def isRunning(self) -> bool:
return False
def __enter__(self) -> "NonePIDFile":
return self
def __exit__(
self,
excType: Optional[Type[BaseException]],
excValue: Optional[BaseException],
traceback: Optional[TracebackType],
) -> None:
return None
nonePIDFile: IPIDFile = 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,166 @@
# -*- test-case-name: twisted.application.runner.test.test_runner -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Twisted application runner.
"""
from os import kill
from signal import SIGTERM
from sys import stderr
from typing import Any, Callable, Mapping, TextIO
from attr import Factory, attrib, attrs
from constantly import NamedConstant
from twisted.internet.interfaces import IReactorCore
from twisted.logger import (
FileLogObserver,
FilteringLogObserver,
Logger,
LogLevel,
LogLevelFilterPredicate,
globalLogBeginner,
textFileLogObserver,
)
from ._exit import ExitStatus, exit
from ._pidfile import AlreadyRunningError, InvalidPIDFileError, IPIDFile, nonePIDFile
@attrs(frozen=True)
class Runner:
"""
Twisted application runner.
@cvar _log: The logger attached to this class.
@ivar _reactor: The reactor to start and run the application in.
@ivar _pidFile: The file to store the running process ID in.
@ivar _kill: Whether this runner should kill an existing running
instance of the application.
@ivar _defaultLogLevel: The default log level to start the logging
system with.
@ivar _logFile: A file stream to write logging output to.
@ivar _fileLogObserverFactory: A factory for the file log observer to
use when starting the logging system.
@ivar _whenRunning: Hook to call after the reactor is running;
this is where the application code that relies on the reactor gets
called.
@ivar _whenRunningArguments: Keyword arguments to pass to
C{whenRunning} when it is called.
@ivar _reactorExited: Hook to call after the reactor exits.
@ivar _reactorExitedArguments: Keyword arguments to pass to
C{reactorExited} when it is called.
"""
_log = Logger()
_reactor = attrib(type=IReactorCore)
_pidFile = attrib(type=IPIDFile, default=nonePIDFile)
_kill = attrib(type=bool, default=False)
_defaultLogLevel = attrib(type=NamedConstant, default=LogLevel.info)
_logFile = attrib(type=TextIO, default=stderr)
_fileLogObserverFactory = attrib(
type=Callable[[TextIO], FileLogObserver], default=textFileLogObserver
)
_whenRunning = attrib(type=Callable[..., None], default=lambda **_: None)
_whenRunningArguments = attrib(type=Mapping[str, Any], default=Factory(dict))
_reactorExited = attrib(type=Callable[..., None], default=lambda **_: None)
_reactorExitedArguments = attrib(type=Mapping[str, Any], default=Factory(dict))
def run(self) -> None:
"""
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.")
# When testing, patched exit doesn't exit
return # type: ignore[unreachable]
def killIfRequested(self) -> None:
"""
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.")
# When testing, patched exit doesn't exit
return # type: ignore[unreachable]
try:
pid = pidFile.read()
except OSError:
exit(ExitStatus.EX_IOERR, "Unable to read PID file.")
# When testing, patched exit doesn't exit
return # type: ignore[unreachable]
except InvalidPIDFileError:
exit(ExitStatus.EX_DATAERR, "Invalid PID file.")
# When testing, patched exit doesn't exit
return # type: ignore[unreachable]
self.startLogging()
self._log.info("Terminating process: {pid}", pid=pid)
kill(pid, SIGTERM)
exit(ExitStatus.EX_OK)
# When testing, patched exit doesn't exit
return # type: ignore[unreachable]
def startLogging(self) -> None:
"""
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) -> None:
"""
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) -> None:
"""
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) -> None:
"""
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,82 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for L{twisted.application.runner._exit}.
"""
from io import StringIO
from typing import Optional, Union
import twisted.trial.unittest
from ...runner import _exit
from .._exit import ExitStatus, exit
class ExitTests(twisted.trial.unittest.TestCase):
"""
Tests for L{exit}.
"""
def setUp(self) -> None:
self.exit = DummyExit()
self.patch(_exit, "sysexit", self.exit)
def test_exitStatusInt(self) -> None:
"""
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) # type: ignore[unreachable]
def test_exitConstant(self) -> None:
"""
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) # type: ignore[unreachable]
def test_exitMessageZero(self) -> None:
"""
L{exit} given a status code of zero (C{0}) writes the given message to
standard output.
"""
out = StringIO()
self.patch(_exit, "stdout", out)
message = "Hello, world."
exit(0, message)
self.assertEqual(out.getvalue(), message + "\n") # type: ignore[unreachable]
def test_exitMessageNonZero(self) -> None:
"""
L{exit} given a non-zero status code writes the given message to
standard error.
"""
out = StringIO()
self.patch(_exit, "stderr", out)
message = "Hello, world."
exit(64, message)
self.assertEqual(out.getvalue(), message + "\n") # type: ignore[unreachable]
class DummyExit:
"""
Stub for L{sys.exit} that remembers whether it's been called and, if it
has, what argument it was given.
"""
def __init__(self) -> None:
self.exited = False
def __call__(self, arg: Optional[Union[int, str]] = None) -> None:
assert not self.exited
self.arg = arg
self.exited = True

View File

@@ -0,0 +1,419 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for L{twisted.application.runner._pidfile}.
"""
import errno
from functools import wraps
from os import getpid, name as SYSTEM_NAME
from typing import Any, Callable, Optional
from zope.interface.verify import verifyObject
from typing_extensions import NoReturn
import twisted.trial.unittest
from twisted.python.filepath import FilePath
from twisted.python.runtime import platform
from twisted.trial.unittest import SkipTest
from ...runner import _pidfile
from .._pidfile import (
AlreadyRunningError,
InvalidPIDFileError,
IPIDFile,
NonePIDFile,
NoPIDFound,
PIDFile,
StalePIDFileError,
)
def ifPlatformSupported(f: Callable[..., Any]) -> Callable[..., Any]:
"""
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.
@return: The wrapped callable.
"""
@wraps(f)
def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
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 filePath(self, content: Optional[bytes] = None) -> FilePath[str]:
filePath = FilePath(self.mktemp())
if content is not None:
filePath.setContent(content)
return filePath
def test_interface(self) -> None:
"""
L{PIDFile} conforms to L{IPIDFile}.
"""
pidFile = PIDFile(self.filePath())
verifyObject(IPIDFile, pidFile)
def test_formatWithPID(self) -> None:
"""
L{PIDFile._format} returns the expected format when given a PID.
"""
self.assertEqual(PIDFile._format(pid=1337), b"1337\n")
def test_readWithPID(self) -> None:
"""
L{PIDFile.read} returns the PID from the given file path.
"""
pid = 1337
pidFile = PIDFile(self.filePath(PIDFile._format(pid=pid)))
self.assertEqual(pid, pidFile.read())
def test_readEmptyPID(self) -> None:
"""
L{PIDFile.read} raises L{InvalidPIDFileError} when given an empty file
path.
"""
pidValue = b""
pidFile = PIDFile(self.filePath(b""))
e = self.assertRaises(InvalidPIDFileError, pidFile.read)
self.assertEqual(str(e), f"non-integer PID value in PID file: {pidValue!r}")
def test_readWithBogusPID(self) -> None:
"""
L{PIDFile.read} raises L{InvalidPIDFileError} when given an empty file
path.
"""
pidValue = b"$foo!"
pidFile = PIDFile(self.filePath(pidValue))
e = self.assertRaises(InvalidPIDFileError, pidFile.read)
self.assertEqual(str(e), f"non-integer PID value in PID file: {pidValue!r}")
def test_readDoesntExist(self) -> None:
"""
L{PIDFile.read} raises L{NoPIDFound} when given a non-existing file
path.
"""
pidFile = PIDFile(self.filePath())
e = self.assertRaises(NoPIDFound, pidFile.read)
self.assertEqual(str(e), "PID file does not exist")
def test_readOpenRaisesOSErrorNotENOENT(self) -> None:
"""
L{PIDFile.read} re-raises L{OSError} if the associated C{errno} is
anything other than L{errno.ENOENT}.
"""
def oops(mode: str = "r") -> NoReturn:
raise OSError(errno.EIO, "I/O error")
self.patch(FilePath, "open", oops)
pidFile = PIDFile(self.filePath())
error = self.assertRaises(OSError, pidFile.read)
self.assertEqual(error.errno, errno.EIO)
def test_writePID(self) -> None:
"""
L{PIDFile._write} stores the given PID.
"""
pid = 1995
pidFile = PIDFile(self.filePath())
pidFile._write(pid)
self.assertEqual(pidFile.read(), pid)
def test_writePIDInvalid(self) -> None:
"""
L{PIDFile._write} raises L{ValueError} when given an invalid PID.
"""
pidFile = PIDFile(self.filePath())
self.assertRaises(ValueError, pidFile._write, "burp")
def test_writeRunningPID(self) -> None:
"""
L{PIDFile.writeRunningPID} stores the PID for the current process.
"""
pidFile = PIDFile(self.filePath())
pidFile.writeRunningPID()
self.assertEqual(pidFile.read(), getpid())
def test_remove(self) -> None:
"""
L{PIDFile.remove} removes the PID file.
"""
pidFile = PIDFile(self.filePath(b""))
self.assertTrue(pidFile.filePath.exists())
pidFile.remove()
self.assertFalse(pidFile.filePath.exists())
@ifPlatformSupported
def test_isRunningDoesExist(self) -> None:
"""
L{PIDFile.isRunning} returns true for a process that does exist.
"""
pidFile = PIDFile(self.filePath())
pidFile._write(1337)
def kill(pid: int, signal: int) -> None:
return # Don't actually kill anything
self.patch(_pidfile, "kill", kill)
self.assertTrue(pidFile.isRunning())
@ifPlatformSupported
def test_isRunningThis(self) -> None:
"""
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(self.filePath())
pidFile.writeRunningPID()
self.assertTrue(pidFile.isRunning())
@ifPlatformSupported
def test_isRunningDoesNotExist(self) -> None:
"""
L{PIDFile.isRunning} raises L{StalePIDFileError} for a process that
does not exist (errno=ESRCH).
"""
pidFile = PIDFile(self.filePath())
pidFile._write(1337)
def kill(pid: int, signal: int) -> None:
raise OSError(errno.ESRCH, "No such process")
self.patch(_pidfile, "kill", kill)
self.assertRaises(StalePIDFileError, pidFile.isRunning)
@ifPlatformSupported
def test_isRunningNotAllowed(self) -> None:
"""
L{PIDFile.isRunning} returns true for a process that we are not allowed
to kill (errno=EPERM).
"""
pidFile = PIDFile(self.filePath())
pidFile._write(1337)
def kill(pid: int, signal: int) -> None:
raise OSError(errno.EPERM, "Operation not permitted")
self.patch(_pidfile, "kill", kill)
self.assertTrue(pidFile.isRunning())
@ifPlatformSupported
def test_isRunningInit(self) -> None:
"""
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(self.filePath())
pidFile._write(1) # PID 1 is init on POSIX systems
self.assertTrue(pidFile.isRunning())
@ifPlatformSupported
def test_isRunningUnknownErrno(self) -> None:
"""
L{PIDFile.isRunning} re-raises L{OSError} if the attached C{errno}
value from L{os.kill} is not an expected one.
"""
pidFile = PIDFile(self.filePath())
pidFile.writeRunningPID()
def kill(pid: int, signal: int) -> None:
raise OSError(errno.EEXIST, "File exists")
self.patch(_pidfile, "kill", kill)
self.assertRaises(OSError, pidFile.isRunning)
def test_isRunningNoPIDFile(self) -> None:
"""
L{PIDFile.isRunning} returns false if the PID file doesn't exist.
"""
pidFile = PIDFile(self.filePath())
self.assertFalse(pidFile.isRunning())
def test_contextManager(self) -> None:
"""
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(self.filePath())
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) -> None:
"""
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(self.filePath())
pidFile._write(1337)
def kill(pid: int, signal: int) -> None:
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) -> None:
"""
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(self.filePath())
pidFile._write(1337)
def kill(pid: int, signal: int) -> None:
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) -> None:
"""
L{NonePIDFile} conforms to L{IPIDFile}.
"""
pidFile = NonePIDFile()
verifyObject(IPIDFile, pidFile)
def test_read(self) -> None:
"""
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) -> None:
"""
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) -> None:
"""
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) -> None:
"""
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) -> None:
"""
L{NonePIDFile.isRunning} returns L{False}.
"""
pidFile = NonePIDFile()
self.assertEqual(pidFile.isRunning(), False)
def test_contextManager(self) -> None:
"""
When used as a context manager, a L{NonePIDFile} doesn't raise, despite
not existing.
"""
pidFile = NonePIDFile()
with pidFile:
pass

View File

@@ -0,0 +1,454 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for L{twisted.application.runner._runner}.
"""
import errno
from io import StringIO
from signal import SIGTERM
from types import TracebackType
from typing import Any, Iterable, List, Optional, TextIO, Tuple, Type, Union, cast
from attr import Factory, attrib, attrs
import twisted.trial.unittest
from twisted.internet.testing import MemoryReactor
from twisted.logger import (
FileLogObserver,
FilteringLogObserver,
ILogObserver,
LogBeginner,
LogLevel,
LogLevelFilterPredicate,
LogPublisher,
)
from twisted.python.filepath import FilePath
from ...runner import _runner
from .._exit import ExitStatus
from .._pidfile import NonePIDFile, PIDFile
from .._runner import Runner
class RunnerTests(twisted.trial.unittest.TestCase):
"""
Tests for L{Runner}.
"""
def filePath(self, content: Optional[bytes] = None) -> FilePath[str]:
filePath = FilePath(self.mktemp())
if content is not None:
filePath.setContent(content)
return filePath
def setUp(self) -> None:
# 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 = f"{self.pid}\n".encode()
# Patch globalLogBeginner so that we aren't trying to install multiple
# global log observers.
self.stdout = StringIO()
self.stderr = StringIO()
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) -> None:
"""
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) -> None:
"""
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) -> None:
"""
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(self.filePath(self.pidFileContent))
pidFile.isRunning = lambda: True # type: ignore[method-assign]
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) -> None:
"""
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) -> None:
"""
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) -> None:
"""
L{Runner.killIfRequested} when C{kill} is true and given a C{pidFile}
performs a targeted killing of the appropriate process.
"""
pidFile = PIDFile(self.filePath(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) -> None:
"""
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(self.filePath(None))
def read() -> int:
raise OSError(errno.EACCES, "Permission denied")
pidFile.read = read # type: ignore[method-assign]
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) -> None:
"""
L{Runner.killIfRequested} when C{kill} is true and given a C{pidFile}
containing no value exits with L{ExitStatus.EX_DATAERR}.
"""
pidFile = PIDFile(self.filePath(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) -> None:
"""
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(self.filePath(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) -> None:
"""
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 = StringIO()
# Patch the log beginner so that we don't try to start the already
# running (started by trial) logging system.
class LogBeginner:
observers: List[ILogObserver] = []
def beginLoggingTo(self, observers: Iterable[ILogObserver]) -> None:
LogBeginner.observers = list(observers)
self.patch(_runner, "globalLogBeginner", LogBeginner())
# Patch FilteringLogObserver so we can capture its arguments
class MockFilteringLogObserver(FilteringLogObserver):
observer: Optional[ILogObserver] = None
predicates: List[LogLevelFilterPredicate] = []
def __init__(
self,
observer: ILogObserver,
predicates: Iterable[LogLevelFilterPredicate],
negativeObserver: ILogObserver = cast(ILogObserver, lambda event: None),
) -> None:
MockFilteringLogObserver.observer = observer
MockFilteringLogObserver.predicates = list(predicates)
FilteringLogObserver.__init__(
self, observer, predicates, negativeObserver
)
self.patch(_runner, "FilteringLogObserver", MockFilteringLogObserver)
# Patch FileLogObserver so we can capture its arguments
class MockFileLogObserver(FileLogObserver):
outFile: Optional[TextIO] = None
def __init__(self, outFile: TextIO) -> None:
MockFileLogObserver.outFile = outFile
FileLogObserver.__init__(self, outFile, str)
# Start logging
runner = Runner(
reactor=MemoryReactor(),
defaultLogLevel=LogLevel.critical,
logFile=logFile,
[AWS-SECRET-REMOVED]er,
)
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
observer = cast(MockFileLogObserver, MockFilteringLogObserver.observer)
self.assertIsInstance(observer, MockFileLogObserver)
# Check for the file we gave it
self.assertIdentical(observer.outFile, logFile)
def test_startReactorWithReactor(self) -> None:
"""
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) -> None:
"""
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) -> None:
"""
L{Runner.whenRunning} calls C{whenRunning} with
C{whenRunningArguments}.
"""
self._testHook("whenRunning")
def test_reactorExitedWithArguments(self) -> None:
"""
L{Runner.whenRunning} calls C{reactorExited} with
C{reactorExitedArguments}.
"""
self._testHook("reactorExited")
def _testHook(self, methodName: str, callerName: Optional[str] = None) -> 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.
@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.
"""
if callerName is None:
callerName = methodName
arguments = dict(a=object(), b=object(), c=object())
argumentsSeen = []
def hook(**arguments: object) -> None:
argumentsSeen.append(arguments)
runnerArguments = {
methodName: hook,
f"{methodName}Arguments": arguments.copy(),
}
runner = Runner(
reactor=MemoryReactor(), **runnerArguments # type: ignore[arg-type]
)
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(type=List[str], default=Factory(list))
def killIfRequested(self) -> None:
self.calledMethods.append("killIfRequested")
def startLogging(self) -> None:
self.calledMethods.append("startLogging")
def startReactor(self) -> None:
self.calledMethods.append("startReactor")
def reactorExited(self) -> None:
self.calledMethods.append("reactorExited")
class DummyPIDFile(NonePIDFile):
"""
Stub for L{PIDFile}.
Tracks context manager entry/exit without doing anything.
"""
def __init__(self) -> None:
NonePIDFile.__init__(self)
self.entered = False
self.exited = False
def __enter__(self) -> "DummyPIDFile":
self.entered = True
return self
def __exit__(
self,
excType: Optional[Type[BaseException]],
excValue: Optional[BaseException],
traceback: Optional[TracebackType],
) -> None:
self.exited = True
class DummyExit:
"""
Stub for L{_exit.exit} that remembers whether it's been called and, if it has,
what arguments it was given.
"""
def __init__(self) -> None:
self.exited = False
def __call__(
self, status: Union[int, ExitStatus], message: Optional[str] = None
) -> None:
assert not self.exited
self.status = status
self.message = message
self.exited = True
class DummyKill:
"""
Stub for L{os.kill} that remembers whether it's been called and, if it has,
what arguments it was given.
"""
def __init__(self) -> None:
self.calls: List[Tuple[int, int]] = []
def __call__(self, pid: int, sig: int) -> None:
self.calls.append((pid, sig))
class DummyStandardIO:
"""
Stub for L{sys} which provides L{StringIO} streams as stdout and stderr.
"""
def __init__(self, stdout: TextIO, stderr: TextIO) -> None:
self.stdout = stdout
self.stderr = stderr
class DummyWarningsModule:
"""
Stub for L{warnings} which provides a C{showwarning} method that is a no-op.
"""
def showwarning(*args: Any, **kwargs: Any) -> None:
"""
Do nothing.
@param args: ignored.
@param kwargs: ignored.
"""

View File

@@ -0,0 +1,420 @@
# -*- 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 zope.interface import Attribute, Interface, implementer
from twisted.internet import defer
from twisted.persisted import sob
from twisted.plugin import IPlugin
from twisted.python import components
from twisted.python.reflect import namedAny
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:
"""
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
@property
def options(self):
return namedAny(self.module).Options
@property
def makeService(self):
return namedAny(self.module).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:
"""
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)
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,83 @@
# -*- 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 typing import Optional, cast
from twisted.application.internet import StreamServerEndpointService
from twisted.internet import endpoints, interfaces
def _getReactor() -> interfaces.IReactorCore:
from twisted.internet import reactor
return cast(interfaces.IReactorCore, reactor)
def service(
description: str,
factory: interfaces.IProtocolFactory,
reactor: Optional[interfaces.IReactorCore] = None,
) -> StreamServerEndpointService:
"""
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:
reactor = _getReactor()
svc = StreamServerEndpointService(
endpoints.serverFromString(reactor, description), factory
)
svc._raiseSynchronously = True
return svc
def listen(
description: str, factory: interfaces.IProtocolFactory
) -> interfaces.IListeningPort:
"""
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 cast(
interfaces.IListeningPort, 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,175 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for L{twisted.application.service}.
"""
from zope.interface import implementer
from zope.interface.exceptions import BrokenImplementation
from zope.interface.verify import verifyObject
from twisted.application.service import (
Application,
IProcess,
IService,
IServiceCollection,
Service,
)
from twisted.persisted.sob import IPersistable
from twisted.trial.unittest import TestCase
@implementer(IService)
class AlmostService:
"""
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: str, parent: IServiceCollection, running: bool) -> None:
self.name = name
self.parent = parent
self.running = running
def makeInvalidByDeletingName(self) -> None:
"""
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) -> None:
"""
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) -> None:
"""
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: object) -> None:
"""
See L{twisted.application.service.IService}.
@param name: ignored
"""
def setServiceParent(self, parent: object) -> None:
"""
See L{twisted.application.service.IService}.
@param parent: ignored
"""
def disownServiceParent(self) -> None:
"""
See L{twisted.application.service.IService}.
"""
def privilegedStartService(self) -> None:
"""
See L{twisted.application.service.IService}.
"""
def startService(self) -> None:
"""
See L{twisted.application.service.IService}.
"""
def stopService(self) -> None:
"""
See L{twisted.application.service.IService}.
"""
class ServiceInterfaceTests(TestCase):
"""
Tests for L{twisted.application.service.IService} implementation.
"""
def setUp(self) -> None:
"""
Build something that implements IService.
"""
self.almostService = AlmostService(parent=None, running=False, name=None) # type: ignore[arg-type]
def test_realService(self) -> None:
"""
Service implements IService.
"""
myService = Service()
verifyObject(IService, myService)
def test_hasAll(self) -> None:
"""
AlmostService implements IService.
"""
verifyObject(IService, self.almostService)
def test_noName(self) -> None:
"""
AlmostService with no name does not implement IService.
"""
self.almostService.makeInvalidByDeletingName()
with self.assertRaises(BrokenImplementation):
verifyObject(IService, self.almostService)
def test_noParent(self) -> None:
"""
AlmostService with no parent does not implement IService.
"""
self.almostService.makeInvalidByDeletingParent()
with self.assertRaises(BrokenImplementation):
verifyObject(IService, self.almostService)
def test_noRunning(self) -> None:
"""
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) -> None:
"""
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,207 @@
# -*- test-case-name: twisted.application.twist.test.test_options -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Command line options for C{twist}.
"""
import typing
from sys import stderr, stdout
from textwrap import dedent
from typing import Callable, Iterable, Mapping, Optional, Sequence, Tuple, cast
from twisted.copyright import version
from twisted.internet.interfaces import IReactorCore
from twisted.logger import (
InvalidLogLevelError,
LogLevel,
jsonFileLogObserver,
textFileLogObserver,
)
from twisted.plugin import getPlugins
from twisted.python.usage import Options, UsageError
from ..reactors import NoSuchReactor, getReactorTypes, installReactor
from ..runner._exit import ExitStatus, exit
from ..service import IServiceMaker
openFile = open
def _update_doc(opt: Callable[["TwistOptions", str], None], **kwargs: str) -> None:
"""
Update the docstring of a method that implements an option.
The string is dedented and the given keyword arguments are substituted.
"""
opt.__doc__ = dedent(opt.__doc__ or "").format(**kwargs)
class TwistOptions(Options):
"""
Command line options for C{twist}.
"""
defaultReactorName = "default"
defaultLogLevel = LogLevel.info
def __init__(self) -> None:
Options.__init__(self)
self["reactorName"] = self.defaultReactorName
self["logLevel"] = self.defaultLogLevel
self["logFile"] = stdout
# An empty long description is explicitly set here as otherwise
# when executing from distributed trial twisted.python.usage will
# pull the description from `__main__` which is another entry point.
self.longdesc = ""
def getSynopsis(self) -> str:
return f"{Options.getSynopsis(self)} plugin [plugin_options]"
def opt_version(self) -> "typing.NoReturn":
"""
Print version and exit.
"""
exit(ExitStatus.EX_OK, f"{version}")
def opt_reactor(self, name: str) -> None:
"""
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(f"Unknown reactor: {name}")
else:
self["reactorName"] = name
_update_doc(
opt_reactor,
options=", ".join(f'"{rt.shortName}"' for rt in getReactorTypes()),
)
def installReactor(self, name: str) -> IReactorCore:
"""
Install the reactor.
"""
if name == self.defaultReactorName:
from twisted.internet import reactor
return cast(IReactorCore, reactor)
else:
return installReactor(name)
def opt_log_level(self, levelName: str) -> None:
"""
Set default log level.
(options: {options}; default: "{default}")
"""
try:
self["logLevel"] = LogLevel.levelWithName(levelName)
except InvalidLogLevelError:
raise UsageError(f"Invalid log level: {levelName}")
_update_doc(
opt_log_level,
options=", ".join(
f'"{constant.name}"' for constant in LogLevel.iterconstants()
),
default=defaultLogLevel.name,
)
def opt_log_file(self, fileName: str) -> None:
"""
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 OSError as e:
exit(
ExitStatus.EX_IOERR,
f"Unable to open log file {fileName!r}: {e}",
)
def opt_log_format(self, format: str) -> None:
"""
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(f"Invalid log format: {format}")
self["logFormat"] = format
_update_doc(opt_log_format)
def selectDefaultLogObserver(self) -> None:
"""
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: Optional[Sequence[str]] = None) -> None:
self.selectDefaultLogObserver()
Options.parseOptions(self, options=options)
if "reactor" not in self:
self["reactor"] = self.installReactor(self["reactorName"])
@property
def plugins(self) -> Mapping[str, IServiceMaker]:
if "plugins" not in self:
plugins = {}
for plugin in getPlugins(IServiceMaker):
plugins[plugin.tapname] = plugin
self["plugins"] = plugins
return cast(Mapping[str, IServiceMaker], self["plugins"])
@property
def subCommands(
self,
) -> Iterable[Tuple[str, None, Callable[[IServiceMaker], Options], str]]:
plugins = self.plugins
for name in sorted(plugins):
plugin = plugins[name]
# Don't pass plugin.options along in order to avoid resolving the
# options attribute right away, in case it's a property with a
# non-trivial getter (eg, one which imports modules).
def options(plugin: IServiceMaker = plugin) -> Options:
return cast(Options, plugin.options())
yield (plugin.tapname, None, options, plugin.description)
def postOptions(self) -> None:
Options.postOptions(self)
if self.subCommand is None:
raise UsageError("No plugin specified.")

View File

@@ -0,0 +1,114 @@
# -*- 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 typing import Sequence
from twisted.application.app import _exitWithSignal
from twisted.internet.interfaces import IReactorCore, _ISupportsExitSignalCapturing
from twisted.python.usage import Options, UsageError
from ..runner._exit import ExitStatus, exit
from ..runner._runner import Runner
from ..service import Application, IService, IServiceMaker
from ._options import TwistOptions
class Twist:
"""
Run a Twisted application.
"""
@staticmethod
def options(argv: Sequence[str]) -> TwistOptions:
"""
Parse command line options.
@param argv: Command line arguments.
@return: The parsed options.
"""
options = TwistOptions()
try:
options.parseOptions(argv[1:])
except UsageError as e:
exit(ExitStatus.EX_USAGE, f"Error: {e}\n\n{options}")
return options
@staticmethod
def service(plugin: IServiceMaker, options: Options) -> IService:
"""
Create the application service.
@param plugin: The name of the plugin that implements the service
application to run.
@param options: Options to pass to the application.
@return: The created application service.
"""
service = plugin.makeService(options)
application = Application(plugin.tapname)
service.setServiceParent(application)
return IService(application)
@staticmethod
def startService(reactor: IReactorCore, service: IService) -> None:
"""
Start the application service.
@param reactor: The reactor to run the service with.
@param service: The application service to run.
"""
service.startService()
# Ask the reactor to stop the service before shutting down
reactor.addSystemEventTrigger("before", "shutdown", service.stopService)
@staticmethod
def run(twistOptions: TwistOptions) -> None:
"""
Run the application service.
@param twistOptions: Command line options to convert to runner
arguments.
"""
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: Sequence[str] = sys.argv) -> None:
"""
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"]
# If subCommand is None, TwistOptions.parseOptions() raises UsageError
# and Twist.options() will exit the runner, so we'll never get here.
subCommand = options.subCommand
assert subCommand is not None
service = cls.service(
plugin=options.plugins[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,355 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for L{twisted.application.twist._options}.
"""
from sys import stderr, stdout
from typing import Callable, Dict, List, Optional, TextIO, Tuple
import twisted.trial.unittest
from twisted.copyright import version
from twisted.internet import reactor
from twisted.internet.interfaces import IReactorCore
from twisted.internet.testing import MemoryReactor
from twisted.logger import (
FileLogObserver,
LogLevel,
jsonFileLogObserver,
textFileLogObserver,
)
from twisted.python.usage import UsageError
from ...reactors import NoSuchReactor
from ...runner._exit import ExitStatus
from ...runner.test.test_runner import DummyExit
from ...service import ServiceMaker
from ...twist import _options
from .._options import TwistOptions
class OptionsTests(twisted.trial.unittest.TestCase):
"""
Tests for L{TwistOptions}.
"""
def patchExit(self) -> None:
"""
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) -> None:
"""
Patch L{_options.open} so we can capture usage and prevent actual opens.
"""
self.opened: List[Tuple[str, Optional[str]]] = []
def fakeOpen(name: str, mode: Optional[str] = None) -> TextIO:
if name == "nocanopen":
raise OSError(None, None, name)
self.opened.append((name, mode))
return NotImplemented
self.patch(_options, "openFile", fakeOpen)
def patchInstallReactor(self) -> None:
"""
Patch C{_options.installReactor} so we can capture usage and prevent
actual installs.
"""
self.installedReactors: Dict[str, IReactorCore] = {}
def installReactor(name: str) -> IReactorCore:
if name != "fusion":
raise NoSuchReactor()
reactor = MemoryReactor()
self.installedReactors[name] = reactor
return reactor
self.patch(_options, "installReactor", installReactor)
def test_synopsis(self) -> None:
"""
L{TwistOptions.getSynopsis} appends arguments.
"""
options = TwistOptions()
self.assertTrue(options.getSynopsis().endswith(" plugin [plugin_options]"))
def test_version(self) -> None:
"""
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) # type: ignore[unreachable]
self.assertEquals(self.exit.message, version)
def test_reactor(self) -> None:
"""
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), {"fusion"})
self.assertEquals(options["reactorName"], "fusion")
def test_installCorrectReactor(self) -> None:
"""
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), {"fusion"})
def test_installReactorBogus(self) -> None:
"""
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) -> None:
"""
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) -> None:
"""
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) -> None:
"""
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: str, expectedStream: TextIO) -> None:
"""
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) -> None:
"""
L{TwistOptions.opt_log_file} given C{"-"} as a file name uses stdout.
"""
self._testLogFile("-", stdout)
def test_logFileStderr(self) -> None:
"""
L{TwistOptions.opt_log_file} given C{"+"} as a file name uses stderr.
"""
self._testLogFile("+", stderr)
def test_logFileNamed(self) -> None:
"""
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) -> None:
"""
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.assertIsNotNone(self.exit.message)
self.assertTrue(
self.exit.message.startswith( # type: ignore[union-attr]
"Unable to open log file 'nocanopen': "
)
)
def _testLogFormat(
self, format: str, expectedObserverFactory: Callable[[TextIO], FileLogObserver]
) -> None:
"""
Set log file format and check the selected observer factory.
@param format: The format of the file.
@param expectedObserverFactory: The expected observer factory.
"""
options = TwistOptions()
options.opt_log_format(format)
self.assertIdentical(options["fileLogObserverFactory"], expectedObserverFactory)
self.assertEqual(options["logFormat"], format)
def test_logFormatText(self) -> None:
"""
L{TwistOptions.opt_log_format} given C{"text"} uses a
L{textFileLogObserver}.
"""
self._testLogFormat("text", textFileLogObserver)
def test_logFormatJSON(self) -> None:
"""
L{TwistOptions.opt_log_format} given C{"text"} uses a
L{textFileLogObserver}.
"""
self._testLogFormat("json", jsonFileLogObserver)
def test_logFormatInvalid(self) -> None:
"""
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) -> None:
"""
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) -> None:
"""
L{TwistOptions.selectDefaultLogObserver} will not override an already
selected observer.
"""
class TTYFile:
def isatty(self) -> bool:
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_[AWS-SECRET-REMOVED]Y(self) -> None:
"""
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) -> None:
"""
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) -> None:
"""
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) -> None:
"""
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) -> None:
"""
L{TwistOptions.subCommands} includes a sub-command for every plug-in.
"""
options = TwistOptions()
plugins = set(options.plugins)
subCommands = {name for name, shortcut, parser, doc in options.subCommands}
self.assertEqual(subCommands, plugins)
def test_postOptionsNoSubCommand(self) -> None:
"""
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,256 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for L{twisted.application.twist._twist}.
"""
from sys import stdout
from typing import Any, Dict, List
import twisted.trial.unittest
from twisted.internet.interfaces import IReactorCore
from twisted.internet.testing import MemoryReactor
from twisted.logger import LogLevel, jsonFileLogObserver
from twisted.test.test_twistd import SignalCapturingMemoryReactor
from ...runner._exit import ExitStatus
from ...runner._runner import Runner
from ...runner.test.test_runner import DummyExit
from ...service import IService, MultiService
from ...twist import _twist
from .._options import TwistOptions
from .._twist import Twist
class TwistTests(twisted.trial.unittest.TestCase):
"""
Tests for L{Twist}.
"""
def setUp(self) -> None:
self.patchInstallReactor()
def patchExit(self) -> None:
"""
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) -> None:
"""
Patch C{_options.installReactor} so we can capture usage and prevent
actual installs.
"""
self.installedReactors: Dict[str, IReactorCore] = {}
def installReactor(_: TwistOptions, name: str) -> IReactorCore:
reactor = MemoryReactor()
self.installedReactors[name] = reactor
return reactor
self.patch(TwistOptions, "installReactor", installReactor)
def patchStartService(self) -> None:
"""
Patch L{MultiService.startService} so we can capture usage and prevent
actual starts.
"""
self.serviceStarts: List[IService] = []
def startService(service: IService) -> None:
self.serviceStarts.append(service)
self.patch(MultiService, "startService", startService)
def test_optionsValidArguments(self) -> None:
"""
L{Twist.options} given valid arguments returns options.
"""
options = Twist.options(["twist", "web"])
self.assertIsInstance(options, TwistOptions)
def test_optionsInvalidArguments(self) -> None:
"""
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.assertIsNotNone(self.exit.message)
self.assertTrue(
self.exit.message.startswith("Error: ") # type: ignore[union-attr]
)
self.assertTrue(
self.exit.message.endswith( # type: ignore[union-attr]
f"\n\n{TwistOptions()}"
)
)
def test_service(self) -> None:
"""
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) -> None:
"""
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"]
subCommand = options.subCommand
assert subCommand is not None
service = Twist.service(
plugin=options.plugins[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) -> None:
"""
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,
[AWS-SECRET-REMOVED]er,
),
)
def test_main(self) -> None:
"""
L{Twist.main} runs the runner with arguments corresponding to the given
command line arguments.
"""
self.patchStartService()
runners = []
class Runner:
def __init__(self, **kwargs: Any) -> None:
self.args = kwargs
self.runs = 0
runners.append(self)
def run(self) -> None:
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,
[AWS-SECRET-REMOVED]er,
),
)
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) -> None:
self.exitWithSignalCalled = False
def fakeExitWithSignal(sig: int) -> None:
"""
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(_: Runner) -> None:
"""
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) -> None:
"""
_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) -> None:
"""
_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) -> None:
"""
_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)