diff -Nru twisted-runner-0.2.0/debian/changelog twisted-runner-12.0.0/debian/changelog --- twisted-runner-0.2.0/debian/changelog 2012-02-22 01:36:47.000000000 +0000 +++ twisted-runner-12.0.0/debian/changelog 2012-02-22 01:36:47.000000000 +0000 @@ -1,8 +1,40 @@ -twisted-runner (0.2.0-4build1) hardy; urgency=low +twisted-runner (12.0.0-1~ppa1~hardy1) hardy; urgency=low - * Rebuild with recent python-central. + * New upstream version. + * Rebuild package for use in PPA. + * Drop support for Python 2.4. + + -- Jessica McKellar Tue, 21 Feb 2012 20:26:29 -0500 + +twisted-runner (10.1.0-0~ppa1~hardy1) hardy; urgency=low + + * New upstream version. + + -- Esteve Fernandez Mon, 12 Jul 2010 19:45:21 +0200 + +twisted-runner (10.0.0-0~ppa1~hardy1) hardy; urgency=low + + * New upstream version. + + -- Esteve Fernandez Thu, 04 Mar 2010 12:27:00 +0100 + +twisted-runner (9.0.0-1~ppa1~hardy1) hardy; urgency=low + + * New upstream version. + + -- Esteve Fernandez Fri, 08 Jan 2010 01:23:41 +0100 + +twisted-runner (8.2.0-1~ppa1~hardy1) hardy; urgency=low + + * New upstream version. + + -- Esteve Fernandez Fri, 24 Jul 2009 10:31:48 +0200 + +twisted-runner (8.0.0-1) unstable; urgency=low + + * New upstream version. - -- Matthias Klose Mon, 10 Mar 2008 12:48:08 +0000 + -- Matthias Klose Sun, 30 Mar 2008 22:05:18 +0200 twisted-runner (0.2.0-4) unstable; urgency=low diff -Nru twisted-runner-0.2.0/debian/compat twisted-runner-12.0.0/debian/compat --- twisted-runner-0.2.0/debian/compat 2012-02-22 01:36:47.000000000 +0000 +++ twisted-runner-12.0.0/debian/compat 2012-02-22 01:36:47.000000000 +0000 @@ -1 +1 @@ -5 +6 diff -Nru twisted-runner-0.2.0/debian/control twisted-runner-12.0.0/debian/control --- twisted-runner-0.2.0/debian/control 2012-02-22 01:36:47.000000000 +0000 +++ twisted-runner-12.0.0/debian/control 2012-02-22 01:36:47.000000000 +0000 @@ -2,15 +2,15 @@ Section: python Priority: optional Maintainer: Matthias Klose -Build-Depends: debhelper (>= 5.0.37.1), python-central (>= 0.4.17), python-all-dev, python-all-dbg, python-twisted-core (>= 2.5), patch -XS-Python-Version: all, >= 2.4 -Standards-Version: 3.7.2 +Build-Depends: debhelper (>= 5.0.37.1), python-central (>= 0.6.7), python-all-dev, python-all-dbg, python-twisted-core (>= 12.0), patch +XS-Python-Version: >= 2.5 +Standards-Version: 3.9.2 Package: python-twisted-runner Architecture: any -Depends: ${python:Depends}, python-twisted-core (>= 2.4) -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.0), ${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} @@ -18,9 +18,10 @@ Twisted Runner has process management, including an inetd replacement. Package: python-twisted-runner-dbg +Section: debug Priority: extra Architecture: any -Depends: python-twisted-runner (= ${Source-Version}), python-dbg, ${shlibs:Depends} +Depends: python-twisted-runner (= ${binary:Version}), python-dbg, ${shlibs:Depends}, ${misc:Depends} Description: Process management, including an inetd server (debug extension) Twisted Runner has process management, including an inetd replacement. . diff -Nru twisted-runner-0.2.0/debian/copyright twisted-runner-12.0.0/debian/copyright --- twisted-runner-0.2.0/debian/copyright 2012-02-22 01:36:47.000000000 +0000 +++ twisted-runner-12.0.0/debian/copyright 2012-02-22 01:36:47.000000000 +0000 @@ -17,7 +17,7 @@ Jonathan Lange Jonathan D. Simms Jp Calderone -Jürgen Hermann +Juergen Hermann Kevin Turner Mary Gardiner Matthew Lefkowitz diff -Nru twisted-runner-0.2.0/debian/python-twisted-runner.triggers twisted-runner-12.0.0/debian/python-twisted-runner.triggers --- twisted-runner-0.2.0/debian/python-twisted-runner.triggers 1970-01-01 00:00:00.000000000 +0000 +++ twisted-runner-12.0.0/debian/python-twisted-runner.triggers 2012-02-22 01:36:47.000000000 +0000 @@ -0,0 +1 @@ +activate twisted-plugins-cache diff -Nru twisted-runner-0.2.0/debian/rules twisted-runner-12.0.0/debian/rules --- twisted-runner-0.2.0/debian/rules 2012-02-22 01:36:47.000000000 +0000 +++ twisted-runner-12.0.0/debian/rules 2012-02-22 01:36:47.000000000 +0000 @@ -3,10 +3,12 @@ SHELL = /bin/bash # all versions -PYVERS := $(shell pyversions -vs) +PYVERS := $(shell pyversions -rvs debian/control) 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 @@ -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-0.2.0/LICENSE twisted-runner-12.0.0/LICENSE --- twisted-runner-0.2.0/LICENSE 2006-04-18 02:43:54.000000000 +0000 +++ twisted-runner-12.0.0/LICENSE 2012-01-23 13:19:44.000000000 +0000 @@ -1,20 +1,27 @@ -Copyright (c) 2001-2006 +Copyright (c) 2001-2012 Allen Short +Andy Gayton Andrew Bennetts +Antoine Pitrou Apple Computer, Inc. Benjamin Bruheim Bob Ippolito Canonical Limited Christopher Armstrong +David Reid Donovan Preston Eric Mangold -Itamar Shtull-Trauring +Eyal Lotem +Itamar Turner-Trauring James Knight Jason A. Mobarak +Jean-Paul Calderone +Jessica McKellar +Jonathan Jacobs Jonathan Lange Jonathan D. Simms -Jp Calderone -Jürgen Hermann +Jürgen Hermann +Kevin Horn Kevin Turner Mary Gardiner Matthew Lefkowitz @@ -24,7 +31,11 @@ Pavel Pergamenshchik Ralph Meijer Sean Riley +Software Freedom Conservancy Travis B. Hartwell +Thijs Triemstra +Thomas Herve +Timothy Allen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff -Nru twisted-runner-0.2.0/NEWS twisted-runner-12.0.0/NEWS --- twisted-runner-0.2.0/NEWS 2006-05-24 14:56:34.000000000 +0000 +++ twisted-runner-12.0.0/NEWS 2012-02-10 14:56:39.000000000 +0000 @@ -1,3 +1,82 @@ +Ticket numbers in this file can be looked up by visiting +http://twistedmatrix.com/trac/ticket/ + +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) +================================== + +Other +----- + - #3961 + + +Twisted Runner 9.0.0 (2009-11-24) +================================= + +Features +-------- + - procmon.ProcessMonitor.addProcess now accepts an 'env' parameter which allows + users to specify the environment in which a process will be run (#3691) + +Other +----- + - #3540 + + +Runner 8.2.0 (2008-12-16) +========================= + +No interesting changes since Twisted 8.0. + +8.0.0 (2008-03-17) +================== + +Misc +---- + - Remove all "API Stability" markers (#2847) + + 0.2.0 (2006-05-24) ================== diff -Nru twisted-runner-0.2.0/README twisted-runner-12.0.0/README --- twisted-runner-0.2.0/README 2006-05-24 14:56:34.000000000 +0000 +++ twisted-runner-12.0.0/README 2012-02-10 14:56:39.000000000 +0000 @@ -1,2 +1,3 @@ -Twisted Runner 0.2.0 +Twisted Runner 12.0.0 +Twisted Runner depends on Twisted. diff -Nru twisted-runner-0.2.0/setup.py twisted-runner-12.0.0/setup.py --- twisted-runner-0.2.0/setup.py 2006-05-13 19:26:09.000000000 +0000 +++ twisted-runner-12.0.0/setup.py 2011-02-14 04:45:15.000000000 +0000 @@ -1,33 +1,29 @@ - -from distutils.core import Extension +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. try: - from twisted.python import dist + from twisted.python.dist import setup, ConditionalExtension as Extension except ImportError: raise SystemExit("twisted.python.dist module not found. Make sure you " "have installed the Twisted core package before " "attempting to install any other Twisted projects.") -def detectExtensions(builder): - if builder._check_header("rpc/rpc.h"): - return [Extension("twisted.runner.portmap", - ["twisted/runner/portmap.c"], - define_macros=builder.define_macros)] - else: - builder.announce("Sun-RPC portmap support is unavailable on this " - "system (but that's OK, you probably don't need it " - "anyway).") +extensions = [ + Extension("twisted.runner.portmap", + ["twisted/runner/portmap.c"], + condition=lambda builder: builder._check_header("rpc/rpc.h")), +] if __name__ == '__main__': - dist.setup( + setup( twisted_subproject="runner", # metadata name="Twisted Runner", - description="Twisted Runner is a process management library and inetd replacement.", + description="Twisted Runner is a process management library and inetd " + "replacement.", author="Twisted Matrix Laboratories", author_email="twisted-python@twistedmatrix.com", maintainer="Andrew Bennetts", - maintainer_email="spiv@twistedmatrix.com", url="http://twistedmatrix.com/trac/wiki/TwistedRunner", license="MIT", long_description="""\ @@ -35,5 +31,5 @@ with Python and Twisted, and has an almost full replacement for inetd. """, # build stuff - detectExtensions=detectExtensions, + conditionalExtensions=extensions, ) diff -Nru twisted-runner-0.2.0/twisted/plugins/twisted_runner.py twisted-runner-12.0.0/twisted/plugins/twisted_runner.py --- twisted-runner-0.2.0/twisted/plugins/twisted_runner.py 1970-01-01 00:00:00.000000000 +0000 +++ twisted-runner-12.0.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-0.2.0/twisted/runner/inetdconf.py twisted-runner-12.0.0/twisted/runner/inetdconf.py --- twisted-runner-0.2.0/twisted/runner/inetdconf.py 2004-08-25 08:36:30.000000000 +0000 +++ twisted-runner-12.0.0/twisted/runner/inetdconf.py 2011-02-14 04:45:15.000000000 +0000 @@ -1,12 +1,11 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# Copyright (c) Twisted Matrix Laboratories. # See LICENSE for details. # -"""Parser for inetd.conf files - -Stability: stable +""" +Parser for inetd.conf files -Maintainer: U{Andrew Bennetts} +Maintainer: Andrew Bennetts Future Plans: xinetd configuration file support? """ diff -Nru twisted-runner-0.2.0/twisted/runner/inetd.py twisted-runner-12.0.0/twisted/runner/inetd.py --- twisted-runner-0.2.0/twisted/runner/inetd.py 2005-08-21 13:31:20.000000000 +0000 +++ twisted-runner-12.0.0/twisted/runner/inetd.py 2011-02-14 04:45:15.000000000 +0000 @@ -1,13 +1,12 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# Copyright (c) Twisted Matrix Laboratories. # See LICENSE for details. # -"""Twisted inetd. - -Stability: semi-stable +""" +Twisted inetd. -Maintainer: U{Andrew Bennetts} +Maintainer: Andrew Bennetts Future Plans: Bugfixes. Specifically for UDP and Sun-RPC, which don't work correctly yet. diff -Nru twisted-runner-0.2.0/twisted/runner/inetdtap.py twisted-runner-12.0.0/twisted/runner/inetdtap.py --- twisted-runner-0.2.0/twisted/runner/inetdtap.py 2005-11-10 03:18:15.000000000 +0000 +++ twisted-runner-12.0.0/twisted/runner/inetdtap.py 2011-10-18 10:31:11.000000000 +0000 @@ -1,13 +1,12 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# Copyright (c) Twisted Matrix Laboratories. # See LICENSE for details. # -"""Twisted inetd TAP support - -Stability: semi-stable +""" +Twisted inetd TAP support -Maintainer: U{Andrew Bennetts} +Maintainer: Andrew Bennetts Future Plans: more configurability. """ @@ -38,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-0.2.0/twisted/runner/__init__.py twisted-runner-12.0.0/twisted/runner/__init__.py --- twisted-runner-0.2.0/twisted/runner/__init__.py 2006-05-25 01:08:06.000000000 +0000 +++ twisted-runner-12.0.0/twisted/runner/__init__.py 2008-07-29 20:13:54.000000000 +0000 @@ -1,8 +1,7 @@ -"""Twisted runer: run and monitor processes - -Stability: semi-stable +""" +Twisted runer: run and monitor processes -Maintainer: U{Andrew Bennetts} +Maintainer: Andrew Bennetts classic inetd(8) support: Future Plans: The basic design should be final. There are some bugs that need diff -Nru twisted-runner-0.2.0/twisted/runner/procmon.py twisted-runner-12.0.0/twisted/runner/procmon.py --- twisted-runner-0.2.0/twisted/runner/procmon.py 2004-08-25 08:36:30.000000000 +0000 +++ twisted-runner-12.0.0/twisted/runner/procmon.py 2011-02-14 04:45:15.000000000 +0000 @@ -1,57 +1,14 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# -*- test-case-name: twisted.runner.test.test_procmon -*- +# Copyright (c) Twisted Matrix Laboratories. # See LICENSE for details. -# """ -ProcessMonitor: run processes, monitor progress, and restart 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 "addProcess" and "removeProcess". When the -service is active (that is, when the application it is attached to -is running), adding a process automatically starts it. - -Each process has a name (a string). This string must uniquely identify -the process. In particular, attempting to add two processes with the -same name will result in a key error. - -The arguments to addProcess are: - - name -- a string, uniquely specifying the process - - args -- a list of arguments. the first will be used to determine the - executable - - optionally, the uid and gid this process should be run as (by default, - it does not change uid/gid before running processes). - -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 -.addProcess("name", ['/bin/sh', '-c', shell_script]) - -removeProcess takes just the name argument. If the process is started, it -kills it, and will never restart it. - -The "restartAll" method restarts all processes. This is useful for 3rd -parties management services to allow a user to restart servers because -of an outside circumstances change -- for example, a new version of a library -which is installed. - -The following attributes on the monitor can be set to configure behaviour - - threshold -- how long a process has to live before the death is considered - instant (default 1, measured in seconds) - - killTime -- how long a process being killed has to get its affairs in - order before it gets killed with an unmaskable signal - (default 5, measured in seconds) - - consistencyDelay -- time between consistency checks - (default 60, measured in seconds) +Support for starting, monitoring, and restarting child process. """ +import warnings -import os, time -from signal import SIGTERM, SIGKILL from twisted.python import log -from twisted.internet import protocol, reactor, process +from twisted.internet import error, protocol, reactor as _reactor from twisted.application import service from twisted.protocols import basic @@ -59,7 +16,7 @@ disconnecting = 0 -transport = DummyTransport() +transport = DummyTransport() class LineLogger(basic.LineReceiver): @@ -69,6 +26,7 @@ def lineReceived(self, line): log.msg('[%s] %s' % (self.tag, line)) + class LoggingProtocol(protocol.ProcessProtocol): service = None @@ -80,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') @@ -93,114 +53,270 @@ 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. + """ 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 _getActive(self): + warnings.warn("active is deprecated since Twisted 10.1.0. " + "Use running instead.", category=DeprecationWarning, + stacklevel=2) + return self.running + + active = property(_getActive, None) + + + def _getConsistency(self): + warnings.warn("consistency is deprecated since Twisted 10.1.0.", + category=DeprecationWarning, stacklevel=2) + return None + + consistency = property(_getConsistency, None) + + + def _getConsistencyDelay(self): + warnings.warn("consistencyDelay is deprecated since Twisted 10.1.0.", + category=DeprecationWarning, stacklevel=2) + return 60 + + consistencyDelay = property(_getConsistencyDelay, None) + 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, process.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): - if self.processes.has_key(name): - raise KeyError("remove %s first" % name) - self.processes[name] = args, uid, gid - if self.active: + + def addProcess(self, name, args, uid=None, gid=None, env={}): + """ + 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 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. + @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} + @raises: C{KeyError} if a process with the given name already + exists + """ + if name in self.processes: + raise KeyError("remove %s first" % (name,)) + self.processes[name] = args, uid, gid, env + self.delay[name] = self.minRestartDelay + if self.running: self.startProcess(name) + def removeProcess(self, name): - del self.processes[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) - 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(): - 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-0.2.0/twisted/runner/procmontap.py twisted-runner-12.0.0/twisted/runner/procmontap.py --- twisted-runner-0.2.0/twisted/runner/procmontap.py 1970-01-01 00:00:00.000000000 +0000 +++ twisted-runner-12.0.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-0.2.0/twisted/runner/procutils.py twisted-runner-12.0.0/twisted/runner/procutils.py --- twisted-runner-0.2.0/twisted/runner/procutils.py 2004-07-29 09:11:56.000000000 +0000 +++ twisted-runner-12.0.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-0.2.0/twisted/runner/test/__init__.py twisted-runner-12.0.0/twisted/runner/test/__init__.py --- twisted-runner-0.2.0/twisted/runner/test/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ twisted-runner-12.0.0/twisted/runner/test/__init__.py 2011-02-14 04:45:15.000000000 +0000 @@ -0,0 +1,6 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Test package for Twisted Runner. +""" diff -Nru twisted-runner-0.2.0/twisted/runner/test/test_procmon.py twisted-runner-12.0.0/twisted/runner/test/test_procmon.py --- twisted-runner-0.2.0/twisted/runner/test/test_procmon.py 1970-01-01 00:00:00.000000000 +0000 +++ twisted-runner-12.0.0/twisted/runner/test/test_procmon.py 2011-07-14 19:05:14.000000000 +0000 @@ -0,0 +1,528 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Tests for L{twisted.runner.procmon}. +""" + +from twisted.trial import unittest +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} 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 + 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_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, {}) + + + def test_activeAttributeEqualsRunning(self): + """ + L{ProcessMonitor.active} unneccessarily duplicates the standard + L{IService.running} flag. + """ + self.assertEqual(self.pm.active, self.pm.running) + self.pm.startService() + self.assertEqual(self.pm.active, self.pm.running) + + + def test_activeAttributeDeprecation(self): + """ + L{ProcessMonitor.active} unneccessarily duplicates the standard + L{IService.running} flag and is therefore deprecated. + """ + def getActive(): + return self.pm.active + expectedMessage = "active is deprecated since Twisted 10.1.0. Use " \ + "running instead." + + self.assertWarns(DeprecationWarning, + expectedMessage, __file__, getActive) + + + def test_consistencyAttributeDeprecation(self): + """ + L{ProcessMonitor.consistency} is no longer needed since the removal of + the ProcessMonitor._checkConsistency function and is therefore + deprecated. + """ + def getConsistency(): + return self.pm.consistency + expectedMessage = "consistency is deprecated since Twisted 10.1.0." + + self.assertWarns(DeprecationWarning, + expectedMessage, __file__, getConsistency) + + + def test_consistencyDelayAttributeDeprecation(self): + """ + L{ProcessMonitor.consistencyDelay} is no longer needed since the + removal of the ProcessMonitor._checkConsistency function and is + therefore deprecated. + """ + def getConsistencyDelay(): + return self.pm.consistencyDelay + expectedMessage = "consistencyDelay is deprecated since Twisted 10.1.0." + + self.assertWarns(DeprecationWarning, + expectedMessage, __file__, getConsistencyDelay) diff -Nru twisted-runner-0.2.0/twisted/runner/test/test_procmontap.py twisted-runner-12.0.0/twisted/runner/test/test_procmontap.py --- twisted-runner-0.2.0/twisted/runner/test/test_procmontap.py 1970-01-01 00:00:00.000000000 +0000 +++ twisted-runner-12.0.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-0.2.0/twisted/runner/_version.py twisted-runner-12.0.0/twisted/runner/_version.py --- twisted-runner-0.2.0/twisted/runner/_version.py 2006-05-24 14:56:34.000000000 +0000 +++ twisted-runner-12.0.0/twisted/runner/_version.py 2012-02-10 14:56:39.000000000 +0000 @@ -1,3 +1,3 @@ -# This is an auto-generated file. Use admin/change-versions to update. +# This is an auto-generated file. Do not edit it. from twisted.python import versions -version = versions.Version(__name__[:__name__.rfind('.')], 0, 2, 0) +version = versions.Version('twisted.runner', 12, 0, 0)