RAHHH
This commit is contained in:
12
backend/lib/python3.12/site-packages/twisted/__init__.py
Normal file
12
backend/lib/python3.12/site-packages/twisted/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# -*- test-case-name: twisted -*-
|
||||
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Twisted: The Framework Of Your Internet.
|
||||
"""
|
||||
|
||||
from twisted._version import __version__ as version
|
||||
|
||||
__version__ = version.short()
|
||||
14
backend/lib/python3.12/site-packages/twisted/__main__.py
Normal file
14
backend/lib/python3.12/site-packages/twisted/__main__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
# Make the twisted module executable with the default behaviour of
|
||||
# running twist.
|
||||
# This is not a docstring to avoid changing the string output of twist.
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
from twisted.application.twist._twist import Twist
|
||||
|
||||
sys.exit(Twist.main())
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
# -*- test-case-name: twisted.test.test_paths -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Twisted integration with operating system threads.
|
||||
"""
|
||||
|
||||
|
||||
from ._ithreads import AlreadyQuit, IWorker
|
||||
from ._memory import createMemoryWorker
|
||||
from ._pool import pool
|
||||
from ._team import Team
|
||||
from ._threadworker import LockWorker, ThreadWorker
|
||||
|
||||
__all__ = [
|
||||
"ThreadWorker",
|
||||
"LockWorker",
|
||||
"IWorker",
|
||||
"AlreadyQuit",
|
||||
"Team",
|
||||
"createMemoryWorker",
|
||||
"pool",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,43 @@
|
||||
# -*- test-case-name: twisted._threads.test.test_convenience -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Common functionality used within the implementation of various workers.
|
||||
"""
|
||||
|
||||
|
||||
from ._ithreads import AlreadyQuit
|
||||
|
||||
|
||||
class Quit:
|
||||
"""
|
||||
A flag representing whether a worker has been quit.
|
||||
|
||||
@ivar isSet: Whether this flag is set.
|
||||
@type isSet: L{bool}
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Create a L{Quit} un-set.
|
||||
"""
|
||||
self.isSet = False
|
||||
|
||||
def set(self) -> None:
|
||||
"""
|
||||
Set the flag if it has not been set.
|
||||
|
||||
@raise AlreadyQuit: If it has been set.
|
||||
"""
|
||||
self.check()
|
||||
self.isSet = True
|
||||
|
||||
def check(self) -> None:
|
||||
"""
|
||||
Check if the flag has been set.
|
||||
|
||||
@raise AlreadyQuit: If it has been set.
|
||||
"""
|
||||
if self.isSet:
|
||||
raise AlreadyQuit()
|
||||
@@ -0,0 +1,61 @@
|
||||
# -*- test-case-name: twisted._threads.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Interfaces related to threads.
|
||||
"""
|
||||
|
||||
|
||||
from typing import Callable
|
||||
|
||||
from zope.interface import Interface
|
||||
|
||||
|
||||
class AlreadyQuit(Exception):
|
||||
"""
|
||||
This worker worker is dead and cannot execute more instructions.
|
||||
"""
|
||||
|
||||
|
||||
class IWorker(Interface):
|
||||
"""
|
||||
A worker that can perform some work concurrently.
|
||||
|
||||
All methods on this interface must be thread-safe.
|
||||
"""
|
||||
|
||||
def do(task: Callable[[], None]) -> None:
|
||||
"""
|
||||
Perform the given task.
|
||||
|
||||
As an interface, this method makes no specific claims about concurrent
|
||||
execution. An L{IWorker}'s C{do} implementation may defer execution
|
||||
for later on the same thread, immediately on a different thread, or
|
||||
some combination of the two. It is valid for a C{do} method to
|
||||
schedule C{task} in such a way that it may never be executed.
|
||||
|
||||
It is important for some implementations to provide specific properties
|
||||
with respect to where C{task} is executed, of course, and client code
|
||||
may rely on a more specific implementation of C{do} than L{IWorker}.
|
||||
|
||||
@param task: a task to call in a thread or other concurrent context.
|
||||
@type task: 0-argument callable
|
||||
|
||||
@raise AlreadyQuit: if C{quit} has been called.
|
||||
"""
|
||||
|
||||
def quit() -> None:
|
||||
"""
|
||||
Free any resources associated with this L{IWorker} and cause it to
|
||||
reject all future work.
|
||||
|
||||
@raise AlreadyQuit: if this method has already been called.
|
||||
"""
|
||||
|
||||
|
||||
class IExclusiveWorker(IWorker):
|
||||
"""
|
||||
Like L{IWorker}, but with the additional guarantee that the callables
|
||||
passed to C{do} will not be called exclusively with each other.
|
||||
"""
|
||||
@@ -0,0 +1,83 @@
|
||||
# -*- test-case-name: twisted._threads.test.test_memory -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Implementation of an in-memory worker that defers execution.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum, auto
|
||||
from typing import Callable, Literal
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from ._convenience import Quit
|
||||
from ._ithreads import IExclusiveWorker
|
||||
|
||||
|
||||
class NoMore(Enum):
|
||||
Work = auto()
|
||||
|
||||
|
||||
NoMoreWork = NoMore.Work
|
||||
|
||||
|
||||
@implementer(IExclusiveWorker)
|
||||
class MemoryWorker:
|
||||
"""
|
||||
An L{IWorker} that queues work for later performance.
|
||||
|
||||
@ivar _quit: a flag indicating
|
||||
@type _quit: L{Quit}
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
pending: Callable[[], list[Callable[[], object] | Literal[NoMore.Work]]] = list,
|
||||
) -> None:
|
||||
"""
|
||||
Create a L{MemoryWorker}.
|
||||
"""
|
||||
self._quit = Quit()
|
||||
self._pending = pending()
|
||||
|
||||
def do(self, work: Callable[[], object]) -> None:
|
||||
"""
|
||||
Queue some work for to perform later; see L{createMemoryWorker}.
|
||||
|
||||
@param work: The work to perform.
|
||||
"""
|
||||
self._quit.check()
|
||||
self._pending.append(work)
|
||||
|
||||
def quit(self) -> None:
|
||||
"""
|
||||
Quit this worker.
|
||||
"""
|
||||
self._quit.set()
|
||||
self._pending.append(NoMoreWork)
|
||||
|
||||
|
||||
def createMemoryWorker() -> tuple[MemoryWorker, Callable[[], bool]]:
|
||||
"""
|
||||
Create an L{IWorker} that does nothing but defer work, to be performed
|
||||
later.
|
||||
|
||||
@return: a worker that will enqueue work to perform later, and a callable
|
||||
that will perform one element of that work.
|
||||
"""
|
||||
|
||||
def perform() -> bool:
|
||||
if not worker._pending:
|
||||
return False
|
||||
peek = worker._pending[0]
|
||||
if peek is NoMoreWork:
|
||||
return False
|
||||
worker._pending.pop(0)
|
||||
peek()
|
||||
return True
|
||||
|
||||
worker = MemoryWorker()
|
||||
return (worker, perform)
|
||||
@@ -0,0 +1,73 @@
|
||||
# -*- test-case-name: twisted._threads.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Top level thread pool interface, used to implement
|
||||
L{twisted.python.threadpool}.
|
||||
"""
|
||||
|
||||
|
||||
from queue import Queue
|
||||
from threading import Lock, Thread, local as LocalStorage
|
||||
from typing import Callable, Optional
|
||||
|
||||
from typing_extensions import Protocol
|
||||
|
||||
from twisted.python.log import err
|
||||
from ._ithreads import IWorker
|
||||
from ._team import Team
|
||||
from ._threadworker import LockWorker, ThreadWorker
|
||||
|
||||
|
||||
class _ThreadFactory(Protocol):
|
||||
def __call__(self, *, target: Callable[..., object]) -> Thread:
|
||||
...
|
||||
|
||||
|
||||
def pool(
|
||||
currentLimit: Callable[[], int], threadFactory: _ThreadFactory = Thread
|
||||
) -> Team:
|
||||
"""
|
||||
Construct a L{Team} that spawns threads as a thread pool, with the given
|
||||
limiting function.
|
||||
|
||||
@note: Future maintainers: while the public API for the eventual move to
|
||||
twisted.threads should look I{something} like this, and while this
|
||||
function is necessary to implement the API described by
|
||||
L{twisted.python.threadpool}, I am starting to think the idea of a hard
|
||||
upper limit on threadpool size is just bad (turning memory performance
|
||||
issues into correctness issues well before we run into memory
|
||||
pressure), and instead we should build something with reactor
|
||||
integration for slowly releasing idle threads when they're not needed
|
||||
and I{rate} limiting the creation of new threads rather than just
|
||||
hard-capping it.
|
||||
|
||||
@param currentLimit: a callable that returns the current limit on the
|
||||
number of workers that the returned L{Team} should create; if it
|
||||
already has more workers than that value, no new workers will be
|
||||
created.
|
||||
@type currentLimit: 0-argument callable returning L{int}
|
||||
|
||||
@param threadFactory: Factory that, when given a C{target} keyword argument,
|
||||
returns a L{threading.Thread} that will run that target.
|
||||
@type threadFactory: callable returning a L{threading.Thread}
|
||||
|
||||
@return: a new L{Team}.
|
||||
"""
|
||||
|
||||
def startThread(target: Callable[..., object]) -> None:
|
||||
return threadFactory(target=target).start()
|
||||
|
||||
def limitedWorkerCreator() -> Optional[IWorker]:
|
||||
stats = team.statistics()
|
||||
if stats.busyWorkerCount + stats.idleWorkerCount >= currentLimit():
|
||||
return None
|
||||
return ThreadWorker(startThread, Queue())
|
||||
|
||||
team = Team(
|
||||
coordinator=LockWorker(Lock(), LocalStorage()),
|
||||
createWorker=limitedWorkerCreator,
|
||||
logException=err,
|
||||
)
|
||||
return team
|
||||
232
backend/lib/python3.12/site-packages/twisted/_threads/_team.py
Normal file
232
backend/lib/python3.12/site-packages/twisted/_threads/_team.py
Normal file
@@ -0,0 +1,232 @@
|
||||
# -*- test-case-name: twisted._threads.test.test_team -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Implementation of a L{Team} of workers; a thread-pool that can allocate work to
|
||||
workers.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
from typing import Callable, Optional, Set
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from . import IWorker
|
||||
from ._convenience import Quit
|
||||
from ._ithreads import IExclusiveWorker
|
||||
|
||||
|
||||
class Statistics:
|
||||
"""
|
||||
Statistics about a L{Team}'s current activity.
|
||||
|
||||
@ivar idleWorkerCount: The number of idle workers.
|
||||
@type idleWorkerCount: L{int}
|
||||
|
||||
@ivar busyWorkerCount: The number of busy workers.
|
||||
@type busyWorkerCount: L{int}
|
||||
|
||||
@ivar backloggedWorkCount: The number of work items passed to L{Team.do}
|
||||
which have not yet been sent to a worker to be performed because not
|
||||
enough workers are available.
|
||||
@type backloggedWorkCount: L{int}
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, idleWorkerCount: int, busyWorkerCount: int, backloggedWorkCount: int
|
||||
) -> None:
|
||||
self.idleWorkerCount = idleWorkerCount
|
||||
self.busyWorkerCount = busyWorkerCount
|
||||
self.backloggedWorkCount = backloggedWorkCount
|
||||
|
||||
|
||||
@implementer(IWorker)
|
||||
class Team:
|
||||
"""
|
||||
A composite L{IWorker} implementation.
|
||||
|
||||
@ivar _quit: A L{Quit} flag indicating whether this L{Team} has been quit
|
||||
yet. This may be set by an arbitrary thread since L{Team.quit} may be
|
||||
called from anywhere.
|
||||
|
||||
@ivar _coordinator: the L{IExclusiveWorker} coordinating access to this
|
||||
L{Team}'s internal resources.
|
||||
|
||||
@ivar _createWorker: a callable that will create new workers.
|
||||
|
||||
@ivar _logException: a 0-argument callable called in an exception context
|
||||
when there is an unhandled error from a task passed to L{Team.do}
|
||||
|
||||
@ivar _idle: a L{set} of idle workers.
|
||||
|
||||
@ivar _busyCount: the number of workers currently busy.
|
||||
|
||||
@ivar _pending: a C{deque} of tasks - that is, 0-argument callables passed
|
||||
to L{Team.do} - that are outstanding.
|
||||
|
||||
@ivar _shouldQuitCoordinator: A flag indicating that the coordinator should
|
||||
be quit at the next available opportunity. Unlike L{Team._quit}, this
|
||||
flag is only set by the coordinator.
|
||||
|
||||
@ivar _toShrink: the number of workers to shrink this L{Team} by at the
|
||||
next available opportunity; set in the coordinator.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: IExclusiveWorker,
|
||||
createWorker: Callable[[], Optional[IWorker]],
|
||||
logException: Callable[[], None],
|
||||
):
|
||||
"""
|
||||
@param coordinator: an L{IExclusiveWorker} which will coordinate access
|
||||
to resources on this L{Team}; that is to say, an
|
||||
L{IExclusiveWorker} whose C{do} method ensures that its given work
|
||||
will be executed in a mutually exclusive context, not in parallel
|
||||
with other work enqueued by C{do} (although possibly in parallel
|
||||
with the caller).
|
||||
|
||||
@param createWorker: A 0-argument callable that will create an
|
||||
L{IWorker} to perform work.
|
||||
|
||||
@param logException: A 0-argument callable called in an exception
|
||||
context when the work passed to C{do} raises an exception.
|
||||
"""
|
||||
self._quit = Quit()
|
||||
self._coordinator = coordinator
|
||||
self._createWorker = createWorker
|
||||
self._logException = logException
|
||||
|
||||
# Don't touch these except from the coordinator.
|
||||
self._idle: Set[IWorker] = set()
|
||||
self._busyCount = 0
|
||||
self._pending: "deque[Callable[..., object]]" = deque()
|
||||
self._shouldQuitCoordinator = False
|
||||
self._toShrink = 0
|
||||
|
||||
def statistics(self) -> Statistics:
|
||||
"""
|
||||
Gather information on the current status of this L{Team}.
|
||||
|
||||
@return: a L{Statistics} describing the current state of this L{Team}.
|
||||
"""
|
||||
return Statistics(len(self._idle), self._busyCount, len(self._pending))
|
||||
|
||||
def grow(self, n: int) -> None:
|
||||
"""
|
||||
Increase the the number of idle workers by C{n}.
|
||||
|
||||
@param n: The number of new idle workers to create.
|
||||
@type n: L{int}
|
||||
"""
|
||||
self._quit.check()
|
||||
|
||||
@self._coordinator.do
|
||||
def createOneWorker() -> None:
|
||||
for x in range(n):
|
||||
worker = self._createWorker()
|
||||
if worker is None:
|
||||
return
|
||||
self._recycleWorker(worker)
|
||||
|
||||
def shrink(self, n: Optional[int] = None) -> None:
|
||||
"""
|
||||
Decrease the number of idle workers by C{n}.
|
||||
|
||||
@param n: The number of idle workers to shut down, or L{None} (or
|
||||
unspecified) to shut down all workers.
|
||||
@type n: L{int} or L{None}
|
||||
"""
|
||||
self._quit.check()
|
||||
self._coordinator.do(lambda: self._quitIdlers(n))
|
||||
|
||||
def _quitIdlers(self, n: Optional[int] = None) -> None:
|
||||
"""
|
||||
The implmentation of C{shrink}, performed by the coordinator worker.
|
||||
|
||||
@param n: see L{Team.shrink}
|
||||
"""
|
||||
if n is None:
|
||||
n = len(self._idle) + self._busyCount
|
||||
for x in range(n):
|
||||
if self._idle:
|
||||
self._idle.pop().quit()
|
||||
else:
|
||||
self._toShrink += 1
|
||||
if self._shouldQuitCoordinator and self._busyCount == 0:
|
||||
self._coordinator.quit()
|
||||
|
||||
def do(self, task: Callable[[], object]) -> None:
|
||||
"""
|
||||
Perform some work in a worker created by C{createWorker}.
|
||||
|
||||
@param task: the callable to run
|
||||
"""
|
||||
self._quit.check()
|
||||
self._coordinator.do(lambda: self._coordinateThisTask(task))
|
||||
|
||||
def _coordinateThisTask(self, task: Callable[..., object]) -> None:
|
||||
"""
|
||||
Select a worker to dispatch to, either an idle one or a new one, and
|
||||
perform it.
|
||||
|
||||
This method should run on the coordinator worker.
|
||||
|
||||
@param task: the task to dispatch
|
||||
@type task: 0-argument callable
|
||||
"""
|
||||
worker = self._idle.pop() if self._idle else self._createWorker()
|
||||
if worker is None:
|
||||
# The createWorker method may return None if we're out of resources
|
||||
# to create workers.
|
||||
self._pending.append(task)
|
||||
return
|
||||
not_none_worker = worker
|
||||
self._busyCount += 1
|
||||
|
||||
@worker.do
|
||||
def doWork() -> None:
|
||||
try:
|
||||
task()
|
||||
except BaseException:
|
||||
self._logException()
|
||||
|
||||
@self._coordinator.do
|
||||
def idleAndPending() -> None:
|
||||
self._busyCount -= 1
|
||||
self._recycleWorker(not_none_worker)
|
||||
|
||||
def _recycleWorker(self, worker: IWorker) -> None:
|
||||
"""
|
||||
Called only from coordinator.
|
||||
|
||||
Recycle the given worker into the idle pool.
|
||||
|
||||
@param worker: a worker created by C{createWorker} and now idle.
|
||||
@type worker: L{IWorker}
|
||||
"""
|
||||
self._idle.add(worker)
|
||||
if self._pending:
|
||||
# Re-try the first enqueued thing.
|
||||
# (Explicitly do _not_ honor _quit.)
|
||||
self._coordinateThisTask(self._pending.popleft())
|
||||
elif self._shouldQuitCoordinator:
|
||||
self._quitIdlers()
|
||||
elif self._toShrink > 0:
|
||||
self._toShrink -= 1
|
||||
self._idle.remove(worker)
|
||||
worker.quit()
|
||||
|
||||
def quit(self) -> None:
|
||||
"""
|
||||
Stop doing work and shut down all idle workers.
|
||||
"""
|
||||
self._quit.set()
|
||||
# In case all the workers are idle when we do this.
|
||||
|
||||
@self._coordinator.do
|
||||
def startFinishing() -> None:
|
||||
self._shouldQuitCoordinator = True
|
||||
self._quitIdlers()
|
||||
@@ -0,0 +1,156 @@
|
||||
# -*- test-case-name: twisted._threads.test.test_threadworker -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Implementation of an L{IWorker} based on native threads and queues.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum, auto
|
||||
from typing import TYPE_CHECKING, Callable, Iterator, Literal, Protocol, TypeVar
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import threading
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from ._convenience import Quit
|
||||
from ._ithreads import IExclusiveWorker
|
||||
|
||||
|
||||
class Stop(Enum):
|
||||
Thread = auto()
|
||||
|
||||
|
||||
StopThread = Stop.Thread
|
||||
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U")
|
||||
|
||||
|
||||
class SimpleQueue(Protocol[T]):
|
||||
def put(self, item: T) -> None:
|
||||
...
|
||||
|
||||
def get(self) -> T:
|
||||
...
|
||||
|
||||
|
||||
# when the sentinel value is a literal in a union, this is how iter works
|
||||
smartiter: Callable[[Callable[[], T | U], U], Iterator[T]]
|
||||
smartiter = iter # type:ignore[assignment]
|
||||
|
||||
|
||||
@implementer(IExclusiveWorker)
|
||||
class ThreadWorker:
|
||||
"""
|
||||
An L{IExclusiveWorker} implemented based on a single thread and a queue.
|
||||
|
||||
This worker ensures exclusivity (i.e. it is an L{IExclusiveWorker} and not
|
||||
an L{IWorker}) by performing all of the work passed to C{do} on the I{same}
|
||||
thread.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
startThread: Callable[[Callable[[], object]], object],
|
||||
queue: SimpleQueue[Callable[[], object] | Literal[Stop.Thread]],
|
||||
):
|
||||
"""
|
||||
Create a L{ThreadWorker} with a function to start a thread and a queue
|
||||
to use to communicate with that thread.
|
||||
|
||||
@param startThread: a callable that takes a callable to run in another
|
||||
thread.
|
||||
|
||||
@param queue: A L{Queue} to use to give tasks to the thread created by
|
||||
C{startThread}.
|
||||
"""
|
||||
self._q = queue
|
||||
self._hasQuit = Quit()
|
||||
|
||||
def work() -> None:
|
||||
for task in smartiter(queue.get, StopThread):
|
||||
task()
|
||||
|
||||
startThread(work)
|
||||
|
||||
def do(self, task: Callable[[], None]) -> None:
|
||||
"""
|
||||
Perform the given task on the thread owned by this L{ThreadWorker}.
|
||||
|
||||
@param task: the function to call on a thread.
|
||||
"""
|
||||
self._hasQuit.check()
|
||||
self._q.put(task)
|
||||
|
||||
def quit(self) -> None:
|
||||
"""
|
||||
Reject all future work and stop the thread started by C{__init__}.
|
||||
"""
|
||||
# Reject all future work. Set this _before_ enqueueing _stop, so
|
||||
# that no work is ever enqueued _after_ _stop.
|
||||
self._hasQuit.set()
|
||||
self._q.put(StopThread)
|
||||
|
||||
|
||||
class SimpleLock(Protocol):
|
||||
def acquire(self) -> bool:
|
||||
...
|
||||
|
||||
def release(self) -> None:
|
||||
...
|
||||
|
||||
|
||||
@implementer(IExclusiveWorker)
|
||||
class LockWorker:
|
||||
"""
|
||||
An L{IWorker} implemented based on a mutual-exclusion lock.
|
||||
"""
|
||||
|
||||
def __init__(self, lock: SimpleLock, local: threading.local):
|
||||
"""
|
||||
@param lock: A mutual-exclusion lock, with C{acquire} and C{release}
|
||||
methods.
|
||||
@type lock: L{threading.Lock}
|
||||
|
||||
@param local: Local storage.
|
||||
@type local: L{threading.local}
|
||||
"""
|
||||
self._quit = Quit()
|
||||
self._lock: SimpleLock | None = lock
|
||||
self._local = local
|
||||
|
||||
def do(self, work: Callable[[], None]) -> None:
|
||||
"""
|
||||
Do the given work on this thread, with the mutex acquired. If this is
|
||||
called re-entrantly, return and wait for the outer invocation to do the
|
||||
work.
|
||||
|
||||
@param work: the work to do with the lock held.
|
||||
"""
|
||||
lock = self._lock
|
||||
local = self._local
|
||||
self._quit.check()
|
||||
working = getattr(local, "working", None)
|
||||
if working is None:
|
||||
assert lock is not None, "LockWorker used after quit()"
|
||||
working = local.working = []
|
||||
working.append(work)
|
||||
lock.acquire()
|
||||
try:
|
||||
while working:
|
||||
working.pop(0)()
|
||||
finally:
|
||||
lock.release()
|
||||
local.working = None
|
||||
else:
|
||||
working.append(work)
|
||||
|
||||
def quit(self) -> None:
|
||||
"""
|
||||
Quit this L{LockWorker}.
|
||||
"""
|
||||
self._quit.set()
|
||||
self._lock = None
|
||||
@@ -0,0 +1,7 @@
|
||||
# -*- test-case-name: twisted._threads.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted._threads}.
|
||||
"""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,56 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases for convenience functionality in L{twisted._threads._convenience}.
|
||||
"""
|
||||
|
||||
|
||||
from twisted.trial.unittest import SynchronousTestCase
|
||||
from .._convenience import Quit
|
||||
from .._ithreads import AlreadyQuit
|
||||
|
||||
|
||||
class QuitTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{Quit}
|
||||
"""
|
||||
|
||||
def test_isInitiallySet(self) -> None:
|
||||
"""
|
||||
L{Quit.isSet} starts as L{False}.
|
||||
"""
|
||||
quit = Quit()
|
||||
self.assertEqual(quit.isSet, False)
|
||||
|
||||
def test_setSetsSet(self) -> None:
|
||||
"""
|
||||
L{Quit.set} sets L{Quit.isSet} to L{True}.
|
||||
"""
|
||||
quit = Quit()
|
||||
quit.set()
|
||||
self.assertEqual(quit.isSet, True)
|
||||
|
||||
def test_checkDoesNothing(self) -> None:
|
||||
"""
|
||||
L{Quit.check} initially does nothing and returns L{None}.
|
||||
"""
|
||||
quit = Quit()
|
||||
checked = quit.check() # type:ignore[func-returns-value]
|
||||
self.assertIs(checked, None)
|
||||
|
||||
def test_checkAfterSetRaises(self) -> None:
|
||||
"""
|
||||
L{Quit.check} raises L{AlreadyQuit} if L{Quit.set} has been called.
|
||||
"""
|
||||
quit = Quit()
|
||||
quit.set()
|
||||
self.assertRaises(AlreadyQuit, quit.check)
|
||||
|
||||
def test_setTwiceRaises(self) -> None:
|
||||
"""
|
||||
L{Quit.set} raises L{AlreadyQuit} if it has been called previously.
|
||||
"""
|
||||
quit = Quit()
|
||||
quit.set()
|
||||
self.assertRaises(AlreadyQuit, quit.set)
|
||||
@@ -0,0 +1,64 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted._threads._memory}.
|
||||
"""
|
||||
|
||||
from zope.interface.verify import verifyObject
|
||||
|
||||
from twisted.trial.unittest import SynchronousTestCase
|
||||
from .. import AlreadyQuit, IWorker, createMemoryWorker
|
||||
|
||||
|
||||
class MemoryWorkerTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{MemoryWorker}.
|
||||
"""
|
||||
|
||||
def test_createWorkerAndPerform(self) -> None:
|
||||
"""
|
||||
L{createMemoryWorker} creates an L{IWorker} and a callable that can
|
||||
perform work on it. The performer returns C{True} if it accomplished
|
||||
useful work.
|
||||
"""
|
||||
worker, performer = createMemoryWorker()
|
||||
verifyObject(IWorker, worker)
|
||||
done = []
|
||||
worker.do(lambda: done.append(3))
|
||||
worker.do(lambda: done.append(4))
|
||||
self.assertEqual(done, [])
|
||||
self.assertEqual(performer(), True)
|
||||
self.assertEqual(done, [3])
|
||||
self.assertEqual(performer(), True)
|
||||
self.assertEqual(done, [3, 4])
|
||||
|
||||
def test_quitQuits(self) -> None:
|
||||
"""
|
||||
Calling C{quit} on the worker returned by L{createMemoryWorker} causes
|
||||
its C{do} and C{quit} methods to raise L{AlreadyQuit}; its C{perform}
|
||||
callable will start raising L{AlreadyQuit} when the work already
|
||||
provided to C{do} has been exhausted.
|
||||
"""
|
||||
worker, performer = createMemoryWorker()
|
||||
done = []
|
||||
|
||||
def moreWork() -> None:
|
||||
done.append(7)
|
||||
|
||||
worker.do(moreWork)
|
||||
worker.quit()
|
||||
self.assertRaises(AlreadyQuit, worker.do, moreWork)
|
||||
self.assertRaises(AlreadyQuit, worker.quit)
|
||||
performer()
|
||||
self.assertEqual(done, [7])
|
||||
self.assertEqual(performer(), False)
|
||||
|
||||
def test_performWhenNothingToDoYet(self) -> None:
|
||||
"""
|
||||
The C{perform} callable returned by L{createMemoryWorker} will return
|
||||
no result when there's no work to do yet. Since there is no work to
|
||||
do, the performer returns C{False}.
|
||||
"""
|
||||
worker, performer = createMemoryWorker()
|
||||
self.assertEqual(performer(), False)
|
||||
@@ -0,0 +1,286 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted._threads._team}.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
from twisted.python.components import proxyForInterface
|
||||
from twisted.python.context import call, get
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.trial.unittest import SynchronousTestCase
|
||||
from .. import AlreadyQuit, IWorker, Team, createMemoryWorker
|
||||
|
||||
|
||||
class ContextualWorker(proxyForInterface(IWorker, "_realWorker")): # type: ignore[misc]
|
||||
"""
|
||||
A worker implementation that supplies a context.
|
||||
"""
|
||||
|
||||
def __init__(self, realWorker: IWorker, **ctx: object) -> None:
|
||||
"""
|
||||
Create with a real worker and a context.
|
||||
"""
|
||||
self._realWorker = realWorker
|
||||
self._context = ctx
|
||||
|
||||
def do(self, work: Callable[[], object]) -> None:
|
||||
"""
|
||||
Perform the given work with the context given to __init__.
|
||||
|
||||
@param work: the work to pass on to the real worker.
|
||||
"""
|
||||
super().do(lambda: call(self._context, work))
|
||||
|
||||
|
||||
class TeamTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{Team}
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""
|
||||
Set up a L{Team} with inspectable, synchronous workers that can be
|
||||
single-stepped.
|
||||
"""
|
||||
coordinator, self.coordinateOnce = createMemoryWorker()
|
||||
self.coordinator = ContextualWorker(coordinator, worker="coordinator")
|
||||
self.workerPerformers: list[Callable[[], object]] = []
|
||||
self.allWorkersEver: list[ContextualWorker] = []
|
||||
self.allUnquitWorkers: list[ContextualWorker] = []
|
||||
self.activePerformers: list[Callable[[], object]] = []
|
||||
self.noMoreWorkers = lambda: False
|
||||
|
||||
def createWorker() -> ContextualWorker | None:
|
||||
if self.noMoreWorkers():
|
||||
return None
|
||||
worker, performer = createMemoryWorker()
|
||||
self.workerPerformers.append(performer)
|
||||
self.activePerformers.append(performer)
|
||||
cw = ContextualWorker(worker, worker=len(self.workerPerformers))
|
||||
self.allWorkersEver.append(cw)
|
||||
self.allUnquitWorkers.append(cw)
|
||||
realQuit = cw.quit
|
||||
|
||||
def quitAndRemove() -> None:
|
||||
realQuit()
|
||||
self.allUnquitWorkers.remove(cw)
|
||||
self.activePerformers.remove(performer)
|
||||
|
||||
cw.quit = quitAndRemove
|
||||
return cw
|
||||
|
||||
self.failures: list[Failure] = []
|
||||
|
||||
def logException() -> None:
|
||||
self.failures.append(Failure())
|
||||
|
||||
self.team = Team(coordinator, createWorker, logException)
|
||||
|
||||
def coordinate(self) -> bool:
|
||||
"""
|
||||
Perform all work currently scheduled in the coordinator.
|
||||
|
||||
@return: whether any coordination work was performed; if the
|
||||
coordinator was idle when this was called, return L{False}
|
||||
(otherwise L{True}).
|
||||
"""
|
||||
did = False
|
||||
while self.coordinateOnce():
|
||||
did = True
|
||||
return did
|
||||
|
||||
def performAllOutstandingWork(self) -> None:
|
||||
"""
|
||||
Perform all work on the coordinator and worker performers that needs to
|
||||
be done.
|
||||
"""
|
||||
continuing = True
|
||||
while continuing:
|
||||
continuing = self.coordinate()
|
||||
for performer in self.workerPerformers:
|
||||
if performer in self.activePerformers:
|
||||
performer()
|
||||
continuing = continuing or self.coordinate()
|
||||
|
||||
def test_doDoesWorkInWorker(self) -> None:
|
||||
"""
|
||||
L{Team.do} does the work in a worker created by the createWorker
|
||||
callable.
|
||||
"""
|
||||
who = None
|
||||
|
||||
def something() -> None:
|
||||
nonlocal who
|
||||
who = get("worker")
|
||||
|
||||
self.team.do(something)
|
||||
self.coordinate()
|
||||
self.assertEqual(self.team.statistics().busyWorkerCount, 1)
|
||||
self.performAllOutstandingWork()
|
||||
self.assertEqual(who, 1)
|
||||
self.assertEqual(self.team.statistics().busyWorkerCount, 0)
|
||||
|
||||
def test_initialStatistics(self) -> None:
|
||||
"""
|
||||
L{Team.statistics} returns an object with idleWorkerCount,
|
||||
busyWorkerCount, and backloggedWorkCount integer attributes.
|
||||
"""
|
||||
stats = self.team.statistics()
|
||||
self.assertEqual(stats.idleWorkerCount, 0)
|
||||
self.assertEqual(stats.busyWorkerCount, 0)
|
||||
self.assertEqual(stats.backloggedWorkCount, 0)
|
||||
|
||||
def test_growCreatesIdleWorkers(self) -> None:
|
||||
"""
|
||||
L{Team.grow} increases the number of available idle workers.
|
||||
"""
|
||||
self.team.grow(5)
|
||||
self.performAllOutstandingWork()
|
||||
self.assertEqual(len(self.workerPerformers), 5)
|
||||
|
||||
def test_growCreateLimit(self) -> None:
|
||||
"""
|
||||
L{Team.grow} increases the number of available idle workers until the
|
||||
C{createWorker} callable starts returning None.
|
||||
"""
|
||||
self.noMoreWorkers = lambda: len(self.allWorkersEver) >= 3
|
||||
self.team.grow(5)
|
||||
self.performAllOutstandingWork()
|
||||
self.assertEqual(len(self.allWorkersEver), 3)
|
||||
self.assertEqual(self.team.statistics().idleWorkerCount, 3)
|
||||
|
||||
def test_shrinkQuitsWorkers(self) -> None:
|
||||
"""
|
||||
L{Team.shrink} will quit the given number of workers.
|
||||
"""
|
||||
self.team.grow(5)
|
||||
self.performAllOutstandingWork()
|
||||
self.team.shrink(3)
|
||||
self.performAllOutstandingWork()
|
||||
self.assertEqual(len(self.allUnquitWorkers), 2)
|
||||
|
||||
def test_shrinkToZero(self) -> None:
|
||||
"""
|
||||
L{Team.shrink} with no arguments will stop all outstanding workers.
|
||||
"""
|
||||
self.team.grow(10)
|
||||
self.performAllOutstandingWork()
|
||||
self.assertEqual(len(self.allUnquitWorkers), 10)
|
||||
self.team.shrink()
|
||||
self.assertEqual(len(self.allUnquitWorkers), 10)
|
||||
self.performAllOutstandingWork()
|
||||
self.assertEqual(len(self.allUnquitWorkers), 0)
|
||||
|
||||
def test_moreWorkWhenNoWorkersAvailable(self) -> None:
|
||||
"""
|
||||
When no additional workers are available, the given work is backlogged,
|
||||
and then performed later when the work was.
|
||||
"""
|
||||
self.team.grow(3)
|
||||
self.coordinate()
|
||||
times = 0
|
||||
|
||||
def something() -> None:
|
||||
nonlocal times
|
||||
times += 1
|
||||
|
||||
self.assertEqual(self.team.statistics().idleWorkerCount, 3)
|
||||
for i in range(3):
|
||||
self.team.do(something)
|
||||
# Make progress on the coordinator but do _not_ actually complete the
|
||||
# work, yet.
|
||||
self.coordinate()
|
||||
self.assertEqual(self.team.statistics().idleWorkerCount, 0)
|
||||
self.noMoreWorkers = lambda: True
|
||||
self.team.do(something)
|
||||
self.coordinate()
|
||||
self.assertEqual(self.team.statistics().idleWorkerCount, 0)
|
||||
self.assertEqual(self.team.statistics().backloggedWorkCount, 1)
|
||||
self.performAllOutstandingWork()
|
||||
self.assertEqual(self.team.statistics().backloggedWorkCount, 0)
|
||||
self.assertEqual(times, 4)
|
||||
|
||||
def test_exceptionInTask(self) -> None:
|
||||
"""
|
||||
When an exception is raised in a task passed to L{Team.do}, the
|
||||
C{logException} given to the L{Team} at construction is invoked in the
|
||||
exception context.
|
||||
"""
|
||||
self.team.do(lambda: 1 / 0)
|
||||
self.performAllOutstandingWork()
|
||||
self.assertEqual(len(self.failures), 1)
|
||||
self.assertEqual(self.failures[0].type, ZeroDivisionError)
|
||||
|
||||
def test_quit(self) -> None:
|
||||
"""
|
||||
L{Team.quit} causes future invocations of L{Team.do} and L{Team.quit}
|
||||
to raise L{AlreadyQuit}.
|
||||
"""
|
||||
self.team.quit()
|
||||
self.assertRaises(AlreadyQuit, self.team.quit)
|
||||
self.assertRaises(AlreadyQuit, self.team.do, list)
|
||||
|
||||
def test_quitQuits(self) -> None:
|
||||
"""
|
||||
L{Team.quit} causes all idle workers, as well as the coordinator
|
||||
worker, to quit.
|
||||
"""
|
||||
for x in range(10):
|
||||
self.team.do(list)
|
||||
self.performAllOutstandingWork()
|
||||
self.team.quit()
|
||||
self.performAllOutstandingWork()
|
||||
self.assertEqual(len(self.allUnquitWorkers), 0)
|
||||
self.assertRaises(AlreadyQuit, self.coordinator.quit)
|
||||
|
||||
def test_quitQuitsLaterWhenBusy(self) -> None:
|
||||
"""
|
||||
L{Team.quit} causes all busy workers to be quit once they've finished
|
||||
the work they've been given.
|
||||
"""
|
||||
self.team.grow(10)
|
||||
for x in range(5):
|
||||
self.team.do(list)
|
||||
self.coordinate()
|
||||
self.team.quit()
|
||||
self.coordinate()
|
||||
self.assertEqual(len(self.allUnquitWorkers), 5)
|
||||
self.performAllOutstandingWork()
|
||||
self.assertEqual(len(self.allUnquitWorkers), 0)
|
||||
self.assertRaises(AlreadyQuit, self.coordinator.quit)
|
||||
|
||||
def test_quitConcurrentWithWorkHappening(self) -> None:
|
||||
"""
|
||||
If work happens after L{Team.quit} sets its C{Quit} flag, but before
|
||||
any other work takes place, the L{Team} should still exit gracefully.
|
||||
"""
|
||||
self.team.do(list)
|
||||
originalSet = self.team._quit.set
|
||||
|
||||
def performWorkConcurrently() -> None:
|
||||
originalSet()
|
||||
self.performAllOutstandingWork()
|
||||
|
||||
self.team._quit.set = performWorkConcurrently # type:ignore[method-assign]
|
||||
self.team.quit()
|
||||
self.assertRaises(AlreadyQuit, self.team.quit)
|
||||
self.assertRaises(AlreadyQuit, self.team.do, list)
|
||||
|
||||
def test_shrinkWhenBusy(self) -> None:
|
||||
"""
|
||||
L{Team.shrink} will wait for busy workers to finish being busy and then
|
||||
quit them.
|
||||
"""
|
||||
for x in range(10):
|
||||
self.team.do(list)
|
||||
self.coordinate()
|
||||
self.assertEqual(len(self.allUnquitWorkers), 10)
|
||||
# There should be 10 busy workers at this point.
|
||||
self.team.shrink(7)
|
||||
self.performAllOutstandingWork()
|
||||
self.assertEqual(len(self.allUnquitWorkers), 3)
|
||||
@@ -0,0 +1,314 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted._threads._threadworker}.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import gc
|
||||
import weakref
|
||||
from threading import ThreadError, local
|
||||
from typing import Callable, Generic, TypeVar
|
||||
|
||||
from twisted.trial.unittest import SynchronousTestCase
|
||||
from .. import AlreadyQuit, LockWorker, ThreadWorker
|
||||
from .._threadworker import SimpleLock
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class FakeQueueEmpty(Exception):
|
||||
"""
|
||||
L{FakeQueue}'s C{get} has exhausted the queue.
|
||||
"""
|
||||
|
||||
|
||||
class WouldDeadlock(Exception):
|
||||
"""
|
||||
If this were a real lock, you'd be deadlocked because the lock would be
|
||||
double-acquired.
|
||||
"""
|
||||
|
||||
|
||||
class FakeThread:
|
||||
"""
|
||||
A fake L{threading.Thread}.
|
||||
|
||||
@ivar target: A target function to run.
|
||||
|
||||
@ivar started: Has this thread been started?
|
||||
@type started: L{bool}
|
||||
"""
|
||||
|
||||
def __init__(self, target: Callable[[], object]) -> None:
|
||||
"""
|
||||
Create a L{FakeThread} with a target.
|
||||
"""
|
||||
self.target = target
|
||||
self.started = False
|
||||
|
||||
def start(self) -> None:
|
||||
"""
|
||||
Set the "started" flag.
|
||||
"""
|
||||
self.started = True
|
||||
|
||||
|
||||
class FakeQueue(Generic[T]):
|
||||
"""
|
||||
A fake L{Queue} implementing C{put} and C{get}.
|
||||
|
||||
@ivar items: A lit of items placed by C{put} but not yet retrieved by
|
||||
C{get}.
|
||||
@type items: L{list}
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Create a L{FakeQueue}.
|
||||
"""
|
||||
self.items: list[T] = []
|
||||
|
||||
def put(self, item: T) -> None:
|
||||
"""
|
||||
Put an item into the queue for later retrieval by L{FakeQueue.get}.
|
||||
|
||||
@param item: any object
|
||||
"""
|
||||
self.items.append(item)
|
||||
|
||||
def get(self) -> T:
|
||||
"""
|
||||
Get an item.
|
||||
|
||||
@return: an item previously put by C{put}.
|
||||
"""
|
||||
if not self.items:
|
||||
raise FakeQueueEmpty()
|
||||
return self.items.pop(0)
|
||||
|
||||
|
||||
class FakeLock:
|
||||
"""
|
||||
A stand-in for L{threading.Lock}.
|
||||
|
||||
@ivar acquired: Whether this lock is presently acquired.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Create a lock in the un-acquired state.
|
||||
"""
|
||||
self.acquired = False
|
||||
|
||||
def acquire(self) -> bool:
|
||||
"""
|
||||
Acquire the lock. Raise an exception if the lock is already acquired.
|
||||
"""
|
||||
if self.acquired:
|
||||
raise WouldDeadlock()
|
||||
self.acquired = True
|
||||
return True
|
||||
|
||||
def release(self) -> None:
|
||||
"""
|
||||
Release the lock. Raise an exception if the lock is not presently
|
||||
acquired.
|
||||
"""
|
||||
if not self.acquired:
|
||||
raise ThreadError()
|
||||
self.acquired = False
|
||||
|
||||
|
||||
class ThreadWorkerTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{ThreadWorker}.
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""
|
||||
Create a worker with fake threads.
|
||||
"""
|
||||
self.fakeThreads: list[FakeThread] = []
|
||||
|
||||
def startThread(target: Callable[[], object]) -> FakeThread:
|
||||
newThread = FakeThread(target=target)
|
||||
newThread.start()
|
||||
self.fakeThreads.append(newThread)
|
||||
return newThread
|
||||
|
||||
self.worker = ThreadWorker(startThread, FakeQueue())
|
||||
|
||||
def test_startsThreadAndPerformsWork(self) -> None:
|
||||
"""
|
||||
L{ThreadWorker} calls its C{createThread} callable to create a thread,
|
||||
its C{createQueue} callable to create a queue, and then the thread's
|
||||
target pulls work from that queue.
|
||||
"""
|
||||
self.assertEqual(len(self.fakeThreads), 1)
|
||||
self.assertEqual(self.fakeThreads[0].started, True)
|
||||
|
||||
done = False
|
||||
|
||||
def doIt() -> None:
|
||||
nonlocal done
|
||||
done = True
|
||||
|
||||
self.worker.do(doIt)
|
||||
self.assertEqual(done, False)
|
||||
self.assertRaises(FakeQueueEmpty, self.fakeThreads[0].target)
|
||||
self.assertEqual(done, True)
|
||||
|
||||
def test_quitPreventsFutureCalls(self) -> None:
|
||||
"""
|
||||
L{ThreadWorker.quit} causes future calls to L{ThreadWorker.do} and
|
||||
L{ThreadWorker.quit} to raise L{AlreadyQuit}.
|
||||
"""
|
||||
self.worker.quit()
|
||||
self.assertRaises(AlreadyQuit, self.worker.quit)
|
||||
self.assertRaises(AlreadyQuit, self.worker.do, list)
|
||||
|
||||
|
||||
class LockWorkerTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{LockWorker}.
|
||||
"""
|
||||
|
||||
def test_fakeDeadlock(self) -> None:
|
||||
"""
|
||||
The L{FakeLock} test fixture will alert us if there's a potential
|
||||
deadlock.
|
||||
"""
|
||||
lock = FakeLock()
|
||||
lock.acquire()
|
||||
self.assertRaises(WouldDeadlock, lock.acquire)
|
||||
|
||||
def test_fakeDoubleRelease(self) -> None:
|
||||
"""
|
||||
The L{FakeLock} test fixture will alert us if there's a potential
|
||||
double-release.
|
||||
"""
|
||||
lock = FakeLock()
|
||||
self.assertRaises(ThreadError, lock.release)
|
||||
lock.acquire()
|
||||
noResult = lock.release() # type:ignore[func-returns-value]
|
||||
self.assertIs(None, noResult)
|
||||
self.assertRaises(ThreadError, lock.release)
|
||||
|
||||
def test_doExecutesImmediatelyWithLock(self) -> None:
|
||||
"""
|
||||
L{LockWorker.do} immediately performs the work it's given, while the
|
||||
lock is acquired.
|
||||
"""
|
||||
storage = local()
|
||||
lock = FakeLock()
|
||||
worker = LockWorker(lock, storage)
|
||||
done = False
|
||||
acquired = False
|
||||
|
||||
def work() -> None:
|
||||
nonlocal done, acquired
|
||||
done = True
|
||||
acquired = lock.acquired
|
||||
|
||||
worker.do(work)
|
||||
self.assertEqual(done, True)
|
||||
self.assertEqual(acquired, True)
|
||||
self.assertEqual(lock.acquired, False)
|
||||
|
||||
def test_doUnwindsReentrancy(self) -> None:
|
||||
"""
|
||||
If L{LockWorker.do} is called recursively, it postpones the inner call
|
||||
until the outer one is complete.
|
||||
"""
|
||||
lock = FakeLock()
|
||||
worker = LockWorker(lock, local())
|
||||
levels = []
|
||||
acquired = []
|
||||
level = 0
|
||||
|
||||
def work() -> None:
|
||||
nonlocal level
|
||||
level += 1
|
||||
levels.append(level)
|
||||
acquired.append(lock.acquired)
|
||||
if len(levels) < 2:
|
||||
worker.do(work)
|
||||
level -= 1
|
||||
|
||||
worker.do(work)
|
||||
self.assertEqual(levels, [1, 1])
|
||||
self.assertEqual(acquired, [True, True])
|
||||
|
||||
def test_quit(self) -> None:
|
||||
"""
|
||||
L{LockWorker.quit} frees the resources associated with its lock and
|
||||
causes further calls to C{do} and C{quit} to fail.
|
||||
"""
|
||||
lock = FakeLock()
|
||||
ref = weakref.ref(lock)
|
||||
worker = LockWorker(lock, local())
|
||||
del lock
|
||||
self.assertIsNot(ref(), None)
|
||||
worker.quit()
|
||||
gc.collect()
|
||||
self.assertIs(ref(), None)
|
||||
self.assertRaises(AlreadyQuit, worker.quit)
|
||||
self.assertRaises(AlreadyQuit, worker.do, list)
|
||||
|
||||
def test_quitWhileWorking(self) -> None:
|
||||
"""
|
||||
If L{LockWorker.quit} is invoked during a call to L{LockWorker.do}, all
|
||||
recursive work scheduled with L{LockWorker.do} will be completed and
|
||||
the lock will be released.
|
||||
"""
|
||||
lock = FakeLock()
|
||||
ref = weakref.ref(lock)
|
||||
worker = LockWorker(lock, local())
|
||||
|
||||
phase1complete = False
|
||||
phase2complete = False
|
||||
phase2acquired = None
|
||||
|
||||
def phase1() -> None:
|
||||
nonlocal phase1complete
|
||||
worker.do(phase2)
|
||||
worker.quit()
|
||||
self.assertRaises(AlreadyQuit, worker.do, list)
|
||||
phase1complete = True
|
||||
|
||||
def phase2() -> None:
|
||||
nonlocal phase2complete, phase2acquired, lock
|
||||
phase2complete = True
|
||||
phase2acquired = lock.acquired
|
||||
|
||||
worker.do(phase1)
|
||||
self.assertEqual(phase1complete, True)
|
||||
self.assertEqual(phase2complete, True)
|
||||
self.assertEqual(lock.acquired, False)
|
||||
del lock
|
||||
gc.collect()
|
||||
self.assertIs(ref(), None)
|
||||
|
||||
def test_quitWhileGettingLock(self) -> None:
|
||||
"""
|
||||
If L{LockWorker.do} is called concurrently with L{LockWorker.quit}, and
|
||||
C{quit} wins the race before C{do} gets the lock attribute, then
|
||||
L{AlreadyQuit} will be raised.
|
||||
"""
|
||||
|
||||
class RacyLockWorker(LockWorker):
|
||||
@property
|
||||
def _lock(self) -> SimpleLock | None:
|
||||
self.quit()
|
||||
it: SimpleLock = self.__dict__["_lock"]
|
||||
return it
|
||||
|
||||
@_lock.setter
|
||||
def _lock(self, value: SimpleLock | None) -> None:
|
||||
self.__dict__["_lock"] = value
|
||||
|
||||
worker = RacyLockWorker(FakeLock(), local())
|
||||
self.assertRaises(AlreadyQuit, worker.do, list)
|
||||
11
backend/lib/python3.12/site-packages/twisted/_version.py
Normal file
11
backend/lib/python3.12/site-packages/twisted/_version.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
Provides Twisted version information.
|
||||
"""
|
||||
|
||||
# This file is auto-generated! Do not edit!
|
||||
# Use `python -m incremental.update Twisted` to change this file.
|
||||
|
||||
from incremental import Version
|
||||
|
||||
__version__ = Version("Twisted", 24, 11, 0)
|
||||
__all__ = ["__version__"]
|
||||
@@ -0,0 +1,6 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Configuration objects for Twisted Applications.
|
||||
"""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
backend/lib/python3.12/site-packages/twisted/application/app.py
Normal file
706
backend/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.
|
||||
"""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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}.
|
||||
"""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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,
|
||||
fileLogObserverFactory=MockFileLogObserver,
|
||||
)
|
||||
runner.startLogging()
|
||||
|
||||
# Check for a filtering observer
|
||||
self.assertEqual(len(LogBeginner.observers), 1)
|
||||
self.assertIsInstance(LogBeginner.observers[0], FilteringLogObserver)
|
||||
|
||||
# Check log level predicate with the correct default log level
|
||||
self.assertEqual(len(MockFilteringLogObserver.predicates), 1)
|
||||
self.assertIsInstance(
|
||||
MockFilteringLogObserver.predicates[0], LogLevelFilterPredicate
|
||||
)
|
||||
self.assertIdentical(
|
||||
MockFilteringLogObserver.predicates[0].defaultLogLevel, LogLevel.critical
|
||||
)
|
||||
|
||||
# Check for a file observer attached to the filtering observer
|
||||
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}.
|
||||
"""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
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.
|
||||
"""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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}.
|
||||
"""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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_selectDefaultLogObserverDefaultWithoutTTY(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,
|
||||
fileLogObserverFactory=jsonFileLogObserver,
|
||||
),
|
||||
)
|
||||
|
||||
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,
|
||||
fileLogObserverFactory=jsonFileLogObserver,
|
||||
),
|
||||
)
|
||||
self.assertEqual(runners[0].runs, 1)
|
||||
|
||||
|
||||
class TwistExitTests(twisted.trial.unittest.TestCase):
|
||||
"""
|
||||
Tests to verify that the Twist script takes the expected actions related
|
||||
to signals and the reactor.
|
||||
"""
|
||||
|
||||
def setUp(self) -> 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)
|
||||
@@ -0,0 +1,7 @@
|
||||
# -*- test-case-name: twisted.conch.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Twisted Conch: The Twisted Shell. Terminal emulation, SSHv2 and telnet.
|
||||
"""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user