[Pkg-privacy-commits] [pyptlib] 71/136: add tests for subproc, and a few more minor features: - SINK to write to /dev/null - make the behaviour of trap_sigint play more nicely with signal.signal() - reap zombies after proc.kill()
Ximin Luo
infinity0 at moszumanska.debian.org
Sat Aug 22 13:25:10 UTC 2015
This is an automated email from the git hooks/post-receive script.
infinity0 pushed a commit to branch master
in repository pyptlib.
commit 02d3c6c4b6cd83a41793c508f131af3b79f94a72
Author: Ximin Luo <infinity0 at gmx.com>
Date: Tue Aug 6 16:03:48 2013 +0100
add tests for subproc, and a few more minor features:
- SINK to write to /dev/null
- make the behaviour of trap_sigint play more nicely with signal.signal()
- reap zombies after proc.kill()
---
pyptlib/test/test_util_subproc.py | 115 +++++++++++++++++++++++++++++++++++++
pyptlib/test/util_subproc_child.py | 22 +++++++
pyptlib/test/util_subproc_main.py | 72 +++++++++++++++++++++++
pyptlib/util/subproc.py | 28 ++++++++-
4 files changed, 234 insertions(+), 3 deletions(-)
diff --git a/pyptlib/test/test_util_subproc.py b/pyptlib/test/test_util_subproc.py
new file mode 100644
index 0000000..deb4d82
--- /dev/null
+++ b/pyptlib/test/test_util_subproc.py
@@ -0,0 +1,115 @@
+import unittest
+
+import signal
+import subprocess
+import time
+
+from pyptlib.util.subproc import auto_killall, create_sink, Popen, SINK
+from subprocess import PIPE
+
+# We ought to run auto_killall(), instead of manually calling proc.terminate()
+# but it's not very good form to use something inside the test for itself. :p
+
+def get_child_pids(pid):
+ # TODO(infinity0): add windows version
+ return subprocess.check_output(("ps h --ppid %s -o pid" % pid).split()).split()
+
+def proc_wait(proc, wait_s):
+ time.sleep(wait_s)
+ proc.poll() # otherwise it doesn't exit properly
+
+def proc_is_alive(pid):
+ r = subprocess.call(("ps -p %s" % pid).split(), stdout=create_sink())
+ return True if r == 0 else False
+
+
+class SubprocTest(unittest.TestCase):
+
+ def name(self):
+ return self.id().split(".")[-1].replace("test_", "")
+
+ def getMainArgs(self):
+ return ["python", "./util_subproc_main.py", self.name()]
+
+ def spawnMain(self, cmd=None, *args, **kwargs):
+ # spawn the main test process and wait a bit for it to initialise
+ proc = Popen(cmd or self.getMainArgs(), *args, **kwargs)
+ time.sleep(0.2)
+ return proc
+
+ def getOnlyChild(self, proc):
+ children = get_child_pids(proc.pid)
+ self.assertTrue(len(children) == 1)
+ return children[0]
+
+ def test_Popen_IOpassthru(self):
+ output = subprocess.check_output(self.getMainArgs())
+ self.assertTrue(len(output) > 0)
+
+ def test_Popen_SINK(self):
+ output = subprocess.check_output(self.getMainArgs())
+ self.assertTrue(len(output) == 0)
+
+ def test_trap_sigint_multiple(self):
+ proc = self.spawnMain(stdout=PIPE)
+ proc.send_signal(signal.SIGINT)
+ self.assertEquals("run h1\n", proc.stdout.readline())
+ proc.send_signal(signal.SIGINT)
+ self.assertEquals("run h2\n", proc.stdout.readline())
+ self.assertEquals("run h1\n", proc.stdout.readline())
+ proc.terminate()
+
+ def test_trap_sigint_reset(self):
+ proc = self.spawnMain(stdout=PIPE)
+ proc.send_signal(signal.SIGINT)
+ self.assertEquals("run h2\n", proc.stdout.readline())
+ proc.terminate()
+
+ def test_killall_kill(self):
+ proc = self.spawnMain()
+ pid = proc.pid
+ cid = self.getOnlyChild(proc)
+ self.assertTrue(proc_is_alive(cid), "child did not hang")
+ time.sleep(2)
+ self.assertTrue(proc_is_alive(cid), "child did not ignore TERM")
+ time.sleep(4)
+ self.assertFalse(proc_is_alive(cid), "child was not killed by parent")
+ proc.terminate()
+
+ def test_auto_killall_2_int(self):
+ proc = self.spawnMain()
+ pid = proc.pid
+ cid = self.getOnlyChild(proc)
+ # test first signal is ignored
+ proc.send_signal(signal.SIGINT)
+ proc_wait(proc, 3)
+ self.assertTrue(proc_is_alive(pid), "1 INT not ignored")
+ self.assertTrue(proc_is_alive(cid), "1 INT not ignored")
+ # test second signal is handled
+ proc.send_signal(signal.SIGINT)
+ proc_wait(proc, 3)
+ self.assertFalse(proc_is_alive(pid), "2 INT not handled")
+ self.assertFalse(proc_is_alive(cid), "2 INT not handled")
+
+ def test_auto_killall_term(self):
+ proc = self.spawnMain()
+ pid = proc.pid
+ cid = self.getOnlyChild(proc)
+ # test TERM is handled
+ proc.send_signal(signal.SIGTERM)
+ proc_wait(proc, 3)
+ self.assertFalse(proc_is_alive(pid), "TERM not handled")
+ self.assertFalse(proc_is_alive(cid), "TERM not handled")
+
+ def test_auto_killall_exit(self):
+ proc = self.spawnMain()
+ pid = proc.pid
+ cid = self.getOnlyChild(proc)
+ # test exit is handled. main exits by itself after 1 seconds
+ # exit handler takes ~2s to run, usually
+ proc_wait(proc, 3)
+ self.assertFalse(proc_is_alive(pid), "unexpectedly did not exit")
+ self.assertFalse(proc_is_alive(cid), "parent did not kill child")
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/pyptlib/test/util_subproc_child.py b/pyptlib/test/util_subproc_child.py
new file mode 100644
index 0000000..62b7cd9
--- /dev/null
+++ b/pyptlib/test/util_subproc_child.py
@@ -0,0 +1,22 @@
+#!/usr/bin/python
+
+import signal
+import sys
+import time
+
+def hangForever(signum=0, sframe=None):
+ time.sleep(1000)
+
+def child_default(subcmd, *argv):
+ time.sleep(100)
+
+def child_default_io(subcmd, *argv):
+ print "child printing output"
+
+def child_killall_kill(subcmd, *argv):
+ signal.signal(signal.SIGINT, hangForever)
+ signal.signal(signal.SIGTERM, hangForever)
+ child_default(None)
+
+if __name__ == '__main__':
+ getattr(sys.modules[__name__], "child_%s" % sys.argv[1])(*sys.argv[1:])
diff --git a/pyptlib/test/util_subproc_main.py b/pyptlib/test/util_subproc_main.py
new file mode 100644
index 0000000..d79500a
--- /dev/null
+++ b/pyptlib/test/util_subproc_main.py
@@ -0,0 +1,72 @@
+#!/usr/bin/python
+
+import os
+import signal
+import sys
+import time
+
+from pyptlib.util.subproc import auto_killall, killall, trap_sigint, Popen, SINK
+from subprocess import PIPE
+
+
+def startChild(subcmd, stdout=SINK, **kwargs):
+ return Popen(
+ ["python", "util_subproc_child.py", subcmd],
+ stdout = stdout,
+ **kwargs
+ )
+
+def sleepIgnoreInts(ignoreNumInts=3):
+ for i in xrange(ignoreNumInts):
+ time.sleep(100)
+
+def handler1(signum=0, sframe=None):
+ print "run h1"
+ sys.stdout.flush()
+
+def handler2(signum=0, sframe=None):
+ print "run h2"
+ sys.stdout.flush()
+
+def main_Popen_IOpassthru(testname, *argv):
+ child = startChild("default_io", stdout=None)
+ child.wait()
+
+def main_Popen_SINK(testname, *argv):
+ child = startChild("default_io")
+ child.wait()
+
+def main_trap_sigint_multiple(testname, *argv):
+ trap_sigint(handler1)
+ trap_sigint(handler2, 1)
+ sleepIgnoreInts(2)
+
+def main_trap_sigint_reset(testname, *argv):
+ trap_sigint(handler1)
+ signal.signal(signal.SIGINT, lambda signum, sframe: None)
+ trap_sigint(handler2)
+ sleepIgnoreInts(1)
+
+def main_killall_kill(testname, *argv):
+ child = startChild(testname)
+ time.sleep(1)
+ killall(4)
+ time.sleep(100)
+
+def main_auto_killall_2_int(testname, *argv):
+ auto_killall(1)
+ child = startChild("default")
+ child.wait()
+
+def main_auto_killall_term(testname, *argv):
+ auto_killall()
+ child = startChild("default")
+ child.wait()
+
+def main_auto_killall_exit(testname, *argv):
+ auto_killall()
+ child = startChild("default")
+ time.sleep(1)
+
+if __name__ == "__main__":
+ getattr(sys.modules[__name__], "main_%s" % sys.argv[1])(*sys.argv[1:])
diff --git a/pyptlib/util/subproc.py b/pyptlib/util/subproc.py
index 4400f41..add34f3 100644
--- a/pyptlib/util/subproc.py
+++ b/pyptlib/util/subproc.py
@@ -15,16 +15,25 @@ _CHILD_PROCS = []
# offer different response strategies for them (e.g. restart the child? or die
# and kill the other children too).
+SINK = object()
+
a = inspect.getargspec(subprocess.Popen.__init__)
_Popen_defaults = zip(a.args[-len(a.defaults):],a.defaults); del a
class Popen(subprocess.Popen):
"""Wrapper for subprocess.Popen that tracks every child process.
See the subprocess module for documentation.
+
+ Additionally, you may use subproc.SINK as the value for either of the
+ stdout, stderr arguments to tell subprocess to discard anything written
+ to those channels.
"""
def __init__(self, *args, **kwargs):
kwargs = dict(_Popen_defaults + kwargs.items())
+ for f in ['stdout', 'stderr']:
+ if kwargs[f] is SINK:
+ kwargs[f] = create_sink()
# super() does some magic that makes **kwargs not work, so just call
# our super-constructor directly
subprocess.Popen.__init__(self, *args, **kwargs)
@@ -34,15 +43,26 @@ class Popen(subprocess.Popen):
# that don't buffer readlines() et. al. Currently one must avoid these and
# use while/readline(); see man page for "python -u" for more details.
+def create_sink():
+ # TODO(infinity0): do a windows version of this
+ return open("/dev/null", "w", 0)
+
_SIGINT_RUN = {}
def trap_sigint(handler, ignoreNum=0):
"""Register a handler for an INT signal.
+ Successive traps registered via this function are cumulative, and override
+ any previous handlers registered using signal.signal(). To reset these
+ cumulative traps, call signal.signal() with another (maybe dummy) handler.
+
Args:
handler: a signal handler; see signal.signal() for details
ignoreNum: number of signals to ignore before activating the handler,
which will be run on all subsequent signals.
"""
+ prev_handler = signal.signal(signal.SIGINT, _run_sigint_handlers)
+ if prev_handler != _run_sigint_handlers:
+ _SIGINT_RUN.clear()
_SIGINT_RUN.setdefault(ignoreNum, []).append(handler)
_intsReceived = 0
@@ -60,15 +80,13 @@ def _run_sigint_handlers(signum=0, sframe=None):
exc_info = sys.exc_info()
except:
import traceback
- print >> sys.stderr, "Error in atexit._run_exitfuncs:"
+ print >> sys.stderr, "Error in subproc._run_sigint_handlers:"
traceback.print_exc()
exc_info = sys.exc_info()
if exc_info is not None:
raise exc_info[0], exc_info[1], exc_info[2]
-signal.signal(signal.SIGINT, _run_sigint_handlers)
-
_isTerminating = False
def killall(wait_s=16):
"""Attempt to gracefully terminate all child processes.
@@ -95,6 +113,10 @@ def killall(wait_s=16):
for proc in _CHILD_PROCS:
if proc.poll() is None:
proc.kill()
+ time.sleep(0.5)
+ # reap any zombies
+ for proc in _CHILD_PROCS:
+ proc.poll()
def auto_killall(ignoreNumSigInts=0):
"""Automatically terminate all child processes on exit.
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-privacy/packages/pyptlib.git
More information about the Pkg-privacy-commits
mailing list