[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