mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 15:51:08 -05:00
okay fine
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Twisted Runner: Run and monitor processes.
|
||||
"""
|
||||
80
.venv/lib/python3.12/site-packages/twisted/runner/inetd.py
Normal file
80
.venv/lib/python3.12/site-packages/twisted/runner/inetd.py
Normal file
@@ -0,0 +1,80 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
#
|
||||
|
||||
"""
|
||||
Twisted inetd.
|
||||
|
||||
Maintainer: Andrew Bennetts
|
||||
|
||||
Future Plans: Bugfixes. Specifically for UDP and Sun-RPC, which don't work
|
||||
correctly yet.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from twisted.internet import fdesc, process, reactor
|
||||
from twisted.internet.protocol import Protocol, ServerFactory
|
||||
from twisted.protocols import wire
|
||||
|
||||
# A dict of known 'internal' services (i.e. those that don't involve spawning
|
||||
# another process.
|
||||
internalProtocols = {
|
||||
"echo": wire.Echo,
|
||||
"chargen": wire.Chargen,
|
||||
"discard": wire.Discard,
|
||||
"daytime": wire.Daytime,
|
||||
"time": wire.Time,
|
||||
}
|
||||
|
||||
|
||||
class InetdProtocol(Protocol):
|
||||
"""Forks a child process on connectionMade, passing the socket as fd 0."""
|
||||
|
||||
def connectionMade(self):
|
||||
sockFD = self.transport.fileno()
|
||||
childFDs = {0: sockFD, 1: sockFD}
|
||||
if self.factory.stderrFile:
|
||||
childFDs[2] = self.factory.stderrFile.fileno()
|
||||
|
||||
# processes run by inetd expect blocking sockets
|
||||
# FIXME: maybe this should be done in process.py? are other uses of
|
||||
# Process possibly affected by this?
|
||||
fdesc.setBlocking(sockFD)
|
||||
if 2 in childFDs:
|
||||
fdesc.setBlocking(childFDs[2])
|
||||
|
||||
service = self.factory.service
|
||||
uid = service.user
|
||||
gid = service.group
|
||||
|
||||
# don't tell Process to change our UID/GID if it's what we
|
||||
# already are
|
||||
if uid == os.getuid():
|
||||
uid = None
|
||||
if gid == os.getgid():
|
||||
gid = None
|
||||
|
||||
process.Process(
|
||||
None,
|
||||
service.program,
|
||||
service.programArgs,
|
||||
os.environ,
|
||||
None,
|
||||
None,
|
||||
uid,
|
||||
gid,
|
||||
childFDs,
|
||||
)
|
||||
|
||||
reactor.removeReader(self.transport)
|
||||
reactor.removeWriter(self.transport)
|
||||
|
||||
|
||||
class InetdFactory(ServerFactory):
|
||||
protocol = InetdProtocol
|
||||
stderrFile = None
|
||||
|
||||
def __init__(self, service):
|
||||
self.service = service
|
||||
203
.venv/lib/python3.12/site-packages/twisted/runner/inetdconf.py
Normal file
203
.venv/lib/python3.12/site-packages/twisted/runner/inetdconf.py
Normal file
@@ -0,0 +1,203 @@
|
||||
# -*- test-case-name: twisted.runner.test.test_inetdconf -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Parser for inetd.conf files
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
|
||||
# Various exceptions
|
||||
class InvalidConfError(Exception):
|
||||
"""
|
||||
Invalid configuration file
|
||||
"""
|
||||
|
||||
|
||||
class InvalidInetdConfError(InvalidConfError):
|
||||
"""
|
||||
Invalid inetd.conf file
|
||||
"""
|
||||
|
||||
|
||||
class InvalidServicesConfError(InvalidConfError):
|
||||
"""
|
||||
Invalid services file
|
||||
"""
|
||||
|
||||
|
||||
class UnknownService(Exception):
|
||||
"""
|
||||
Unknown service name
|
||||
"""
|
||||
|
||||
|
||||
class SimpleConfFile:
|
||||
"""
|
||||
Simple configuration file parser superclass.
|
||||
|
||||
Filters out comments and empty lines (which includes lines that only
|
||||
contain comments).
|
||||
|
||||
To use this class, override parseLine or parseFields.
|
||||
"""
|
||||
|
||||
commentChar = "#"
|
||||
defaultFilename: Optional[str] = None
|
||||
|
||||
def parseFile(self, file=None):
|
||||
"""
|
||||
Parse a configuration file
|
||||
|
||||
If file is None and self.defaultFilename is set, it will open
|
||||
defaultFilename and use it.
|
||||
"""
|
||||
close = False
|
||||
if file is None and self.defaultFilename:
|
||||
file = open(self.defaultFilename)
|
||||
close = True
|
||||
|
||||
try:
|
||||
for line in file.readlines():
|
||||
# Strip out comments
|
||||
comment = line.find(self.commentChar)
|
||||
if comment != -1:
|
||||
line = line[:comment]
|
||||
|
||||
# Strip whitespace
|
||||
line = line.strip()
|
||||
|
||||
# Skip empty lines (and lines which only contain comments)
|
||||
if not line:
|
||||
continue
|
||||
|
||||
self.parseLine(line)
|
||||
finally:
|
||||
if close:
|
||||
file.close()
|
||||
|
||||
def parseLine(self, line):
|
||||
"""
|
||||
Override this.
|
||||
|
||||
By default, this will split the line on whitespace and call
|
||||
self.parseFields (catching any errors).
|
||||
"""
|
||||
try:
|
||||
self.parseFields(*line.split())
|
||||
except ValueError:
|
||||
raise InvalidInetdConfError("Invalid line: " + repr(line))
|
||||
|
||||
def parseFields(self, *fields):
|
||||
"""
|
||||
Override this.
|
||||
"""
|
||||
|
||||
|
||||
class InetdService:
|
||||
"""
|
||||
A simple description of an inetd service.
|
||||
"""
|
||||
|
||||
name = None
|
||||
port = None
|
||||
socketType = None
|
||||
protocol = None
|
||||
wait = None
|
||||
user = None
|
||||
group = None
|
||||
program = None
|
||||
programArgs = None
|
||||
|
||||
def __init__(
|
||||
self, name, port, socketType, protocol, wait, user, group, program, programArgs
|
||||
):
|
||||
self.name = name
|
||||
self.port = port
|
||||
self.socketType = socketType
|
||||
self.protocol = protocol
|
||||
self.wait = wait
|
||||
self.user = user
|
||||
self.group = group
|
||||
self.program = program
|
||||
self.programArgs = programArgs
|
||||
|
||||
|
||||
class InetdConf(SimpleConfFile):
|
||||
"""
|
||||
Configuration parser for a traditional UNIX inetd(8)
|
||||
"""
|
||||
|
||||
defaultFilename = "/etc/inetd.conf"
|
||||
|
||||
def __init__(self, knownServices=None):
|
||||
self.services = []
|
||||
|
||||
if knownServices is None:
|
||||
knownServices = ServicesConf()
|
||||
knownServices.parseFile()
|
||||
self.knownServices = knownServices
|
||||
|
||||
def parseFields(
|
||||
self, serviceName, socketType, protocol, wait, user, program, *programArgs
|
||||
):
|
||||
"""
|
||||
Parse an inetd.conf file.
|
||||
|
||||
Implemented from the description in the Debian inetd.conf man page.
|
||||
"""
|
||||
# Extract user (and optional group)
|
||||
user, group = (user.split(".") + [None])[:2]
|
||||
|
||||
# Find the port for a service
|
||||
port = self.knownServices.services.get((serviceName, protocol), None)
|
||||
if not port and not protocol.startswith("rpc/"):
|
||||
# FIXME: Should this be discarded/ignored, rather than throwing
|
||||
# an exception?
|
||||
try:
|
||||
port = int(serviceName)
|
||||
serviceName = "unknown"
|
||||
except BaseException:
|
||||
raise UnknownService(f"Unknown service: {serviceName} ({protocol})")
|
||||
|
||||
self.services.append(
|
||||
InetdService(
|
||||
serviceName,
|
||||
port,
|
||||
socketType,
|
||||
protocol,
|
||||
wait,
|
||||
user,
|
||||
group,
|
||||
program,
|
||||
programArgs,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ServicesConf(SimpleConfFile):
|
||||
"""
|
||||
/etc/services parser
|
||||
|
||||
@ivar services: dict mapping service names to (port, protocol) tuples.
|
||||
"""
|
||||
|
||||
defaultFilename = "/etc/services"
|
||||
|
||||
def __init__(self):
|
||||
self.services = {}
|
||||
|
||||
def parseFields(self, name, portAndProtocol, *aliases):
|
||||
try:
|
||||
port, protocol = portAndProtocol.split("/")
|
||||
port = int(port)
|
||||
except BaseException:
|
||||
raise InvalidServicesConfError(
|
||||
f"Invalid port/protocol: {repr(portAndProtocol)}"
|
||||
)
|
||||
|
||||
self.services[(name, protocol)] = port
|
||||
for alias in aliases:
|
||||
self.services[(alias, protocol)] = port
|
||||
109
.venv/lib/python3.12/site-packages/twisted/runner/inetdtap.py
Normal file
109
.venv/lib/python3.12/site-packages/twisted/runner/inetdtap.py
Normal file
@@ -0,0 +1,109 @@
|
||||
# -*- test-case-name: twisted.runner.test.test_inetdtap -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Twisted inetd TAP support
|
||||
|
||||
The purpose of inetdtap is to provide an inetd-like server, to allow Twisted to
|
||||
invoke other programs to handle incoming sockets.
|
||||
This is a useful thing as a "networking swiss army knife" tool, like netcat.
|
||||
"""
|
||||
|
||||
import grp
|
||||
import pwd
|
||||
import socket
|
||||
|
||||
from twisted.application import internet, service as appservice
|
||||
from twisted.internet.protocol import ServerFactory
|
||||
from twisted.python import log, usage
|
||||
from twisted.runner import inetd, inetdconf
|
||||
|
||||
# Protocol map
|
||||
protocolDict = {"tcp": socket.IPPROTO_TCP, "udp": socket.IPPROTO_UDP}
|
||||
|
||||
|
||||
class Options(usage.Options):
|
||||
"""
|
||||
To use it, create a file named `sample-inetd.conf` with:
|
||||
|
||||
8123 stream tcp wait some_user /bin/cat -
|
||||
|
||||
You can then run it as in the following example and port 8123 became an
|
||||
echo server.
|
||||
|
||||
twistd -n inetd -f sample-inetd.conf
|
||||
"""
|
||||
|
||||
optParameters = [
|
||||
["rpc", "r", "/etc/rpc", "DEPRECATED. RPC procedure table file"],
|
||||
["file", "f", "/etc/inetd.conf", "Service configuration file"],
|
||||
]
|
||||
|
||||
optFlags = [["nointernal", "i", "Don't run internal services"]]
|
||||
|
||||
compData = usage.Completions(optActions={"file": usage.CompleteFiles("*.conf")})
|
||||
|
||||
|
||||
def makeService(config):
|
||||
s = appservice.MultiService()
|
||||
conf = inetdconf.InetdConf()
|
||||
with open(config["file"]) as f:
|
||||
conf.parseFile(f)
|
||||
|
||||
for service in conf.services:
|
||||
protocol = service.protocol
|
||||
|
||||
if service.protocol.startswith("rpc/"):
|
||||
log.msg("Skipping rpc service due to lack of rpc support")
|
||||
continue
|
||||
|
||||
if (protocol, service.socketType) not in [("tcp", "stream"), ("udp", "dgram")]:
|
||||
log.msg(
|
||||
"Skipping unsupported type/protocol: %s/%s"
|
||||
% (service.socketType, service.protocol)
|
||||
)
|
||||
continue
|
||||
|
||||
# Convert the username into a uid (if necessary)
|
||||
try:
|
||||
service.user = int(service.user)
|
||||
except ValueError:
|
||||
try:
|
||||
service.user = pwd.getpwnam(service.user)[2]
|
||||
except KeyError:
|
||||
log.msg("Unknown user: " + service.user)
|
||||
continue
|
||||
|
||||
# Convert the group name into a gid (if necessary)
|
||||
if service.group is None:
|
||||
# If no group was specified, use the user's primary group
|
||||
service.group = pwd.getpwuid(service.user)[3]
|
||||
else:
|
||||
try:
|
||||
service.group = int(service.group)
|
||||
except ValueError:
|
||||
try:
|
||||
service.group = grp.getgrnam(service.group)[2]
|
||||
except KeyError:
|
||||
log.msg("Unknown group: " + service.group)
|
||||
continue
|
||||
|
||||
if service.program == "internal":
|
||||
if config["nointernal"]:
|
||||
continue
|
||||
|
||||
# Internal services can use a standard ServerFactory
|
||||
if service.name not in inetd.internalProtocols:
|
||||
log.msg("Unknown internal service: " + service.name)
|
||||
continue
|
||||
factory = ServerFactory()
|
||||
factory.protocol = inetd.internalProtocols[service.name]
|
||||
else:
|
||||
factory = inetd.InetdFactory(service)
|
||||
|
||||
if protocol == "tcp":
|
||||
internet.TCPServer(service.port, factory).setServiceParent(s)
|
||||
elif protocol == "udp":
|
||||
raise RuntimeError("not supporting UDP")
|
||||
return s
|
||||
@@ -0,0 +1 @@
|
||||
twisted.runner.procmon now logs to a twisted.logger.Logger instance defined on instances of ProcessMonitor instead of to the global legacy twisted.python.log instance.
|
||||
407
.venv/lib/python3.12/site-packages/twisted/runner/procmon.py
Normal file
407
.venv/lib/python3.12/site-packages/twisted/runner/procmon.py
Normal file
@@ -0,0 +1,407 @@
|
||||
# -*- test-case-name: twisted.runner.test.test_procmon -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Support for starting, monitoring, and restarting child process.
|
||||
"""
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import attr
|
||||
import incremental
|
||||
|
||||
from twisted.application import service
|
||||
from twisted.internet import error, protocol, reactor as _reactor
|
||||
from twisted.logger import Logger
|
||||
from twisted.protocols import basic
|
||||
from twisted.python import deprecate
|
||||
|
||||
|
||||
@attr.s(frozen=True, auto_attribs=True)
|
||||
class _Process:
|
||||
"""
|
||||
The parameters of a process to be restarted.
|
||||
|
||||
@ivar args: command-line arguments (including name of command as first one)
|
||||
@type args: C{list}
|
||||
|
||||
@ivar uid: user-id to run process as, or None (which means inherit uid)
|
||||
@type uid: C{int}
|
||||
|
||||
@ivar gid: group-id to run process as, or None (which means inherit gid)
|
||||
@type gid: C{int}
|
||||
|
||||
@ivar env: environment for process
|
||||
@type env: C{dict}
|
||||
|
||||
@ivar cwd: initial working directory for process or None
|
||||
(which means inherit cwd)
|
||||
@type cwd: C{str}
|
||||
"""
|
||||
|
||||
args: List[str]
|
||||
uid: Optional[int] = None
|
||||
gid: Optional[int] = None
|
||||
env: Dict[str, str] = attr.ib(default=attr.Factory(dict))
|
||||
cwd: Optional[str] = None
|
||||
|
||||
@deprecate.deprecated(incremental.Version("Twisted", 18, 7, 0))
|
||||
def toTuple(self):
|
||||
"""
|
||||
Convert process to tuple.
|
||||
|
||||
Convert process to tuple that looks like the legacy structure
|
||||
of processes, for potential users who inspected processes
|
||||
directly.
|
||||
|
||||
This was only an accidental feature, and will be removed. If
|
||||
you need to remember what processes were added to a process monitor,
|
||||
keep track of that when they are added. The process list
|
||||
inside the process monitor is no longer a public API.
|
||||
|
||||
This allows changing the internal structure of the process list,
|
||||
when warranted by bug fixes or additional features.
|
||||
|
||||
@return: tuple representation of process
|
||||
"""
|
||||
return (self.args, self.uid, self.gid, self.env)
|
||||
|
||||
|
||||
class DummyTransport:
|
||||
disconnecting = 0
|
||||
|
||||
|
||||
transport = DummyTransport()
|
||||
|
||||
|
||||
class LineLogger(basic.LineReceiver):
|
||||
tag = None
|
||||
stream = None
|
||||
delimiter = b"\n"
|
||||
service = None
|
||||
|
||||
def lineReceived(self, line):
|
||||
try:
|
||||
line = line.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
line = repr(line)
|
||||
|
||||
self.service.log.info(
|
||||
"[{tag}] {line}", tag=self.tag, line=line, stream=self.stream
|
||||
)
|
||||
|
||||
|
||||
class LoggingProtocol(protocol.ProcessProtocol):
|
||||
service = None
|
||||
name = None
|
||||
|
||||
def connectionMade(self):
|
||||
self._output = LineLogger()
|
||||
self._output.tag = self.name
|
||||
self._output.stream = "stdout"
|
||||
self._output.service = self.service
|
||||
self._outputEmpty = True
|
||||
|
||||
self._error = LineLogger()
|
||||
self._error.tag = self.name
|
||||
self._error.stream = "stderr"
|
||||
self._error.service = self.service
|
||||
self._errorEmpty = True
|
||||
|
||||
self._output.makeConnection(transport)
|
||||
self._error.makeConnection(transport)
|
||||
|
||||
def outReceived(self, data):
|
||||
self._output.dataReceived(data)
|
||||
self._outputEmpty = data[-1] == b"\n"
|
||||
|
||||
def errReceived(self, data):
|
||||
self._error.dataReceived(data)
|
||||
self._errorEmpty = data[-1] == b"\n"
|
||||
|
||||
def processEnded(self, reason):
|
||||
if not self._outputEmpty:
|
||||
self._output.dataReceived(b"\n")
|
||||
if not self._errorEmpty:
|
||||
self._error.dataReceived(b"\n")
|
||||
self.service.connectionLost(self.name)
|
||||
|
||||
@property
|
||||
def output(self):
|
||||
return self._output
|
||||
|
||||
@property
|
||||
def empty(self):
|
||||
return self._outputEmpty
|
||||
|
||||
|
||||
class ProcessMonitor(service.Service):
|
||||
"""
|
||||
ProcessMonitor runs processes, monitors their progress, and restarts
|
||||
them when they die.
|
||||
|
||||
The ProcessMonitor will not attempt to restart a process that appears to
|
||||
die instantly -- with each "instant" death (less than 1 second, by
|
||||
default), it will delay approximately twice as long before restarting
|
||||
it. A successful run will reset the counter.
|
||||
|
||||
The primary interface is L{addProcess} and L{removeProcess}. When the
|
||||
service is running (that is, when the application it is attached to is
|
||||
running), adding a process automatically starts it.
|
||||
|
||||
Each process has a name. This name string must uniquely identify the
|
||||
process. In particular, attempting to add two processes with the same
|
||||
name will result in a C{KeyError}.
|
||||
|
||||
@type threshold: C{float}
|
||||
@ivar threshold: How long a process has to live before the death is
|
||||
considered instant, in seconds. The default value is 1 second.
|
||||
|
||||
@type killTime: C{float}
|
||||
@ivar killTime: How long a process being killed has to get its affairs
|
||||
in order before it gets killed with an unmaskable signal. The
|
||||
default value is 5 seconds.
|
||||
|
||||
@type minRestartDelay: C{float}
|
||||
@ivar minRestartDelay: The minimum time (in seconds) to wait before
|
||||
attempting to restart a process. Default 1s.
|
||||
|
||||
@type maxRestartDelay: C{float}
|
||||
@ivar maxRestartDelay: The maximum time (in seconds) to wait before
|
||||
attempting to restart a process. Default 3600s (1h).
|
||||
|
||||
@type _reactor: L{IReactorProcess} provider
|
||||
@ivar _reactor: A provider of L{IReactorProcess} and L{IReactorTime}
|
||||
which will be used to spawn processes and register delayed calls.
|
||||
|
||||
@type log: L{Logger}
|
||||
@ivar log: The logger used to propagate log messages from spawned
|
||||
processes.
|
||||
|
||||
"""
|
||||
|
||||
threshold = 1
|
||||
killTime = 5
|
||||
minRestartDelay = 1
|
||||
maxRestartDelay = 3600
|
||||
log = Logger()
|
||||
|
||||
def __init__(self, reactor=_reactor):
|
||||
self._reactor = reactor
|
||||
|
||||
self._processes = {}
|
||||
self.protocols = {}
|
||||
self.delay = {}
|
||||
self.timeStarted = {}
|
||||
self.murder = {}
|
||||
self.restart = {}
|
||||
|
||||
@deprecate.deprecatedProperty(incremental.Version("Twisted", 18, 7, 0))
|
||||
def processes(self):
|
||||
"""
|
||||
Processes as dict of tuples
|
||||
|
||||
@return: Dict of process name to monitored processes as tuples
|
||||
"""
|
||||
return {name: process.toTuple() for name, process in self._processes.items()}
|
||||
|
||||
@deprecate.deprecated(incremental.Version("Twisted", 18, 7, 0))
|
||||
def __getstate__(self):
|
||||
dct = service.Service.__getstate__(self)
|
||||
del dct["_reactor"]
|
||||
dct["protocols"] = {}
|
||||
dct["delay"] = {}
|
||||
dct["timeStarted"] = {}
|
||||
dct["murder"] = {}
|
||||
dct["restart"] = {}
|
||||
del dct["_processes"]
|
||||
dct["processes"] = self.processes
|
||||
return dct
|
||||
|
||||
def addProcess(self, name, args, uid=None, gid=None, env={}, cwd=None):
|
||||
"""
|
||||
Add a new monitored process and start it immediately if the
|
||||
L{ProcessMonitor} service is running.
|
||||
|
||||
Note that args are passed to the system call, not to the shell. If
|
||||
running the shell is desired, the common idiom is to use
|
||||
C{ProcessMonitor.addProcess("name", ['/bin/sh', '-c', shell_script])}
|
||||
|
||||
@param name: A name for this process. This value must be
|
||||
unique across all processes added to this monitor.
|
||||
@type name: C{str}
|
||||
@param args: The argv sequence for the process to launch.
|
||||
@param uid: The user ID to use to run the process. If L{None},
|
||||
the current UID is used.
|
||||
@type uid: C{int}
|
||||
@param gid: The group ID to use to run the process. If L{None},
|
||||
the current GID is used.
|
||||
@type uid: C{int}
|
||||
@param env: The environment to give to the launched process. See
|
||||
L{IReactorProcess.spawnProcess}'s C{env} parameter.
|
||||
@type env: C{dict}
|
||||
@param cwd: The initial working directory of the launched process.
|
||||
The default of C{None} means inheriting the laucnhing process's
|
||||
working directory.
|
||||
@type env: C{dict}
|
||||
@raise KeyError: If a process with the given name already exists.
|
||||
"""
|
||||
if name in self._processes:
|
||||
raise KeyError(f"remove {name} first")
|
||||
self._processes[name] = _Process(args, uid, gid, env, cwd)
|
||||
self.delay[name] = self.minRestartDelay
|
||||
if self.running:
|
||||
self.startProcess(name)
|
||||
|
||||
def removeProcess(self, name):
|
||||
"""
|
||||
Stop the named process and remove it from the list of monitored
|
||||
processes.
|
||||
|
||||
@type name: C{str}
|
||||
@param name: A string that uniquely identifies the process.
|
||||
"""
|
||||
self.stopProcess(name)
|
||||
del self._processes[name]
|
||||
|
||||
def startService(self):
|
||||
"""
|
||||
Start all monitored processes.
|
||||
"""
|
||||
service.Service.startService(self)
|
||||
for name in list(self._processes):
|
||||
self.startProcess(name)
|
||||
|
||||
def stopService(self):
|
||||
"""
|
||||
Stop all monitored processes and cancel all scheduled process restarts.
|
||||
"""
|
||||
service.Service.stopService(self)
|
||||
|
||||
# Cancel any outstanding restarts
|
||||
for name, delayedCall in list(self.restart.items()):
|
||||
if delayedCall.active():
|
||||
delayedCall.cancel()
|
||||
|
||||
for name in list(self._processes):
|
||||
self.stopProcess(name)
|
||||
|
||||
def connectionLost(self, name):
|
||||
"""
|
||||
Called when a monitored processes exits. If
|
||||
L{service.IService.running} is L{True} (ie the service is started), the
|
||||
process will be restarted.
|
||||
If the process had been running for more than
|
||||
L{ProcessMonitor.threshold} seconds it will be restarted immediately.
|
||||
If the process had been running for less than
|
||||
L{ProcessMonitor.threshold} seconds, the restart will be delayed and
|
||||
each time the process dies before the configured threshold, the restart
|
||||
delay will be doubled - up to a maximum delay of maxRestartDelay sec.
|
||||
|
||||
@type name: C{str}
|
||||
@param name: A string that uniquely identifies the process
|
||||
which exited.
|
||||
"""
|
||||
# Cancel the scheduled _forceStopProcess function if the process
|
||||
# dies naturally
|
||||
if name in self.murder:
|
||||
if self.murder[name].active():
|
||||
self.murder[name].cancel()
|
||||
del self.murder[name]
|
||||
|
||||
del self.protocols[name]
|
||||
|
||||
if self._reactor.seconds() - self.timeStarted[name] < self.threshold:
|
||||
# The process died too fast - backoff
|
||||
nextDelay = self.delay[name]
|
||||
self.delay[name] = min(self.delay[name] * 2, self.maxRestartDelay)
|
||||
|
||||
else:
|
||||
# Process had been running for a significant amount of time
|
||||
# restart immediately
|
||||
nextDelay = 0
|
||||
self.delay[name] = self.minRestartDelay
|
||||
|
||||
# Schedule a process restart if the service is running
|
||||
if self.running and name in self._processes:
|
||||
self.restart[name] = self._reactor.callLater(
|
||||
nextDelay, self.startProcess, name
|
||||
)
|
||||
|
||||
def startProcess(self, name):
|
||||
"""
|
||||
@param name: The name of the process to be started
|
||||
"""
|
||||
# If a protocol instance already exists, it means the process is
|
||||
# already running
|
||||
if name in self.protocols:
|
||||
return
|
||||
|
||||
process = self._processes[name]
|
||||
|
||||
proto = LoggingProtocol()
|
||||
proto.service = self
|
||||
proto.name = name
|
||||
self.protocols[name] = proto
|
||||
self.timeStarted[name] = self._reactor.seconds()
|
||||
self._reactor.spawnProcess(
|
||||
proto,
|
||||
process.args[0],
|
||||
process.args,
|
||||
uid=process.uid,
|
||||
gid=process.gid,
|
||||
env=process.env,
|
||||
path=process.cwd,
|
||||
)
|
||||
|
||||
def _forceStopProcess(self, proc):
|
||||
"""
|
||||
@param proc: An L{IProcessTransport} provider
|
||||
"""
|
||||
try:
|
||||
proc.signalProcess("KILL")
|
||||
except error.ProcessExitedAlready:
|
||||
pass
|
||||
|
||||
def stopProcess(self, name):
|
||||
"""
|
||||
@param name: The name of the process to be stopped
|
||||
"""
|
||||
if name not in self._processes:
|
||||
raise KeyError(f"Unrecognized process name: {name}")
|
||||
|
||||
proto = self.protocols.get(name, None)
|
||||
if proto is not None:
|
||||
proc = proto.transport
|
||||
try:
|
||||
proc.signalProcess("TERM")
|
||||
except error.ProcessExitedAlready:
|
||||
pass
|
||||
else:
|
||||
self.murder[name] = self._reactor.callLater(
|
||||
self.killTime, self._forceStopProcess, proc
|
||||
)
|
||||
|
||||
def restartAll(self):
|
||||
"""
|
||||
Restart all processes. This is useful for third party management
|
||||
services to allow a user to restart servers because of an outside change
|
||||
in circumstances -- for example, a new version of a library is
|
||||
installed.
|
||||
"""
|
||||
for name in self._processes:
|
||||
self.stopProcess(name)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
lst = []
|
||||
for name, proc in self._processes.items():
|
||||
uidgid = ""
|
||||
if proc.uid is not None:
|
||||
uidgid = str(proc.uid)
|
||||
if proc.gid is not None:
|
||||
uidgid += ":" + str(proc.gid)
|
||||
|
||||
if uidgid:
|
||||
uidgid = "(" + uidgid + ")"
|
||||
lst.append(f"{name!r}{uidgid}: {proc.args!r}")
|
||||
return "<" + self.__class__.__name__ + " " + " ".join(lst) + ">"
|
||||
@@ -0,0 +1,96 @@
|
||||
# -*- test-case-name: twisted.runner.test.test_procmontap -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Support for creating a service which runs a process monitor.
|
||||
"""
|
||||
|
||||
from typing import List, Sequence
|
||||
|
||||
from twisted.python import usage
|
||||
from twisted.runner.procmon import ProcessMonitor
|
||||
|
||||
|
||||
class Options(usage.Options):
|
||||
"""
|
||||
Define the options accepted by the I{twistd procmon} plugin.
|
||||
"""
|
||||
|
||||
synopsis = "[procmon options] commandline"
|
||||
|
||||
optParameters = [
|
||||
[
|
||||
"threshold",
|
||||
"t",
|
||||
1,
|
||||
"How long a process has to live "
|
||||
"before the death is considered instant, in seconds.",
|
||||
float,
|
||||
],
|
||||
[
|
||||
"killtime",
|
||||
"k",
|
||||
5,
|
||||
"How long a process being killed "
|
||||
"has to get its affairs in order before it gets killed "
|
||||
"with an unmaskable signal.",
|
||||
float,
|
||||
],
|
||||
[
|
||||
"minrestartdelay",
|
||||
"m",
|
||||
1,
|
||||
"The minimum time (in "
|
||||
"seconds) to wait before attempting to restart a "
|
||||
"process",
|
||||
float,
|
||||
],
|
||||
[
|
||||
"maxrestartdelay",
|
||||
"M",
|
||||
3600,
|
||||
"The maximum time (in "
|
||||
"seconds) to wait before attempting to restart a "
|
||||
"process",
|
||||
float,
|
||||
],
|
||||
]
|
||||
|
||||
optFlags: List[Sequence[str]] = []
|
||||
|
||||
longdesc = """\
|
||||
procmon runs processes, monitors their progress, and restarts them when they
|
||||
die.
|
||||
|
||||
procmon will not attempt to restart a process that appears to die instantly;
|
||||
with each "instant" death (less than 1 second, by default), it will delay
|
||||
approximately twice as long before restarting it. A successful run will reset
|
||||
the counter.
|
||||
|
||||
Eg twistd procmon sleep 10"""
|
||||
|
||||
def parseArgs(self, *args: str) -> None:
|
||||
"""
|
||||
Grab the command line that is going to be started and monitored
|
||||
"""
|
||||
self["args"] = args
|
||||
|
||||
def postOptions(self) -> None:
|
||||
"""
|
||||
Check for dependencies.
|
||||
"""
|
||||
if len(self["args"]) < 1:
|
||||
raise usage.UsageError("Please specify a process commandline")
|
||||
|
||||
|
||||
def makeService(config: Options) -> ProcessMonitor:
|
||||
s = ProcessMonitor()
|
||||
|
||||
s.threshold = config["threshold"]
|
||||
s.killTime = config["killtime"]
|
||||
s.minRestartDelay = config["minrestartdelay"]
|
||||
s.maxRestartDelay = config["maxrestartdelay"]
|
||||
|
||||
s.addProcess(" ".join(config["args"]), config["args"])
|
||||
return s
|
||||
@@ -0,0 +1,6 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test package for Twisted Runner.
|
||||
"""
|
||||
@@ -0,0 +1,68 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for implementations of L{inetdconf}.
|
||||
"""
|
||||
|
||||
from twisted.runner import inetdconf
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
class ServicesConfTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{inetdconf.ServicesConf}
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.servicesFilename1 = self.mktemp()
|
||||
with open(self.servicesFilename1, "w") as f:
|
||||
f.write(
|
||||
"""
|
||||
# This is a comment
|
||||
http 80/tcp www www-http # WorldWideWeb HTTP
|
||||
http 80/udp www www-http
|
||||
http 80/sctp
|
||||
"""
|
||||
)
|
||||
self.servicesFilename2 = self.mktemp()
|
||||
with open(self.servicesFilename2, "w") as f:
|
||||
f.write(
|
||||
"""
|
||||
https 443/tcp # http protocol over TLS/SSL
|
||||
"""
|
||||
)
|
||||
|
||||
def test_parseDefaultFilename(self) -> None:
|
||||
"""
|
||||
Services are parsed from default filename.
|
||||
"""
|
||||
conf = inetdconf.ServicesConf()
|
||||
conf.defaultFilename = self.servicesFilename1
|
||||
conf.parseFile()
|
||||
self.assertEqual(
|
||||
conf.services,
|
||||
{
|
||||
("http", "tcp"): 80,
|
||||
("http", "udp"): 80,
|
||||
("http", "sctp"): 80,
|
||||
("www", "tcp"): 80,
|
||||
("www", "udp"): 80,
|
||||
("www-http", "tcp"): 80,
|
||||
("www-http", "udp"): 80,
|
||||
},
|
||||
)
|
||||
|
||||
def test_parseFile(self) -> None:
|
||||
"""
|
||||
Services are parsed from given C{file}.
|
||||
"""
|
||||
conf = inetdconf.ServicesConf()
|
||||
with open(self.servicesFilename2) as f:
|
||||
conf.parseFile(f)
|
||||
self.assertEqual(
|
||||
conf.services,
|
||||
{
|
||||
("https", "tcp"): 443,
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,698 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.runner.procmon}.
|
||||
"""
|
||||
import pickle
|
||||
|
||||
from twisted.internet.error import ProcessDone, ProcessExitedAlready, ProcessTerminated
|
||||
from twisted.internet.task import Clock
|
||||
from twisted.internet.testing import MemoryReactor
|
||||
from twisted.logger import globalLogPublisher
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.runner.procmon import LoggingProtocol, ProcessMonitor
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
class DummyProcess:
|
||||
"""
|
||||
An incomplete and fake L{IProcessTransport} implementation for testing how
|
||||
L{ProcessMonitor} behaves when its monitored processes exit.
|
||||
|
||||
@ivar _terminationDelay: the delay in seconds after which the DummyProcess
|
||||
will appear to exit when it receives a TERM signal
|
||||
"""
|
||||
|
||||
pid = 1
|
||||
proto = None
|
||||
|
||||
_terminationDelay = 1
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reactor,
|
||||
executable,
|
||||
args,
|
||||
environment,
|
||||
path,
|
||||
proto,
|
||||
uid=None,
|
||||
gid=None,
|
||||
usePTY=0,
|
||||
childFDs=None,
|
||||
):
|
||||
self.proto = proto
|
||||
|
||||
self._reactor = reactor
|
||||
self._executable = executable
|
||||
self._args = args
|
||||
self._environment = environment
|
||||
self._path = path
|
||||
self._uid = uid
|
||||
self._gid = gid
|
||||
self._usePTY = usePTY
|
||||
self._childFDs = childFDs
|
||||
|
||||
def signalProcess(self, signalID):
|
||||
"""
|
||||
A partial implementation of signalProcess which can only handle TERM and
|
||||
KILL signals.
|
||||
- When a TERM signal is given, the dummy process will appear to exit
|
||||
after L{DummyProcess._terminationDelay} seconds with exit code 0
|
||||
- When a KILL signal is given, the dummy process will appear to exit
|
||||
immediately with exit code 1.
|
||||
|
||||
@param signalID: The signal name or number to be issued to the process.
|
||||
@type signalID: C{str}
|
||||
"""
|
||||
params = {"TERM": (self._terminationDelay, 0), "KILL": (0, 1)}
|
||||
|
||||
if self.pid is None:
|
||||
raise ProcessExitedAlready()
|
||||
|
||||
if signalID in params:
|
||||
delay, status = params[signalID]
|
||||
self._signalHandler = self._reactor.callLater(
|
||||
delay, self.processEnded, status
|
||||
)
|
||||
|
||||
def processEnded(self, status):
|
||||
"""
|
||||
Deliver the process ended event to C{self.proto}.
|
||||
"""
|
||||
self.pid = None
|
||||
statusMap = {
|
||||
0: ProcessDone,
|
||||
1: ProcessTerminated,
|
||||
}
|
||||
self.proto.processEnded(Failure(statusMap[status](status)))
|
||||
|
||||
|
||||
class DummyProcessReactor(MemoryReactor, Clock):
|
||||
"""
|
||||
@ivar spawnedProcesses: a list that keeps track of the fake process
|
||||
instances built by C{spawnProcess}.
|
||||
@type spawnedProcesses: C{list}
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
MemoryReactor.__init__(self)
|
||||
Clock.__init__(self)
|
||||
|
||||
self.spawnedProcesses = []
|
||||
|
||||
def spawnProcess(
|
||||
self,
|
||||
processProtocol,
|
||||
executable,
|
||||
args=(),
|
||||
env={},
|
||||
path=None,
|
||||
uid=None,
|
||||
gid=None,
|
||||
usePTY=0,
|
||||
childFDs=None,
|
||||
):
|
||||
"""
|
||||
Fake L{reactor.spawnProcess}, that logs all the process
|
||||
arguments and returns a L{DummyProcess}.
|
||||
"""
|
||||
|
||||
proc = DummyProcess(
|
||||
self,
|
||||
executable,
|
||||
args,
|
||||
env,
|
||||
path,
|
||||
processProtocol,
|
||||
uid,
|
||||
gid,
|
||||
usePTY,
|
||||
childFDs,
|
||||
)
|
||||
processProtocol.makeConnection(proc)
|
||||
self.spawnedProcesses.append(proc)
|
||||
return proc
|
||||
|
||||
|
||||
class ProcmonTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{ProcessMonitor}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create an L{ProcessMonitor} wrapped around a fake reactor.
|
||||
"""
|
||||
self.reactor = DummyProcessReactor()
|
||||
self.pm = ProcessMonitor(reactor=self.reactor)
|
||||
self.pm.minRestartDelay = 2
|
||||
self.pm.maxRestartDelay = 10
|
||||
self.pm.threshold = 10
|
||||
|
||||
def test_reprLooksGood(self):
|
||||
"""
|
||||
Repr includes all details
|
||||
"""
|
||||
self.pm.addProcess("foo", ["arg1", "arg2"], uid=1, gid=2, env={})
|
||||
representation = repr(self.pm)
|
||||
self.assertIn("foo", representation)
|
||||
self.assertIn("1", representation)
|
||||
self.assertIn("2", representation)
|
||||
|
||||
def test_simpleReprLooksGood(self):
|
||||
"""
|
||||
Repr does not include unneeded details.
|
||||
|
||||
Values of attributes that just mean "inherit from launching
|
||||
process" do not appear in the repr of a process.
|
||||
"""
|
||||
self.pm.addProcess("foo", ["arg1", "arg2"], env={})
|
||||
representation = repr(self.pm)
|
||||
self.assertNotIn("(", representation)
|
||||
self.assertNotIn(")", representation)
|
||||
|
||||
def test_getStateIncludesProcesses(self):
|
||||
"""
|
||||
The list of monitored processes must be included in the pickle state.
|
||||
"""
|
||||
self.pm.addProcess("foo", ["arg1", "arg2"], uid=1, gid=2, env={})
|
||||
self.assertEqual(
|
||||
self.pm.__getstate__()["processes"], {"foo": (["arg1", "arg2"], 1, 2, {})}
|
||||
)
|
||||
|
||||
def test_getStateExcludesReactor(self):
|
||||
"""
|
||||
The private L{ProcessMonitor._reactor} instance variable should not be
|
||||
included in the pickle state.
|
||||
"""
|
||||
self.assertNotIn("_reactor", self.pm.__getstate__())
|
||||
|
||||
def test_addProcess(self):
|
||||
"""
|
||||
L{ProcessMonitor.addProcess} only starts the named program if
|
||||
L{ProcessMonitor.startService} has been called.
|
||||
"""
|
||||
self.pm.addProcess("foo", ["arg1", "arg2"], uid=1, gid=2, env={})
|
||||
self.assertEqual(self.pm.protocols, {})
|
||||
self.assertEqual(self.pm.processes, {"foo": (["arg1", "arg2"], 1, 2, {})})
|
||||
self.pm.startService()
|
||||
self.reactor.advance(0)
|
||||
self.assertEqual(list(self.pm.protocols.keys()), ["foo"])
|
||||
|
||||
def test_addProcessDuplicateKeyError(self):
|
||||
"""
|
||||
L{ProcessMonitor.addProcess} raises a C{KeyError} if a process with the
|
||||
given name already exists.
|
||||
"""
|
||||
self.pm.addProcess("foo", ["arg1", "arg2"], uid=1, gid=2, env={})
|
||||
self.assertRaises(
|
||||
KeyError, self.pm.addProcess, "foo", ["arg1", "arg2"], uid=1, gid=2, env={}
|
||||
)
|
||||
|
||||
def test_addProcessEnv(self):
|
||||
"""
|
||||
L{ProcessMonitor.addProcess} takes an C{env} parameter that is passed to
|
||||
L{IReactorProcess.spawnProcess}.
|
||||
"""
|
||||
fakeEnv = {"KEY": "value"}
|
||||
self.pm.startService()
|
||||
self.pm.addProcess("foo", ["foo"], uid=1, gid=2, env=fakeEnv)
|
||||
self.reactor.advance(0)
|
||||
self.assertEqual(self.reactor.spawnedProcesses[0]._environment, fakeEnv)
|
||||
|
||||
def test_addProcessCwd(self):
|
||||
"""
|
||||
L{ProcessMonitor.addProcess} takes an C{cwd} parameter that is passed
|
||||
to L{IReactorProcess.spawnProcess}.
|
||||
"""
|
||||
self.pm.startService()
|
||||
self.pm.addProcess("foo", ["foo"], cwd="/mnt/lala")
|
||||
self.reactor.advance(0)
|
||||
self.assertEqual(self.reactor.spawnedProcesses[0]._path, "/mnt/lala")
|
||||
|
||||
def test_removeProcess(self):
|
||||
"""
|
||||
L{ProcessMonitor.removeProcess} removes the process from the public
|
||||
processes list.
|
||||
"""
|
||||
self.pm.startService()
|
||||
self.pm.addProcess("foo", ["foo"])
|
||||
self.assertEqual(len(self.pm.processes), 1)
|
||||
self.pm.removeProcess("foo")
|
||||
self.assertEqual(len(self.pm.processes), 0)
|
||||
|
||||
def test_removeProcessUnknownKeyError(self):
|
||||
"""
|
||||
L{ProcessMonitor.removeProcess} raises a C{KeyError} if the given
|
||||
process name isn't recognised.
|
||||
"""
|
||||
self.pm.startService()
|
||||
self.assertRaises(KeyError, self.pm.removeProcess, "foo")
|
||||
|
||||
def test_startProcess(self):
|
||||
"""
|
||||
When a process has been started, an instance of L{LoggingProtocol} will
|
||||
be added to the L{ProcessMonitor.protocols} dict and the start time of
|
||||
the process will be recorded in the L{ProcessMonitor.timeStarted}
|
||||
dictionary.
|
||||
"""
|
||||
self.pm.addProcess("foo", ["foo"])
|
||||
self.pm.startProcess("foo")
|
||||
self.assertIsInstance(self.pm.protocols["foo"], LoggingProtocol)
|
||||
self.assertIn("foo", self.pm.timeStarted.keys())
|
||||
|
||||
def test_startProcessAlreadyStarted(self):
|
||||
"""
|
||||
L{ProcessMonitor.startProcess} silently returns if the named process is
|
||||
already started.
|
||||
"""
|
||||
self.pm.addProcess("foo", ["foo"])
|
||||
self.pm.startProcess("foo")
|
||||
self.assertIsNone(self.pm.startProcess("foo"))
|
||||
|
||||
def test_startProcessUnknownKeyError(self):
|
||||
"""
|
||||
L{ProcessMonitor.startProcess} raises a C{KeyError} if the given
|
||||
process name isn't recognised.
|
||||
"""
|
||||
self.assertRaises(KeyError, self.pm.startProcess, "foo")
|
||||
|
||||
def test_stopProcessNaturalTermination(self):
|
||||
"""
|
||||
L{ProcessMonitor.stopProcess} immediately sends a TERM signal to the
|
||||
named process.
|
||||
"""
|
||||
self.pm.startService()
|
||||
self.pm.addProcess("foo", ["foo"])
|
||||
self.assertIn("foo", self.pm.protocols)
|
||||
|
||||
# Configure fake process to die 1 second after receiving term signal
|
||||
timeToDie = self.pm.protocols["foo"].transport._terminationDelay = 1
|
||||
|
||||
# Advance the reactor to just before the short lived process threshold
|
||||
# and leave enough time for the process to die
|
||||
self.reactor.advance(self.pm.threshold)
|
||||
# Then signal the process to stop
|
||||
self.pm.stopProcess("foo")
|
||||
|
||||
# Advance the reactor just enough to give the process time to die and
|
||||
# verify that the process restarts
|
||||
self.reactor.advance(timeToDie)
|
||||
|
||||
# No further time is required to pass here but the reactor must
|
||||
# iterate due to implementation details. See the comment in
|
||||
# test_stopProcessForcedKill.
|
||||
self.reactor.advance(0)
|
||||
|
||||
# We expect it to be restarted immediately
|
||||
self.assertEqual(self.reactor.seconds(), self.pm.timeStarted["foo"])
|
||||
|
||||
def test_stopProcessForcedKill(self):
|
||||
"""
|
||||
L{ProcessMonitor.stopProcess} kills a process which fails to terminate
|
||||
naturally within L{ProcessMonitor.killTime} seconds.
|
||||
"""
|
||||
self.pm.startService()
|
||||
self.pm.addProcess("foo", ["foo"])
|
||||
self.assertIn("foo", self.pm.protocols)
|
||||
self.reactor.advance(self.pm.threshold)
|
||||
proc = self.pm.protocols["foo"].transport
|
||||
# Arrange for the fake process to live longer than the killTime
|
||||
proc._terminationDelay = self.pm.killTime + 1
|
||||
self.pm.stopProcess("foo")
|
||||
# If process doesn't die before the killTime, procmon should
|
||||
# terminate it
|
||||
self.reactor.advance(self.pm.killTime - 1)
|
||||
self.assertEqual(0.0, self.pm.timeStarted["foo"])
|
||||
|
||||
self.reactor.advance(1)
|
||||
|
||||
# We expect it to be immediately restarted. While no actual time
|
||||
# should need to pass for this to happen, the reactor will need to
|
||||
# iterate a couple times because the implementation uses `callLater`
|
||||
# (twice!) to schedule the restart and no delayed call can run sooner
|
||||
# than the reactor iteration after it is scheduled.
|
||||
self.reactor.pump([0, 0])
|
||||
|
||||
self.assertEqual(self.reactor.seconds(), self.pm.timeStarted["foo"])
|
||||
|
||||
def test_stopProcessUnknownKeyError(self):
|
||||
"""
|
||||
L{ProcessMonitor.stopProcess} raises a C{KeyError} if the given process
|
||||
name isn't recognised.
|
||||
"""
|
||||
self.assertRaises(KeyError, self.pm.stopProcess, "foo")
|
||||
|
||||
def test_stopProcessAlreadyStopped(self):
|
||||
"""
|
||||
L{ProcessMonitor.stopProcess} silently returns if the named process
|
||||
is already stopped. eg Process has crashed and a restart has been
|
||||
rescheduled, but in the meantime, the service is stopped.
|
||||
"""
|
||||
self.pm.addProcess("foo", ["foo"])
|
||||
self.assertIsNone(self.pm.stopProcess("foo"))
|
||||
|
||||
def test_outputReceivedCompleteLine(self):
|
||||
"""
|
||||
Getting a complete output line on stdout generates a log message.
|
||||
"""
|
||||
events = []
|
||||
self.addCleanup(globalLogPublisher.removeObserver, events.append)
|
||||
globalLogPublisher.addObserver(events.append)
|
||||
self.pm.addProcess("foo", ["foo"])
|
||||
# Schedule the process to start
|
||||
self.pm.startService()
|
||||
# Advance the reactor to start the process
|
||||
self.reactor.advance(0)
|
||||
self.assertIn("foo", self.pm.protocols)
|
||||
# Long time passes
|
||||
self.reactor.advance(self.pm.threshold)
|
||||
# Process greets
|
||||
self.pm.protocols["foo"].outReceived(b"hello world!\n")
|
||||
self.assertEquals(len(events), 1)
|
||||
namespace = events[0]["log_namespace"]
|
||||
stream = events[0]["stream"]
|
||||
tag = events[0]["tag"]
|
||||
line = events[0]["line"]
|
||||
self.assertEquals(namespace, "twisted.runner.procmon.ProcessMonitor")
|
||||
self.assertEquals(stream, "stdout")
|
||||
self.assertEquals(tag, "foo")
|
||||
self.assertEquals(line, "hello world!")
|
||||
|
||||
def test_ouputReceivedCompleteErrLine(self):
|
||||
"""
|
||||
Getting a complete output line on stderr generates a log message.
|
||||
"""
|
||||
events = []
|
||||
self.addCleanup(globalLogPublisher.removeObserver, events.append)
|
||||
globalLogPublisher.addObserver(events.append)
|
||||
self.pm.addProcess("foo", ["foo"])
|
||||
# Schedule the process to start
|
||||
self.pm.startService()
|
||||
# Advance the reactor to start the process
|
||||
self.reactor.advance(0)
|
||||
self.assertIn("foo", self.pm.protocols)
|
||||
# Long time passes
|
||||
self.reactor.advance(self.pm.threshold)
|
||||
# Process greets
|
||||
self.pm.protocols["foo"].errReceived(b"hello world!\n")
|
||||
self.assertEquals(len(events), 1)
|
||||
namespace = events[0]["log_namespace"]
|
||||
stream = events[0]["stream"]
|
||||
tag = events[0]["tag"]
|
||||
line = events[0]["line"]
|
||||
self.assertEquals(namespace, "twisted.runner.procmon.ProcessMonitor")
|
||||
self.assertEquals(stream, "stderr")
|
||||
self.assertEquals(tag, "foo")
|
||||
self.assertEquals(line, "hello world!")
|
||||
|
||||
def test_outputReceivedCompleteLineInvalidUTF8(self):
|
||||
"""
|
||||
Getting invalid UTF-8 results in the repr of the raw message
|
||||
"""
|
||||
events = []
|
||||
self.addCleanup(globalLogPublisher.removeObserver, events.append)
|
||||
globalLogPublisher.addObserver(events.append)
|
||||
self.pm.addProcess("foo", ["foo"])
|
||||
# Schedule the process to start
|
||||
self.pm.startService()
|
||||
# Advance the reactor to start the process
|
||||
self.reactor.advance(0)
|
||||
self.assertIn("foo", self.pm.protocols)
|
||||
# Long time passes
|
||||
self.reactor.advance(self.pm.threshold)
|
||||
# Process greets
|
||||
self.pm.protocols["foo"].outReceived(b"\xffhello world!\n")
|
||||
self.assertEquals(len(events), 1)
|
||||
message = events[0]
|
||||
namespace = message["log_namespace"]
|
||||
stream = message["stream"]
|
||||
tag = message["tag"]
|
||||
output = message["line"]
|
||||
self.assertEquals(namespace, "twisted.runner.procmon.ProcessMonitor")
|
||||
self.assertEquals(stream, "stdout")
|
||||
self.assertEquals(tag, "foo")
|
||||
self.assertEquals(output, repr(b"\xffhello world!"))
|
||||
|
||||
def test_outputReceivedPartialLine(self):
|
||||
"""
|
||||
Getting partial line results in no events until process end
|
||||
"""
|
||||
events = []
|
||||
self.addCleanup(globalLogPublisher.removeObserver, events.append)
|
||||
globalLogPublisher.addObserver(events.append)
|
||||
self.pm.addProcess("foo", ["foo"])
|
||||
# Schedule the process to start
|
||||
self.pm.startService()
|
||||
# Advance the reactor to start the process
|
||||
self.reactor.advance(0)
|
||||
self.assertIn("foo", self.pm.protocols)
|
||||
# Long time passes
|
||||
self.reactor.advance(self.pm.threshold)
|
||||
# Process greets
|
||||
self.pm.protocols["foo"].outReceived(b"hello world!")
|
||||
self.assertEquals(len(events), 0)
|
||||
self.pm.protocols["foo"].processEnded(Failure(ProcessDone(0)))
|
||||
self.assertEquals(len(events), 1)
|
||||
namespace = events[0]["log_namespace"]
|
||||
stream = events[0]["stream"]
|
||||
tag = events[0]["tag"]
|
||||
line = events[0]["line"]
|
||||
self.assertEquals(namespace, "twisted.runner.procmon.ProcessMonitor")
|
||||
self.assertEquals(stream, "stdout")
|
||||
self.assertEquals(tag, "foo")
|
||||
self.assertEquals(line, "hello world!")
|
||||
|
||||
def test_connectionLostLongLivedProcess(self):
|
||||
"""
|
||||
L{ProcessMonitor.connectionLost} should immediately restart a process
|
||||
if it has been running longer than L{ProcessMonitor.threshold} seconds.
|
||||
"""
|
||||
self.pm.addProcess("foo", ["foo"])
|
||||
# Schedule the process to start
|
||||
self.pm.startService()
|
||||
# advance the reactor to start the process
|
||||
self.reactor.advance(0)
|
||||
self.assertIn("foo", self.pm.protocols)
|
||||
# Long time passes
|
||||
self.reactor.advance(self.pm.threshold)
|
||||
# Process dies after threshold
|
||||
self.pm.protocols["foo"].processEnded(Failure(ProcessDone(0)))
|
||||
self.assertNotIn("foo", self.pm.protocols)
|
||||
# Process should be restarted immediately
|
||||
self.reactor.advance(0)
|
||||
self.assertIn("foo", self.pm.protocols)
|
||||
|
||||
def test_connectionLostMurderCancel(self):
|
||||
"""
|
||||
L{ProcessMonitor.connectionLost} cancels a scheduled process killer and
|
||||
deletes the DelayedCall from the L{ProcessMonitor.murder} list.
|
||||
"""
|
||||
self.pm.addProcess("foo", ["foo"])
|
||||
# Schedule the process to start
|
||||
self.pm.startService()
|
||||
# Advance 1s to start the process then ask ProcMon to stop it
|
||||
self.reactor.advance(1)
|
||||
self.pm.stopProcess("foo")
|
||||
# A process killer has been scheduled, delayedCall is active
|
||||
self.assertIn("foo", self.pm.murder)
|
||||
delayedCall = self.pm.murder["foo"]
|
||||
self.assertTrue(delayedCall.active())
|
||||
# Advance to the point at which the dummy process exits
|
||||
self.reactor.advance(self.pm.protocols["foo"].transport._terminationDelay)
|
||||
# Now the delayedCall has been cancelled and deleted
|
||||
self.assertFalse(delayedCall.active())
|
||||
self.assertNotIn("foo", self.pm.murder)
|
||||
|
||||
def test_connectionLostProtocolDeletion(self):
|
||||
"""
|
||||
L{ProcessMonitor.connectionLost} removes the corresponding
|
||||
ProcessProtocol instance from the L{ProcessMonitor.protocols} list.
|
||||
"""
|
||||
self.pm.startService()
|
||||
self.pm.addProcess("foo", ["foo"])
|
||||
self.assertIn("foo", self.pm.protocols)
|
||||
self.pm.protocols["foo"].transport.signalProcess("KILL")
|
||||
self.reactor.advance(self.pm.protocols["foo"].transport._terminationDelay)
|
||||
self.assertNotIn("foo", self.pm.protocols)
|
||||
|
||||
def test_connectionLostMinMaxRestartDelay(self):
|
||||
"""
|
||||
L{ProcessMonitor.connectionLost} will wait at least minRestartDelay s
|
||||
and at most maxRestartDelay s
|
||||
"""
|
||||
self.pm.minRestartDelay = 2
|
||||
self.pm.maxRestartDelay = 3
|
||||
|
||||
self.pm.startService()
|
||||
self.pm.addProcess("foo", ["foo"])
|
||||
|
||||
self.assertEqual(self.pm.delay["foo"], self.pm.minRestartDelay)
|
||||
self.reactor.advance(self.pm.threshold - 1)
|
||||
self.pm.protocols["foo"].processEnded(Failure(ProcessDone(0)))
|
||||
self.assertEqual(self.pm.delay["foo"], self.pm.maxRestartDelay)
|
||||
|
||||
def test_connectionLostBackoffDelayDoubles(self):
|
||||
"""
|
||||
L{ProcessMonitor.connectionLost} doubles the restart delay each time
|
||||
the process dies too quickly.
|
||||
"""
|
||||
self.pm.startService()
|
||||
self.pm.addProcess("foo", ["foo"])
|
||||
self.reactor.advance(self.pm.threshold - 1) # 9s
|
||||
self.assertIn("foo", self.pm.protocols)
|
||||
self.assertEqual(self.pm.delay["foo"], self.pm.minRestartDelay)
|
||||
# process dies within the threshold and should not restart immediately
|
||||
self.pm.protocols["foo"].processEnded(Failure(ProcessDone(0)))
|
||||
self.assertEqual(self.pm.delay["foo"], self.pm.minRestartDelay * 2)
|
||||
|
||||
def test_startService(self):
|
||||
"""
|
||||
L{ProcessMonitor.startService} starts all monitored processes.
|
||||
"""
|
||||
self.pm.addProcess("foo", ["foo"])
|
||||
# Schedule the process to start
|
||||
self.pm.startService()
|
||||
# advance the reactor to start the process
|
||||
self.reactor.advance(0)
|
||||
self.assertIn("foo", self.pm.protocols)
|
||||
|
||||
def test_stopService(self):
|
||||
"""
|
||||
L{ProcessMonitor.stopService} should stop all monitored processes.
|
||||
"""
|
||||
self.pm.addProcess("foo", ["foo"])
|
||||
self.pm.addProcess("bar", ["bar"])
|
||||
# Schedule the process to start
|
||||
self.pm.startService()
|
||||
# advance the reactor to start the processes
|
||||
self.reactor.advance(self.pm.threshold)
|
||||
self.assertIn("foo", self.pm.protocols)
|
||||
self.assertIn("bar", self.pm.protocols)
|
||||
|
||||
self.reactor.advance(1)
|
||||
|
||||
self.pm.stopService()
|
||||
# Advance to beyond the killTime - all monitored processes
|
||||
# should have exited
|
||||
self.reactor.advance(self.pm.killTime + 1)
|
||||
# The processes shouldn't be restarted
|
||||
self.assertEqual({}, self.pm.protocols)
|
||||
|
||||
def test_restartAllRestartsOneProcess(self):
|
||||
"""
|
||||
L{ProcessMonitor.restartAll} succeeds when there is one process.
|
||||
"""
|
||||
self.pm.addProcess("foo", ["foo"])
|
||||
self.pm.startService()
|
||||
self.reactor.advance(1)
|
||||
self.pm.restartAll()
|
||||
# Just enough time for the process to die,
|
||||
# not enough time to start a new one.
|
||||
self.reactor.advance(1)
|
||||
processes = list(self.reactor.spawnedProcesses)
|
||||
myProcess = processes.pop()
|
||||
self.assertEquals(processes, [])
|
||||
self.assertIsNone(myProcess.pid)
|
||||
|
||||
def test_stopServiceCancelRestarts(self):
|
||||
"""
|
||||
L{ProcessMonitor.stopService} should cancel any scheduled process
|
||||
restarts.
|
||||
"""
|
||||
self.pm.addProcess("foo", ["foo"])
|
||||
# Schedule the process to start
|
||||
self.pm.startService()
|
||||
# advance the reactor to start the processes
|
||||
self.reactor.advance(self.pm.threshold)
|
||||
self.assertIn("foo", self.pm.protocols)
|
||||
|
||||
self.reactor.advance(1)
|
||||
# Kill the process early
|
||||
self.pm.protocols["foo"].processEnded(Failure(ProcessDone(0)))
|
||||
self.assertTrue(self.pm.restart["foo"].active())
|
||||
self.pm.stopService()
|
||||
# Scheduled restart should have been cancelled
|
||||
self.assertFalse(self.pm.restart["foo"].active())
|
||||
|
||||
def test_stopServiceCleanupScheduledRestarts(self):
|
||||
"""
|
||||
L{ProcessMonitor.stopService} should cancel all scheduled process
|
||||
restarts.
|
||||
"""
|
||||
self.pm.threshold = 5
|
||||
self.pm.minRestartDelay = 5
|
||||
# Start service and add a process (started immediately)
|
||||
self.pm.startService()
|
||||
self.pm.addProcess("foo", ["foo"])
|
||||
# Stop the process after 1s
|
||||
self.reactor.advance(1)
|
||||
self.pm.stopProcess("foo")
|
||||
# Wait 1s for it to exit it will be scheduled to restart 5s later
|
||||
self.reactor.advance(1)
|
||||
# Meanwhile stop the service
|
||||
self.pm.stopService()
|
||||
# Advance to beyond the process restart time
|
||||
self.reactor.advance(6)
|
||||
# The process shouldn't have restarted because stopService has cancelled
|
||||
# all pending process restarts.
|
||||
self.assertEqual(self.pm.protocols, {})
|
||||
|
||||
|
||||
class DeprecationTests(unittest.SynchronousTestCase):
|
||||
|
||||
"""
|
||||
Tests that check functionality that should be deprecated is deprecated.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create reactor and process monitor.
|
||||
"""
|
||||
self.reactor = DummyProcessReactor()
|
||||
self.pm = ProcessMonitor(reactor=self.reactor)
|
||||
|
||||
def test_toTuple(self):
|
||||
"""
|
||||
_Process.toTuple is deprecated.
|
||||
|
||||
When getting the deprecated processes property, the actual
|
||||
data (kept in the class _Process) is converted to a tuple --
|
||||
which produces a DeprecationWarning per process so converted.
|
||||
"""
|
||||
self.pm.addProcess("foo", ["foo"])
|
||||
myprocesses = self.pm.processes
|
||||
self.assertEquals(len(myprocesses), 1)
|
||||
warnings = self.flushWarnings()
|
||||
foundToTuple = False
|
||||
for warning in warnings:
|
||||
self.assertIs(warning["category"], DeprecationWarning)
|
||||
if "toTuple" in warning["message"]:
|
||||
foundToTuple = True
|
||||
self.assertTrue(foundToTuple, f"no tuple deprecation found:{repr(warnings)}")
|
||||
|
||||
def test_processes(self):
|
||||
"""
|
||||
Accessing L{ProcessMonitor.processes} results in deprecation warning
|
||||
|
||||
Even when there are no processes, and thus no process is converted
|
||||
to a tuple, accessing the L{ProcessMonitor.processes} property
|
||||
should generate its own DeprecationWarning.
|
||||
"""
|
||||
myProcesses = self.pm.processes
|
||||
self.assertEquals(myProcesses, {})
|
||||
warnings = self.flushWarnings()
|
||||
first = warnings.pop(0)
|
||||
self.assertIs(first["category"], DeprecationWarning)
|
||||
self.assertEquals(warnings, [])
|
||||
|
||||
def test_getstate(self):
|
||||
"""
|
||||
Pickling an L{ProcessMonitor} results in deprecation warnings
|
||||
"""
|
||||
pickle.dumps(self.pm)
|
||||
warnings = self.flushWarnings()
|
||||
for warning in warnings:
|
||||
self.assertIs(warning["category"], DeprecationWarning)
|
||||
@@ -0,0 +1,81 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.runner.procmontap}.
|
||||
"""
|
||||
|
||||
from twisted.python.usage import UsageError
|
||||
from twisted.runner import procmontap as tap
|
||||
from twisted.runner.procmon import ProcessMonitor
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
class ProcessMonitorTapTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{twisted.runner.procmontap}'s option parsing and makeService
|
||||
method.
|
||||
"""
|
||||
|
||||
def test_commandLineRequired(self) -> None:
|
||||
"""
|
||||
The command line arguments must be provided.
|
||||
"""
|
||||
opt = tap.Options()
|
||||
self.assertRaises(UsageError, opt.parseOptions, [])
|
||||
|
||||
def test_threshold(self) -> None:
|
||||
"""
|
||||
The threshold option is recognised as a parameter and coerced to
|
||||
float.
|
||||
"""
|
||||
opt = tap.Options()
|
||||
opt.parseOptions(["--threshold", "7.5", "foo"])
|
||||
self.assertEqual(opt["threshold"], 7.5)
|
||||
|
||||
def test_killTime(self) -> None:
|
||||
"""
|
||||
The killtime option is recognised as a parameter and coerced to float.
|
||||
"""
|
||||
opt = tap.Options()
|
||||
opt.parseOptions(["--killtime", "7.5", "foo"])
|
||||
self.assertEqual(opt["killtime"], 7.5)
|
||||
|
||||
def test_minRestartDelay(self) -> None:
|
||||
"""
|
||||
The minrestartdelay option is recognised as a parameter and coerced to
|
||||
float.
|
||||
"""
|
||||
opt = tap.Options()
|
||||
opt.parseOptions(["--minrestartdelay", "7.5", "foo"])
|
||||
self.assertEqual(opt["minrestartdelay"], 7.5)
|
||||
|
||||
def test_maxRestartDelay(self) -> None:
|
||||
"""
|
||||
The maxrestartdelay option is recognised as a parameter and coerced to
|
||||
float.
|
||||
"""
|
||||
opt = tap.Options()
|
||||
opt.parseOptions(["--maxrestartdelay", "7.5", "foo"])
|
||||
self.assertEqual(opt["maxrestartdelay"], 7.5)
|
||||
|
||||
def test_parameterDefaults(self) -> None:
|
||||
"""
|
||||
The parameters all have default values
|
||||
"""
|
||||
opt = tap.Options()
|
||||
opt.parseOptions(["foo"])
|
||||
self.assertEqual(opt["threshold"], 1)
|
||||
self.assertEqual(opt["killtime"], 5)
|
||||
self.assertEqual(opt["minrestartdelay"], 1)
|
||||
self.assertEqual(opt["maxrestartdelay"], 3600)
|
||||
|
||||
def test_makeService(self) -> None:
|
||||
"""
|
||||
The command line gets added as a process to the ProcessMontor.
|
||||
"""
|
||||
opt = tap.Options()
|
||||
opt.parseOptions(["ping", "-c", "3", "8.8.8.8"])
|
||||
s = tap.makeService(opt)
|
||||
self.assertIsInstance(s, ProcessMonitor)
|
||||
self.assertIn("ping -c 3 8.8.8.8", s.processes)
|
||||
Reference in New Issue
Block a user