diff -Nru twisted-runner-10.0.0/debian/changelog twisted-runner-12.1.0/debian/changelog --- twisted-runner-10.0.0/debian/changelog 2012-06-29 14:13:57.000000000 +0000 +++ twisted-runner-12.1.0/debian/changelog 2012-06-29 13:51:54.000000000 +0000 @@ -1,3 +1,17 @@ +twisted-runner (12.1.0-1~ppa1~lucid1) lucid; urgency=low + + * New upstream version. + * Rebuild package for use in PPA. + + -- Jessica McKellar Fri, 29 Jun 2012 09:51:36 -0400 + +twisted-runner (12.0.0-1~ppa1~lucid1) lucid; urgency=low + + * New upstream version. + * Rebuild package for use in PPA. + + -- Jessica McKellar Sat, 18 Feb 2012 20:42:06 -0500 + twisted-runner (10.0.0-1) unstable; urgency=low * New upstream release diff -Nru twisted-runner-10.0.0/debian/compat twisted-runner-12.1.0/debian/compat --- twisted-runner-10.0.0/debian/compat 2012-06-29 14:13:57.000000000 +0000 +++ twisted-runner-12.1.0/debian/compat 2012-06-29 13:51:11.000000000 +0000 @@ -1 +1 @@ -5 +7 diff -Nru twisted-runner-10.0.0/debian/control twisted-runner-12.1.0/debian/control --- twisted-runner-10.0.0/debian/control 2012-06-29 14:13:57.000000000 +0000 +++ twisted-runner-12.1.0/debian/control 2012-06-29 13:51:33.000000000 +0000 @@ -3,18 +3,17 @@ Priority: optional Maintainer: Matthias Klose Uploaders: Free Ekanayaka -Build-Depends: debhelper (>= 5.0.37.1), python-central (>= 0.6.11), python-all-dev, python-all-dbg, python-twisted-core (>= 10.0), patch +Build-Depends: debhelper (>=7.0.50~), python-central (>= 0.6.11), python-all-dev, python-all-dbg, python-twisted-core (>= 12.1), patch XS-Python-Version: >= 2.4 -Standards-Version: 3.8.4 +Standards-Version: 3.9.2 Package: python-twisted-runner Architecture: any -Depends: ${python:Depends}, python-twisted-core (>= 10.0), ${shlibs:Depends}, ${misc:Depends} -Conflicts: python2.3-twisted-runner, python2.4-twisted-runner, python2.3-twisted-bin (<< 0.2.0), python2.4-twisted-bin (<< 0.2.0) -Replaces: python2.3-twisted-runner, python2.4-twisted-runner, python2.3-twisted-bin (<< 0.2.0), python2.4-twisted-bin (<< 0.2.0) +Depends: ${python:Depends}, python-twisted-core (>= 12.1), ${shlibs:Depends}, ${misc:Depends} +Conflicts: python2.3-twisted-runner, python2.4-twisted-runner, python2.3-twisted-bin, python2.4-twisted-bin +Replaces: python2.3-twisted-runner, python2.4-twisted-runner, python2.3-twisted-bin, python2.4-twisted-bin Provides: ${python:Provides} Suggests: python-twisted-runner-dbg -XB-Python-Version: ${python:Versions} Description: Process management, including an inetd server Twisted Runner has process management, including an inetd replacement. diff -Nru twisted-runner-10.0.0/debian/python-twisted-runner.triggers twisted-runner-12.1.0/debian/python-twisted-runner.triggers --- twisted-runner-10.0.0/debian/python-twisted-runner.triggers 1970-01-01 00:00:00.000000000 +0000 +++ twisted-runner-12.1.0/debian/python-twisted-runner.triggers 2012-06-29 13:51:11.000000000 +0000 @@ -0,0 +1 @@ +activate twisted-plugins-cache diff -Nru twisted-runner-10.0.0/debian/rules twisted-runner-12.1.0/debian/rules --- twisted-runner-10.0.0/debian/rules 2012-06-29 14:13:57.000000000 +0000 +++ twisted-runner-12.1.0/debian/rules 2012-06-29 13:51:11.000000000 +0000 @@ -7,6 +7,8 @@ VER := $(shell /usr/bin/python -c 'import sys; print sys.version[:3]') build: build-stamp +build-arch: build-stamp +build-indep: build-stamp build-stamp: $(PYVERS:%=build-python%) $(PYVERS:%=dbg-build-python%) touch $@ build-python%: @@ -20,6 +22,7 @@ rm -rf *-stamp build-python* dbg-build-python* build rm -rf $(addprefix debian/,$(packages)) debian/files debian/substvars rm -rf _trial_temp test.log + rm -f twisted/plugins/dropin.cache find . -name "*.pyc" |xargs -r rm dh_clean @@ -28,7 +31,7 @@ install-prereq: build-stamp dh_testdir dh_testroot - dh_clean -k + dh_prep install-python%: install-prereq : # python-twisted-runner @@ -87,7 +90,6 @@ dh_testroot dh_installchangelogs -i dh_installdocs -i -A NEWS README -# dh_installdocs -ppython-twisted-runner doc/examples doc/howto dh_installmenu -i dh_compress -i -X.py dh_fixperms -i diff -Nru twisted-runner-10.0.0/debian/source/format twisted-runner-12.1.0/debian/source/format --- twisted-runner-10.0.0/debian/source/format 1970-01-01 00:00:00.000000000 +0000 +++ twisted-runner-12.1.0/debian/source/format 2012-06-29 14:13:57.000000000 +0000 @@ -0,0 +1 @@ +3.0 (quilt) diff -Nru twisted-runner-10.0.0/LICENSE twisted-runner-12.1.0/LICENSE --- twisted-runner-10.0.0/LICENSE 2010-03-09 13:41:21.000000000 +0000 +++ twisted-runner-12.1.0/LICENSE 2012-02-11 13:47:35.000000000 +0000 @@ -1,4 +1,4 @@ -Copyright (c) 2001-2010 +Copyright (c) 2001-2012 Allen Short Andy Gayton Andrew Bennetts @@ -12,7 +12,7 @@ Donovan Preston Eric Mangold Eyal Lotem -Itamar Shtull-Trauring +Itamar Turner-Trauring James Knight Jason A. Mobarak Jean-Paul Calderone diff -Nru twisted-runner-10.0.0/NEWS twisted-runner-12.1.0/NEWS --- twisted-runner-10.0.0/NEWS 2010-03-09 13:41:21.000000000 +0000 +++ twisted-runner-12.1.0/NEWS 2012-06-02 07:04:01.000000000 +0000 @@ -1,6 +1,58 @@ Ticket numbers in this file can be looked up by visiting http://twistedmatrix.com/trac/ticket/ +Twisted Runner 12.1.0 (2012-06-02) +================================== + +Deprecations and Removals +------------------------- + - ProcessMonitor.active, consistencyDelay, and consistency in + twisted.runner.procmon were deprecated since 10.1 have been + removed. (#5517) + + +Twisted Runner 12.0.0 (2012-02-10) +================================== + +No significant changes have been made for this release. + + +Twisted Runner 11.1.0 (2011-11-15) +================================== + +No significant changes have been made for this release. + + +Twisted Runner 11.0.0 (2011-04-01) +================================== + +No significant changes have been made for this release. + + +Twisted Runner 10.2.0 (2010-11-29) +================================== + +No significant changes have been made for this release. + + +Twisted Runner 10.1.0 (2010-06-27) +================================== + +Features +-------- + - twistd now has a procmon subcommand plugin - a convenient way to + monitor and automatically restart another process. (#4356) + +Deprecations and Removals +------------------------- + - twisted.runner.procmon.ProcessMonitor's active, consistency, and + consistencyDelay attributes are now deprecated. (#1763) + +Other +----- + - #3775 + + Twisted Runner 10.0.0 (2010-03-01) ================================== diff -Nru twisted-runner-10.0.0/README twisted-runner-12.1.0/README --- twisted-runner-10.0.0/README 2010-03-09 13:41:21.000000000 +0000 +++ twisted-runner-12.1.0/README 2012-06-02 07:04:01.000000000 +0000 @@ -1,2 +1,3 @@ -Twisted Runner 10.0.0 +Twisted Runner 12.1.0 +Twisted Runner depends on Twisted. diff -Nru twisted-runner-10.0.0/setup.py twisted-runner-12.1.0/setup.py --- twisted-runner-10.0.0/setup.py 2010-03-09 13:41:21.000000000 +0000 +++ twisted-runner-12.1.0/setup.py 2011-02-14 04:45:15.000000000 +0000 @@ -1,4 +1,4 @@ -# Copyright (c) 2008 Twisted Matrix Laboratories. +# Copyright (c) Twisted Matrix Laboratories. # See LICENSE for details. try: diff -Nru twisted-runner-10.0.0/twisted/plugins/twisted_runner.py twisted-runner-12.1.0/twisted/plugins/twisted_runner.py --- twisted-runner-10.0.0/twisted/plugins/twisted_runner.py 1970-01-01 00:00:00.000000000 +0000 +++ twisted-runner-12.1.0/twisted/plugins/twisted_runner.py 2011-02-14 04:45:15.000000000 +0000 @@ -0,0 +1,10 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +from twisted.application.service import ServiceMaker + +TwistedProcmon = ServiceMaker( + "Twisted Process Monitor", + "twisted.runner.procmontap", + ("A process watchdog / supervisor"), + "procmon") diff -Nru twisted-runner-10.0.0/twisted/runner/inetdconf.py twisted-runner-12.1.0/twisted/runner/inetdconf.py --- twisted-runner-10.0.0/twisted/runner/inetdconf.py 2010-03-09 13:41:21.000000000 +0000 +++ twisted-runner-12.1.0/twisted/runner/inetdconf.py 2011-02-14 04:45:15.000000000 +0000 @@ -1,4 +1,4 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# Copyright (c) Twisted Matrix Laboratories. # See LICENSE for details. # diff -Nru twisted-runner-10.0.0/twisted/runner/inetd.py twisted-runner-12.1.0/twisted/runner/inetd.py --- twisted-runner-10.0.0/twisted/runner/inetd.py 2010-03-09 13:41:21.000000000 +0000 +++ twisted-runner-12.1.0/twisted/runner/inetd.py 2011-02-14 04:45:15.000000000 +0000 @@ -1,4 +1,4 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# Copyright (c) Twisted Matrix Laboratories. # See LICENSE for details. # diff -Nru twisted-runner-10.0.0/twisted/runner/inetdtap.py twisted-runner-12.1.0/twisted/runner/inetdtap.py --- twisted-runner-10.0.0/twisted/runner/inetdtap.py 2010-03-09 13:41:21.000000000 +0000 +++ twisted-runner-12.1.0/twisted/runner/inetdtap.py 2011-10-18 10:31:11.000000000 +0000 @@ -1,4 +1,4 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# Copyright (c) Twisted Matrix Laboratories. # See LICENSE for details. # @@ -37,7 +37,10 @@ ] optFlags = [['nointernal', 'i', "Don't run internal services"]] - zsh_actions = {"file" : "_files -g '*.conf'"} + + compData = usage.Completions( + optActions={"file": usage.CompleteFiles('*.conf')} + ) class RPCServer(internet.TCPServer): diff -Nru twisted-runner-10.0.0/twisted/runner/procmon.py twisted-runner-12.1.0/twisted/runner/procmon.py --- twisted-runner-10.0.0/twisted/runner/procmon.py 2010-03-09 13:41:21.000000000 +0000 +++ twisted-runner-12.1.0/twisted/runner/procmon.py 2012-03-12 18:14:10.000000000 +0000 @@ -1,14 +1,14 @@ -# Copyright (c) 2001-2010 Twisted Matrix Laboratories. +# -*- 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. """ - -import os, time +import warnings from twisted.python import log -from twisted.internet import error, protocol, reactor +from twisted.internet import error, protocol, reactor as _reactor from twisted.application import service from twisted.protocols import basic @@ -26,6 +26,7 @@ def lineReceived(self, line): log.msg('[%s] %s' % (self.tag, line)) + class LoggingProtocol(protocol.ProcessProtocol): service = None @@ -37,12 +38,14 @@ self.output.tag = self.name self.output.makeConnection(transport) + def outReceived(self, data): self.output.dataReceived(data) self.empty = data[-1] == '\n' errReceived = outReceived + def processEnded(self, reason): if not self.empty: self.output.dataReceived('\n') @@ -51,171 +54,232 @@ class ProcessMonitor(service.Service): """ - ProcessMonitor runs processes, monitors their progress, and restarts them - when they die. + 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 active (that is, when the application it is attached to is running), - adding a process automatically starts it. + 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}. + 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. + 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 consistencyDelay: C{float} - @ivar consistencyDelay: The time between consistency checks. The default - value is 60 seconds. + @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. + """ threshold = 1 - active = 0 killTime = 5 - consistency = None - consistencyDelay = 60 + minRestartDelay = 1 + maxRestartDelay = 3600 + + + def __init__(self, reactor=_reactor): + self._reactor = reactor - def __init__(self): self.processes = {} self.protocols = {} self.delay = {} self.timeStarted = {} self.murder = {} + self.restart = {} + def __getstate__(self): dct = service.Service.__getstate__(self) - for k in ('active', 'consistency'): - if dct.has_key(k): - del dct[k] + del dct['_reactor'] dct['protocols'] = {} dct['delay'] = {} dct['timeStarted'] = {} dct['murder'] = {} + dct['restart'] = {} return dct - def _checkConsistency(self): - for name, protocol in self.protocols.items(): - proc = protocol.transport - try: - proc.signalProcess(0) - except (OSError, error.ProcessExitedAlready): - log.msg("Lost process %r somehow, restarting." % name) - del self.protocols[name] - self.startProcess(name) - self.consistency = reactor.callLater(self.consistencyDelay, - self._checkConsistency) - def addProcess(self, name, args, uid=None, gid=None, env={}): """ - Add a new process to launch, monitor, and restart when necessary. + 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{.addProcess("name", ['/bin/sh', '-c', shell_script])} + C{ProcessMonitor.addProcess("name", ['/bin/sh', '-c', shell_script])} - See L{removeProcess} for removing processes from the monitor. - - @param name: A label for this process. This value must be unique - across all processes added to this monitor. + @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 C{None}, the - current UID is used. + @param uid: The user ID to use to run the process. If C{None}, + the current UID is used. @type uid: C{int} - @param gid: The group ID to use to run the process. If C{None}, the - current GID is used. + @param gid: The group ID to use to run the process. If C{None}, + the current GID is used. @type uid: C{int} - @param env: The environment to give to the launched process. See + @param env: The environment to give to the launched process. See L{IReactorProcess.spawnProcess}'s C{env} parameter. @type env: C{dict} + @raises: C{KeyError} if a process with the given name already + exists """ if name in self.processes: - raise KeyError("remove %s first" % name) + raise KeyError("remove %s first" % (name,)) self.processes[name] = args, uid, gid, env - if self.active: + self.delay[name] = self.minRestartDelay + if self.running: self.startProcess(name) def removeProcess(self, name): """ - If the process is started, kill it. It will never get restarted. - - See L{addProcess} for adding processes to the monitor. + Stop the named process and remove it from the list of monitored + processes. @type name: C{str} - @param name: The string that uniquely identifies the process. + @param name: A string that uniquely identifies the process. """ - del self.processes[name] self.stopProcess(name) + del self.processes[name] def startService(self): + """ + Start all monitored processes. + """ service.Service.startService(self) - self.active = 1 - for name in self.processes.keys(): - reactor.callLater(0, self.startProcess, name) - self.consistency = reactor.callLater(self.consistencyDelay, - self._checkConsistency) + for name in self.processes: + self.startProcess(name) + def stopService(self): + """ + Stop all monitored processes and cancel all scheduled process restarts. + """ service.Service.stopService(self) - self.active = 0 - for name in self.processes.keys(): + + # Cancel any outstanding restarts + for name, delayedCall in self.restart.items(): + if delayedCall.active(): + delayedCall.cancel() + + for name in self.processes: self.stopProcess(name) - self.consistency.cancel() + def connectionLost(self, name): - if self.murder.has_key(name): - self.murder[name].cancel() + """ + Called when a monitored processes exits. If + L{ProcessMonitor.running} is C{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] - if self.protocols.has_key(name): - del self.protocols[name] - if time.time()-self.timeStarted[name]') - -def main(): - from signal import SIGTERM - mon = ProcessMonitor() - mon.addProcess('foo', ['/bin/sh', '-c', 'sleep 2;echo hello']) - mon.addProcess('qux', ['/bin/sh', '-c', 'sleep 2;printf pilim']) - mon.addProcess('bar', ['/bin/sh', '-c', 'echo goodbye']) - mon.addProcess('baz', ['/bin/sh', '-c', - 'echo welcome;while :;do echo blah;sleep 5;done']) - reactor.callLater(30, lambda mon=mon: - os.kill(mon.protocols['baz'].transport.pid, SIGTERM)) - reactor.callLater(60, mon.restartAll) - mon.startService() - reactor.addSystemEventTrigger('before', 'shutdown', mon.stopService) - reactor.run() - -if __name__ == '__main__': - main() diff -Nru twisted-runner-10.0.0/twisted/runner/procmontap.py twisted-runner-12.1.0/twisted/runner/procmontap.py --- twisted-runner-10.0.0/twisted/runner/procmontap.py 1970-01-01 00:00:00.000000000 +0000 +++ twisted-runner-12.1.0/twisted/runner/procmontap.py 2011-10-18 10:31:11.000000000 +0000 @@ -0,0 +1,73 @@ +# -*- 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 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 = [] + + + 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): + """ + Grab the command line that is going to be started and monitored + """ + self['args'] = args + + + def postOptions(self): + """ + Check for dependencies. + """ + if len(self["args"]) < 1: + raise usage.UsageError("Please specify a process commandline") + + + +def makeService(config): + 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 diff -Nru twisted-runner-10.0.0/twisted/runner/procutils.py twisted-runner-12.1.0/twisted/runner/procutils.py --- twisted-runner-10.0.0/twisted/runner/procutils.py 2010-03-09 13:41:21.000000000 +0000 +++ twisted-runner-12.1.0/twisted/runner/procutils.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,5 +0,0 @@ -import warnings -warnings.warn("twisted.runner.procutils is DEPRECATED. import twisted.python.procutils instead.", - DeprecationWarning, stacklevel=2) - -from twisted.python.procutils import which diff -Nru twisted-runner-10.0.0/twisted/runner/test/__init__.py twisted-runner-12.1.0/twisted/runner/test/__init__.py --- twisted-runner-10.0.0/twisted/runner/test/__init__.py 2010-03-09 13:41:21.000000000 +0000 +++ twisted-runner-12.1.0/twisted/runner/test/__init__.py 2011-02-14 04:45:15.000000000 +0000 @@ -1,4 +1,4 @@ -# Copyright (c) 2009 Twisted Matrix Laboratories. +# Copyright (c) Twisted Matrix Laboratories. # See LICENSE for details. """ diff -Nru twisted-runner-10.0.0/twisted/runner/test/test_procmon.py twisted-runner-12.1.0/twisted/runner/test/test_procmon.py --- twisted-runner-10.0.0/twisted/runner/test/test_procmon.py 2010-03-09 13:41:21.000000000 +0000 +++ twisted-runner-12.1.0/twisted/runner/test/test_procmon.py 2012-03-12 18:14:10.000000000 +0000 @@ -1,4 +1,4 @@ -# Copyright (c) 2009 Twisted Matrix Laboratories. +# Copyright (c) Twisted Matrix Laboratories. # See LICENSE for details. """ @@ -6,50 +6,472 @@ """ from twisted.trial import unittest -from twisted.runner.procmon import ProcessMonitor -from twisted.internet import reactor +from twisted.runner.procmon import LoggingProtocol, ProcessMonitor +from twisted.internet.error import (ProcessDone, ProcessTerminated, + ProcessExitedAlready) +from twisted.internet.task import Clock +from twisted.python.failure import Failure +from twisted.test.proto_helpers import MemoryReactor + + + +class DummyProcess(object): + """ + 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_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} starts the named program and tracks the - resulting process, a protocol for collecting its stdout, and the time - it was started. - """ - spawnedProcesses = [] - def fakeSpawnProcess(*args, **kwargs): - spawnedProcesses.append((args, kwargs)) - self.patch(reactor, "spawnProcess", fakeSpawnProcess) - pm = ProcessMonitor() - pm.active = True - pm.addProcess("foo", ["arg1", "arg2"], uid=1, gid=2) - self.assertEquals(pm.processes, {"foo": (["arg1", "arg2"], 1, 2, {})}) - self.assertEquals(pm.protocols.keys(), ["foo"]) - lp = pm.protocols["foo"] - self.assertEquals( - spawnedProcesses, - [((lp, "arg1", ["arg1", "arg2"]), - {"uid": 1, "gid": 2, "env": {}})]) + 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(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 C{spawnProcess}. + L{ProcessMonitor.addProcess} takes an C{env} parameter that is passed to + L{IReactorProcess.spawnProcess}. """ - spawnedProcesses = [] - def fakeSpawnProcess(*args, **kwargs): - spawnedProcesses.append((args, kwargs)) - self.patch(reactor, "spawnProcess", fakeSpawnProcess) - pm = ProcessMonitor() - pm.active = True fakeEnv = {"KEY": "value"} - pm.addProcess("foo", ["foo"], uid=1, gid=2, env=fakeEnv) - self.assertEquals( - spawnedProcesses, - [((pm.protocols["foo"], "foo", ["foo"]), - {"uid": 1, "gid": 2, "env": fakeEnv})]) + 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_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.assertIdentical(None, 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) + + # 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 + 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.assertIdentical(None, self.pm.stopProcess("foo")) + + + 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.assertTrue("foo" in 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_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, {}) + diff -Nru twisted-runner-10.0.0/twisted/runner/test/test_procmontap.py twisted-runner-12.1.0/twisted/runner/test/test_procmontap.py --- twisted-runner-10.0.0/twisted/runner/test/test_procmontap.py 1970-01-01 00:00:00.000000000 +0000 +++ twisted-runner-12.1.0/twisted/runner/test/test_procmontap.py 2011-07-14 19:05:14.000000000 +0000 @@ -0,0 +1,87 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Tests for L{twisted.runner.procmontap}. +""" + +from twisted.python.usage import UsageError +from twisted.trial import unittest +from twisted.runner.procmon import ProcessMonitor +from twisted.runner import procmontap as tap + + +class ProcessMonitorTapTest(unittest.TestCase): + """ + Tests for L{twisted.runner.procmontap}'s option parsing and makeService + method. + """ + + def test_commandLineRequired(self): + """ + The command line arguments must be provided. + """ + opt = tap.Options() + self.assertRaises(UsageError, opt.parseOptions, []) + + + def test_threshold(self): + """ + 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): + """ + 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): + """ + 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): + """ + 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): + """ + 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): + """ + 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) diff -Nru twisted-runner-10.0.0/twisted/runner/_version.py twisted-runner-12.1.0/twisted/runner/_version.py --- twisted-runner-10.0.0/twisted/runner/_version.py 2010-03-09 13:41:21.000000000 +0000 +++ twisted-runner-12.1.0/twisted/runner/_version.py 2012-06-02 07:04:01.000000000 +0000 @@ -1,3 +1,3 @@ # This is an auto-generated file. Do not edit it. from twisted.python import versions -version = versions.Version('twisted.runner', 10, 0, 0) +version = versions.Version('twisted.runner', 12, 1, 0)