mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 15:11:09 -05:00
okay fine
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Configuration objects for Twisted Applications.
|
||||
"""
|
||||
@@ -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()
|
||||
706
.venv/lib/python3.12/site-packages/twisted/application/app.py
Normal file
706
.venv/lib/python3.12/site-packages/twisted/application/app.py
Normal 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)
|
||||
@@ -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",
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,7 @@
|
||||
# -*- test-case-name: twisted.application.runner.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Facilities for running a Twisted application.
|
||||
"""
|
||||
@@ -0,0 +1,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)
|
||||
@@ -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.
|
||||
"""
|
||||
@@ -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)
|
||||
@@ -0,0 +1,7 @@
|
||||
# -*- test-case-name: twisted.application.runner.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.application.runner}.
|
||||
"""
|
||||
@@ -0,0 +1,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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
"""
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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"]
|
||||
@@ -0,0 +1,6 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.internet.application}.
|
||||
"""
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,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)))
|
||||
@@ -0,0 +1,7 @@
|
||||
# -*- test-case-name: twisted.application.twist.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
C{twist} command line tool.
|
||||
"""
|
||||
@@ -0,0 +1,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.")
|
||||
@@ -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)
|
||||
@@ -0,0 +1,7 @@
|
||||
# -*- test-case-name: twisted.application.twist.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.application.twist}.
|
||||
"""
|
||||
@@ -0,0 +1,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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user