Repository: giampaolo/psutil
Pull request #1747 (2020-05-02 22:17:03)

Process wait() improvements
* `Process.wait()` on POSIX now returns an `enum` showing the negative which was used to terminate the process:
```python
>>> import psutil
>>> p = psutil.Process(9891)
>>> p.terminate()
>>> p.wait()
<Negsignal.SIGTERM: -15>
```

* the return value is cached so that the exit code can be retrieved on then next call, mimicking `subprocess.Popen.wait()`
* `Process` object provides more `status` and `exitcode` additional info on `str()` and `repr()`:

```
 >>> proc
 psutil.Process(pid=12739, name='python3', status='terminated', exitcode=<Negsigs.SIGTERM: -15>, started='15:08:20')
```

Extra:
* improved `wait()` doc
* reverted #1736: `psutil.Popen` uses original `subprocess.Popen.wait` method (safer)


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index 6f8087db..0962be3e 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -10,7 +10,24 @@ XXXX-XX-XX
 - 1729_: parallel tests on UNIX (make test-parallel). They're twice as fast!
 - 1741_: "make build/install" is now run in parallel and it's about 15% faster
   on UNIX.
-
+- 1747_: `Process.wait()` on POSIX returns an enum, showing the negative signal
+  which was used to terminate the process.
+  ```
+  >>> import psutil
+  >>> p = psutil.Process(9891)
+  >>> p.terminate()
+  >>> p.wait()
+  <Negsignal.SIGTERM: -15>
+  ```
+- 1747_: `Process.wait()` return value is cached so that the exit code can be
+  retrieved on then next call.
+- 1747_: Process provides more info about the process on str() and repr()
+  (status and exit code).
+  ```
+  >>> proc
+  psutil.Process(pid=12739, name='python3', status='terminated',
+                 exitcode=<Negsigs.SIGTERM: -15>, started='15:08:20')
+  ```
 **Bug fixes**
 
 - 1726_: [Linux] cpu_freq() parsing should use spaces instead of tabs on ia64.
diff --git a/README.rst b/README.rst
index 62ab3f23..2137a1a2 100644
--- a/README.rst
+++ b/README.rst
@@ -438,7 +438,7 @@ Process management
     >>> p.terminate()
     >>> p.kill()
     >>> p.wait(timeout=3)
-    0
+    <Exitcode.EX_OK: 0>
     >>>
     >>> psutil.test()
     USER         PID %CPU %MEM     VSZ     RSS TTY        START    TIME  COMMAND
diff --git a/docs/Makefile b/docs/Makefile
index cca5435f..860a2b0e 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -15,8 +15,9 @@ ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
 # the i18n builder cannot share the environment and doctrees with the others
 I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
 
-DEPS = sphinx
-
+DEPS = \
+	sphinx \
+	sphinx_rtd_theme
 
 .PHONY: help
 help:
diff --git a/docs/index.rst b/docs/index.rst
index 133e69fe..699ea1f1 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1943,34 +1943,51 @@ Process class
 
   .. method:: wait(timeout=None)
 
-    Wait for process termination and if the process is a child of the current
-    one also return the exit code, else ``None``. On Windows there's
-    no such limitation (exit code is always returned). If the process is
-    already terminated immediately return ``None`` instead of raising
-    :class:`NoSuchProcess`.
+    Wait for a process PID to terminate. The details about the return value
+    differ on UNIX and Windows.
+
+    *On UNIX*: if the process terminated normally, the return value is a
+    positive integer >= 0 indicating the exit code.
+    If the process was terminated by a signal return the negated value of the
+    signal which caused the termination (e.g. ``-SIGTERM``).
+    If PID is not a children of `os.getpid`_ (current process) just wait until
+    the process disappears and return ``None``.
+    If PID does not exist return ``None`` immediately.
+
+    *On Windows*: always return the exit code, which is a positive integer as
+    returned by `GetExitCodeProcess`_.
+
     *timeout* is expressed in seconds. If specified and the process is still
     alive raise :class:`TimeoutExpired` exception.
     ``timeout=0`` can be used in non-blocking apps: it will either return
     immediately or raise :class:`TimeoutExpired`.
+
+    The return value is cached.
     To wait for multiple processes use :func:`psutil.wait_procs()`.
 
     >>> import psutil
     >>> p = psutil.Process(9891)
     >>> p.terminate()
     >>> p.wait()
+    <Negsignal.SIGTERM: -15>
+
+    .. versionchanged:: 5.7.1 return value is cached (instead of returning
+      ``None``).
+
+    .. versionchanged:: 5.7.1 on POSIX, in case of negative signal, return it
+      as a human readable `enum`_.
 
 .. class:: Popen(*args, **kwargs)
 
-  Starts a sub-process via `subprocess.Popen`_, and in addition it provides
-  all the methods of :class:`psutil.Process` in a single class.
-  For method names common to both classes such as
+  Same as `subprocess.Popen`_ but in addition it provides all
+  :class:`psutil.Process` methods in a single class.
+  For the following methods which are common to both classes, psutil
+  implementation takes precedence:
   :meth:`send_signal() <psutil.Process.send_signal()>`,
   :meth:`terminate() <psutil.Process.terminate()>`,
-  :meth:`kill() <psutil.Process.kill()>` and
-  :meth:`wait() <psutil.Process.wait()>`
-  :class:`psutil.Process` implementation takes precedence.
-  This may have some advantages, like making sure PID has not been reused,
-  fixing `BPO-6973`_.
+  :meth:`kill() <psutil.Process.kill()>`.
+  This is done in order to avoid killing another process in case its PID has
+  been reused, fixing  `BPO-6973`_.
 
   >>> import psutil
   >>> from subprocess import PIPE
@@ -1988,9 +2005,6 @@ Process class
 
   .. versionchanged:: 4.4.0 added context manager support
 
-  .. versionchanged:: 5.7.1 wait() invokes :meth:`wait() <psutil.Process.wait()>`
-    instead of `subprocess.Popen.wait`_.
-
 Windows services
 ================
 
@@ -2818,11 +2832,12 @@ Timeline
 .. _`development guide`: https://github.com/giampaolo/psutil/blob/master/docs/DEVGUIDE.rst
 .. _`disk_usage.py`: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py
 .. _`donation`: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A9ZS7PKKRM3S8
-.. _`enums`: https://docs.python.org/3/library/enum.html#module-enum
+.. _`enum`: https://docs.python.org/3/library/enum.html#module-enum
 .. _`fans.py`: https://github.com/giampaolo/psutil/blob/master/scripts/fans.py
 .. _`GetDriveType`: https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-getdrivetypea
 .. _`getfsstat`: http://www.manpagez.com/man/2/getfsstat/
 .. _`GetPriorityClass`: https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-getpriorityclass
+.. _`GetExitCodeProcess`: https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess
 .. _`Giampaolo Rodola`: http://grodola.blogspot.com/p/about.html
 .. _`hash`: https://docs.python.org/3/library/functions.html#hash
 .. _`ifconfig.py`: https://github.com/giampaolo/psutil/blob/master/scripts/ifconfig.py
diff --git a/psutil/__init__.py b/psutil/__init__.py
index cabf0e0e..7fdbc0fc 100644
--- a/psutil/__init__.py
+++ b/psutil/__init__.py
@@ -232,6 +232,7 @@ version_info = tuple([int(num) for num in __version__.split('.')])
 _timer = getattr(time, 'monotonic', time.time)
 _TOTAL_PHYMEM = None
 _LOWEST_PID = None
+_SENTINEL = object()
 
 # Sanity check in case the user messed up with psutil installation
 # or did something weird with sys.path. In this case we might end
@@ -364,6 +365,7 @@ class Process(object):
         self._proc = _psplatform.Process(pid)
         self._last_sys_cpu_times = None
         self._last_proc_cpu_times = None
+        self._exitcode = _SENTINEL
         # cache creation time for later use in is_running() method
         try:
             self.create_time()
@@ -394,18 +396,22 @@ class Process(object):
         except AttributeError:
             info = {}  # Python 2.6
         info["pid"] = self.pid
+        if self._name:
+            info['name'] = self._name
         with self.oneshot():
             try:
                 info["name"] = self.name()
                 info["status"] = self.status()
-                if self._create_time:
-                    info['started'] = _pprint_secs(self._create_time)
             except ZombieProcess:
                 info["status"] = "zombie"
             except NoSuchProcess:
                 info["status"] = "terminated"
             except AccessDenied:
                 pass
+            if self._exitcode not in (_SENTINEL, None):
+                info["exitcode"] = self._exitcode
+            if self._create_time:
+                info['started'] = _pprint_secs(self._create_time)
             return "%s.%s(%s)" % (
                 self.__class__.__module__,
                 self.__class__.__name__,
@@ -1270,7 +1276,10 @@ class Process(object):
         """
         if timeout is not None and not timeout >= 0:
             raise ValueError("timeout must be a positive integer")
-        return self._proc.wait(timeout)
+        if self._exitcode is not _SENTINEL:
+            return self._exitcode
+        self._exitcode = self._proc.wait(timeout)
+        return self._exitcode
 
 
 # The valid attr names which can be processed by Process.as_dict().
@@ -1287,11 +1296,18 @@ _as_dict_attrnames = set(
 
 
 class Popen(Process):
-    """A more convenient interface to stdlib subprocess.Popen class.
-    It starts a sub process and deals with it exactly as when using
-    subprocess.Popen class but in addition also provides all the
-    properties and methods of psutil.Process class as a unified
-    interface:
+    """Same as subprocess.Popen, but in addition it provides all
+    psutil.Process methods in a single class.
+    For the following methods which are common to both classes, psutil
+    implementation takes precedence:
+
+    * send_signal()
+    * terminate()
+    * kill()
+
+    This is done in order to avoid killing another process in case its
+    PID has been reused, fixing BPO-6973.
+
       >>> import psutil
       >>> from subprocess import PIPE
       >>> p = psutil.Popen(["python", "-c", "print 'hi'"], stdout=PIPE)
@@ -1307,14 +1323,6 @@ class Popen(Process):
       >>> p.wait(timeout=2)
       0
       >>>
-    For method names common to both classes such as kill(), terminate()
-    and wait(), psutil.Process implementation takes precedence.
-    Unlike subprocess.Popen this class pre-emptively checks whether PID
-    has been reused on send_signal(), terminate() and kill() so that
-    you don't accidentally terminate another process, fixing
-    http://bugs.python.org/issue6973.
-    For a complete documentation refer to:
-    http://docs.python.org/3/library/subprocess.html
     """
 
     def __init__(self, *args, **kwargs):
@@ -1361,11 +1369,7 @@ class Popen(Process):
     def wait(self, timeout=None):
         if self.__subproc.returncode is not None:
             return self.__subproc.returncode
-        # Note: using psutil's wait() on UNIX should make no difference.
-        # On Windows it does, because PID can still be alive (see
-        # _pswindows.py counterpart addressing this). Python 2.7 doesn't
-        # have timeout arg, so this acts as a backport.
-        ret = Process.wait(self, timeout)
+        ret = super(Popen, self).wait(timeout)
         self.__subproc.returncode = ret
         return ret
 
diff --git a/psutil/_psposix.py b/psutil/_psposix.py
index 88213ef8..2e6711a3 100644
--- a/psutil/_psposix.py
+++ b/psutil/_psposix.py
@@ -6,6 +6,7 @@
 
 import glob
 import os
+import signal
 import sys
 import time
 
@@ -21,6 +22,11 @@ from ._compat import ProcessLookupError
 from ._compat import PY3
 from ._compat import unicode
 
+if sys.version_info >= (3, 4):
+    import enum
+else:
+    enum = None
+
 
 __all__ = ['pid_exists', 'wait_pid', 'disk_usage', 'get_terminal_map']
 
@@ -47,66 +53,108 @@ def pid_exists(pid):
         return True
 
 
-def wait_pid(pid, timeout=None, proc_name=None):
-    """Wait for process with pid 'pid' to terminate and return its
-    exit status code as an integer.
+# Python 3.5 signals enum (contributed by me ^^):
+# https://bugs.python.org/issue21076
+if enum is not None and hasattr(signal, "Signals"):
+    Negsignal = enum.IntEnum(
+        'Negsignal', dict([(x.name, -x.value) for x in signal.Signals]))
+
+    def negsig_to_enum(num):
+        """Convert a negative signal value to an enum."""
+        try:
+            return Negsignal(num)
+        except ValueError:
+            return num
+else:
+    def negsig_to_enum(num):
+        return num
+
+
+def wait_pid(pid, timeout=None, proc_name=None,
+             _waitpid=os.waitpid,
+             _timer=getattr(time, 'monotonic', time.time),
+             _min=min,
+             _sleep=time.sleep,
+             _pid_exists=pid_exists):
+    """Wait for a process PID to terminate.
+
+    If the process terminated normally by calling exit(3) or _exit(2),
+    or by returning from main(), the return value is the positive integer
+    passed to *exit().
 
-    If pid is not a children of os.getpid() (current process) just
-    waits until the process disappears and return None.
+    If it was terminated by a signal it returns the negated value of the
+    signal which caused the termination (e.g. -SIGTERM).
 
-    If pid does not exist at all return None immediately.
+    If PID is not a children of os.getpid() (current process) just
+    wait until the process disappears and return None.
 
-    Raise TimeoutExpired on timeout expired.
+    If PID does not exist at all return None immediately.
+
+    If *timeout* != None and process is still alive raise TimeoutExpired.
+    timeout=0 is also possible (either return immediately or raise).
     """
-    def check_timeout(delay):
+    if pid <= 0:
+        raise ValueError("can't wait for PID 0")  # see "man waitpid"
+    interval = 0.0001
+    flags = 0
+    if timeout is not None:
+        flags |= os.WNOHANG
+        stop_at = _timer() + timeout
+
+    def sleep(interval):
+        # Sleep for some time and return a new increased interval.
         if timeout is not None:
-            if timer() >= stop_at:
+            if _timer() >= stop_at:
                 raise TimeoutExpired(timeout, pid=pid, name=proc_name)
-        time.sleep(delay)
-        return min(delay * 2, 0.04)
-
-    timer = getattr(time, 'monotonic', time.time)
-    if timeout is not None:
-        def waitcall():
-            return os.waitpid(pid, os.WNOHANG)
-        stop_at = timer() + timeout
-    else:
-        def waitcall():
-            return os.waitpid(pid, 0)
+        _sleep(interval)
+        return _min(interval * 2, 0.04)
 
-    delay = 0.0001
+    # See: https://linux.die.net/man/2/waitpid
     while True:
         try:
-            retpid, status = waitcall()
+            retpid, status = os.waitpid(pid, flags)
         except InterruptedError:
-            delay = check_timeout(delay)
+            interval = sleep(interval)
         except ChildProcessError:
             # This has two meanings:
-            # - pid is not a child of os.getpid() in which case
+            # - PID is not a child of os.getpid() in which case
             #   we keep polling until it's gone
-            # - pid never existed in the first place
+            # - PID never existed in the first place
             # In both cases we'll eventually return None as we
             # can't determine its exit status code.
-            while True:
-                if pid_exists(pid):
-                    delay = check_timeout(delay)
-                else:
-                    return
+            while _pid_exists(pid):
+                interval = sleep(interval)
+            return
         else:
             if retpid == 0:
-                # WNOHANG was used, pid is still running
-                delay = check_timeout(delay)
+                # WNOHANG flag was used and PID is still running.
+                interval = sleep(interval)
                 continue
-            # process exited due to a signal; return the integer of
-            # that signal
-            if os.WIFSIGNALED(status):
-                return -os.WTERMSIG(status)
-            # process exited using exit(2) system call; return the
-            # integer exit(2) system call has been called with
             elif os.WIFEXITED(status):
+                # Process terminated normally by calling exit(3) or _exit(2),
+                # or by returning from main(). The return value is the
+                # positive integer passed to *exit().
                 return os.WEXITSTATUS(status)
+            elif os.WIFSIGNALED(status):
+                # Process exited due to a signal. Return the negative value
+                # of that signal.
+                return negsig_to_enum(-os.WTERMSIG(status))
+            # elif os.WIFSTOPPED(status):
+            #     # Process was stopped via SIGSTOP or is being traced, and
+            #     # waitpid() was called with WUNTRACED flag. PID is still
+            #     # alive. From now on waitpid() will keep returning (0, 0)
+            #     # until the process state doesn't change.
+            #     # It may make sense to catch/enable this since stopped PIDs
+            #     # ignore SIGTERM.
+            #     interval = sleep(interval)
+            #     continue
+            # elif os.WIFCONTINUED(status):
+            #     # Process was resumed via SIGCONT and waitpid() was called
+            #     # with WCONTINUED flag.
+            #     interval = sleep(interval)
+            #     continue
             else:
-                # should never happen
+                # Should never happen.
                 raise ValueError("unknown process exit status %r" % status)
 
 


Test patch:
diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py
index c2766e23..ffb73f5b 100644
--- a/psutil/tests/__init__.py
+++ b/psutil/tests/__init__.py
@@ -458,21 +458,6 @@ def sh(cmd, **kwds):
     return stdout
 
 
-def _assert_no_pid(pid):
-    # This is here to make sure wait_procs() behaves properly and
-    # investigate:
-    # https://ci.appveyor.com/project/giampaolo/psutil/build/job/
-    #     jiq2cgd6stsbtn60
-    assert not psutil.pid_exists(pid), pid
-    assert pid not in psutil.pids(), pid
-    try:
-        p = psutil.Process(pid)
-    except psutil.NoSuchProcess:
-        pass
-    else:
-        assert 0, "%s is still alive" % p
-
-
 def terminate(proc_or_pid, sig=signal.SIGTERM, wait_timeout=GLOBAL_TIMEOUT):
     """Terminate a process and wait() for it.
     Process can be a PID or an instance of psutil.Process(),
@@ -494,9 +479,16 @@ def terminate(proc_or_pid, sig=signal.SIGTERM, wait_timeout=GLOBAL_TIMEOUT):
             if POSIX:
                 return wait_pid(proc.pid, timeout)
 
+    def sendsig(proc, sig):
+        # If the process received SIGSTOP, SIGCONT is necessary first,
+        # otherwise SIGTERM won't work.
+        if POSIX and sig != signal.SIGKILL:
+            proc.send_signal(signal.SIGCONT)
+        proc.send_signal(sig)
+
     def term_subproc(proc, timeout):
         try:
-            proc.send_signal(sig)
+            sendsig(proc, sig)
         except OSError as err:
             if WINDOWS and err.winerror == 6:  # "invalid handle"
                 pass
@@ -506,7 +498,7 @@ def terminate(proc_or_pid, sig=signal.SIGTERM, wait_timeout=GLOBAL_TIMEOUT):
 
     def term_psproc(proc, timeout):
         try:
-            proc.send_signal(sig)
+            sendsig(proc, sig)
         except psutil.NoSuchProcess:
             pass
         return wait(proc, timeout)
@@ -543,7 +535,8 @@ def terminate(proc_or_pid, sig=signal.SIGTERM, wait_timeout=GLOBAL_TIMEOUT):
     finally:
         if isinstance(p, (subprocess.Popen, psutil.Popen)):
             flush_popen(p)
-        _assert_no_pid(p if isinstance(p, int) else p.pid)
+        pid = p if isinstance(p, int) else p.pid
+        assert not psutil.pid_exists(pid), pid
 
 
 def reap_children(recursive=False):
@@ -881,6 +874,15 @@ class PsutilTestCase(TestCase):
         self.addCleanup(terminate, sproc)  # executed first
         return sproc
 
+    def assertProcessGone(self, proc):
+        self.assertRaises(psutil.NoSuchProcess, psutil.Process, proc.pid)
+        if isinstance(proc, (psutil.Process, psutil.Popen)):
+            assert not proc.is_running()
+            self.assertRaises(psutil.NoSuchProcess, proc.status)
+            proc.wait(timeout=0)  # assert not raise TimeoutExpired
+        assert not psutil.pid_exists(proc.pid), proc.pid
+        self.assertNotIn(proc.pid, psutil.pids())
+
 
 @unittest.skipIf(PYPY, "unreliable on PYPY")
 class TestMemoryLeak(PsutilTestCase):
diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py
index be5ee789..70203c8e 100755
--- a/psutil/tests/test_contracts.py
+++ b/psutil/tests/test_contracts.py
@@ -12,6 +12,7 @@ Some of these are duplicates of tests test_system.py and test_process.py
 import errno
 import multiprocessing
 import os
+import signal
 import stat
 import time
 import traceback
@@ -254,9 +255,9 @@ class TestSystemAPITypes(PsutilTestCase):
             self.assertIsInstance(ifname, str)
             for addr in addrs:
                 if enum is not None:
-                    assert isinstance(addr.family, enum.IntEnum), addr
+                    self.assertIsInstance(addr.family, enum.IntEnum)
                 else:
-                    assert isinstance(addr.family, int), addr
+                    self.assertIsInstance(addr.family, int)
                 self.assertIsInstance(addr.address, str)
                 self.assertIsInstance(addr.netmask, (str, type(None)))
                 self.assertIsInstance(addr.broadcast, (str, type(None)))
@@ -671,6 +672,20 @@ class TestFetchAllProcesses(PsutilTestCase):
             self.assertIsInstance(v, str)
 
 
+class TestProcessWaitType(PsutilTestCase):
+
+    @unittest.skipIf(not POSIX, "not POSIX")
+    def test_negative_signal(self):
+        p = psutil.Process(self.spawn_testproc().pid)
+        p.terminate()
+        code = p.wait()
+        self.assertEqual(code, -signal.SIGTERM)
+        if enum is not None:
+            self.assertIsInstance(code, enum.IntEnum)
+        else:
+            self.assertIsInstance(code, int)
+
+
 if __name__ == '__main__':
     from psutil.tests.runner import run_from_name
     run_from_name(__file__)
diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py
index fcc9d5db..709c77ba 100755
--- a/psutil/tests/test_linux.py
+++ b/psutil/tests/test_linux.py
@@ -1362,6 +1362,7 @@ class TestMisc(PsutilTestCase):
         finally:
             psutil.PROCFS_PATH = "/proc"
 
+    @retry_on_failure()
     def test_issue_687(self):
         # In case of thread ID:
         # - pid_exists() is supposed to return False
diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py
index 4fb8ba5a..15b589ad 100755
--- a/psutil/tests/test_misc.py
+++ b/psutil/tests/test_misc.py
@@ -57,20 +57,25 @@ import psutil.tests
 class TestMisc(PsutilTestCase):
 
     def test_process__repr__(self, func=repr):
-        p = psutil.Process()
+        p = psutil.Process(self.spawn_testproc().pid)
         r = func(p)
         self.assertIn("psutil.Process", r)
         self.assertIn("pid=%s" % p.pid, r)
-        self.assertIn("name=", r)
+        self.assertIn("name='%s'" % p.name(), r)
         self.assertIn("status=", r)
-        self.assertIn(p.name(), r)
-        self.assertIn("status='running'", r)
+        self.assertNotIn("exitcode=", r)
+        p.terminate()
+        p.wait()
+        r = func(p)
+        self.assertIn("status='terminated'", r)
+        self.assertIn("exitcode=", r)
+
         with mock.patch.object(psutil.Process, "name",
                                side_effect=psutil.ZombieProcess(os.getpid())):
             p = psutil.Process()
             r = func(p)
             self.assertIn("pid=%s" % p.pid, r)
-            self.assertIn("zombie", r)
+            self.assertIn("status='zombie'", r)
             self.assertNotIn("name=", r)
         with mock.patch.object(psutil.Process, "name",
                                side_effect=psutil.NoSuchProcess(os.getpid())):
@@ -303,7 +308,10 @@ class TestMisc(PsutilTestCase):
         else:
             with self.assertRaises(Exception):
                 sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
-                sock.bind(("::1", 0))
+                try:
+                    sock.bind(("::1", 0))
+                finally:
+                    sock.close()
 
     def test_isfile_strict(self):
         from psutil._common import isfile_strict
diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py
index a9080b26..dbf15f1c 100755
--- a/psutil/tests/test_process.py
+++ b/psutil/tests/test_process.py
@@ -92,8 +92,7 @@ class TestProcess(PsutilTestCase):
             self.assertEqual(code, signal.SIGTERM)
         else:
             self.assertEqual(code, -signal.SIGKILL)
-        assert not p.is_running()
-        assert not psutil.pid_exists(p.pid)
+        self.assertProcessGone(p)
 
     def test_terminate(self):
         p = self.spawn_psproc()
@@ -103,54 +102,79 @@ class TestProcess(PsutilTestCase):
             self.assertEqual(code, signal.SIGTERM)
         else:
             self.assertEqual(code, -signal.SIGTERM)
-        assert not p.is_running()
-        assert not psutil.pid_exists(p.pid)
+        self.assertProcessGone(p)
 
     def test_send_signal(self):
         sig = signal.SIGKILL if POSIX else signal.SIGTERM
         p = self.spawn_psproc()
         p.send_signal(sig)
         code = p.wait()
-        assert not p.is_running()
-        assert not psutil.pid_exists(p.pid)
-        if POSIX:
+        if WINDOWS:
+            self.assertEqual(code, sig)
+        else:
             self.assertEqual(code, -sig)
-            #
-            p = self.spawn_psproc()
-            p.send_signal(sig)
-            with mock.patch('psutil.os.kill',
-                            side_effect=OSError(errno.ESRCH, "")):
-                with self.assertRaises(psutil.NoSuchProcess):
-                    p.send_signal(sig)
-            #
-            p = self.spawn_psproc()
-            p.send_signal(sig)
-            with mock.patch('psutil.os.kill',
-                            side_effect=OSError(errno.EPERM, "")):
-                with self.assertRaises(psutil.AccessDenied):
-                    p.send_signal(sig)
-            # Sending a signal to process with PID 0 is not allowed as
-            # it would affect every process in the process group of
-            # the calling process (os.getpid()) instead of PID 0").
-            if 0 in psutil.pids():
-                p = psutil.Process(0)
-                self.assertRaises(ValueError, p.send_signal, signal.SIGTERM)
-
-    def test_wait_sysexit(self):
-        # check sys.exit() code
-        pycode = "import time, sys; time.sleep(0.01); sys.exit(5);"
-        p = self.spawn_psproc([PYTHON_EXE, "-c", pycode])
-        self.assertEqual(p.wait(), 5)
-        assert not p.is_running()
+        self.assertProcessGone(p)
+
+    @unittest.skipIf(not POSIX, "not POSIX")
+    def test_send_signal_mocked(self):
+        sig = signal.SIGTERM
+        p = self.spawn_psproc()
+        with mock.patch('psutil.os.kill',
+                        side_effect=OSError(errno.ESRCH, "")):
+            self.assertRaises(psutil.NoSuchProcess, p.send_signal, sig)
+
+        p = self.spawn_psproc()
+        with mock.patch('psutil.os.kill',
+                        side_effect=OSError(errno.EPERM, "")):
+            self.assertRaises(psutil.AccessDenied, p.send_signal, sig)
+
+    def test_wait_exited(self):
+        # Test waitpid() + WIFEXITED -> WEXITSTATUS.
+        # normal return, same as exit(0)
+        cmd = [PYTHON_EXE, "-c", "pass"]
+        p = self.spawn_psproc(cmd)
+        code = p.wait()
+        self.assertEqual(code, 0)
+        self.assertProcessGone(p)
+        # exit(1), implicit in case of error
+        cmd = [PYTHON_EXE, "-c", "1 / 0"]
+        p = self.spawn_psproc(cmd, stderr=subprocess.PIPE)
+        code = p.wait()
+        self.assertEqual(code, 1)
+        self.assertProcessGone(p)
+        # via sys.exit()
+        cmd = [PYTHON_EXE, "-c", "import sys; sys.exit(5);"]
+        p = self.spawn_psproc(cmd)
+        code = p.wait()
+        self.assertEqual(code, 5)
+        self.assertProcessGone(p)
+        # via os._exit()
+        cmd = [PYTHON_EXE, "-c", "import os; os._exit(5);"]
+        p = self.spawn_psproc(cmd)
+        code = p.wait()
+        self.assertEqual(code, 5)
+        self.assertProcessGone(p)
 
-    def test_wait_issued_twice(self):
-        # It is not supposed to raise NSP when the process is gone.
-        # On UNIX this should return None, on Windows it should keep
-        # returning the exit code.
-        pycode = "import time, sys; time.sleep(0.01); sys.exit(5);"
-        p = self.spawn_psproc([PYTHON_EXE, "-c", pycode])
-        self.assertEqual(p.wait(), 5)
-        self.assertIn(p.wait(), (5, None))
+    def test_wait_stopped(self):
+        p = self.spawn_psproc()
+        if POSIX:
+            # Test waitpid() + WIFSTOPPED and WIFCONTINUED.
+            # Note: if a process is stopped it ignores SIGTERM.
+            p.send_signal(signal.SIGSTOP)
+            self.assertRaises(psutil.TimeoutExpired, p.wait, timeout=0.001)
+            p.send_signal(signal.SIGCONT)
+            self.assertRaises(psutil.TimeoutExpired, p.wait, timeout=0.001)
+            p.send_signal(signal.SIGTERM)
+            self.assertEqual(p.wait(), -signal.SIGTERM)
+            self.assertEqual(p.wait(), -signal.SIGTERM)
+        else:
+            p.suspend()
+            self.assertRaises(psutil.TimeoutExpired, p.wait, timeout=0.001)
+            p.resume()
+            self.assertRaises(psutil.TimeoutExpired, p.wait, timeout=0.001)
+            p.terminate()
+            self.assertEqual(p.wait(), signal.SIGTERM)
+            self.assertEqual(p.wait(), signal.SIGTERM)
 
     def test_wait_non_children(self):
         # Test wait() against a process which is not our direct
@@ -197,7 +221,7 @@ class TestProcess(PsutilTestCase):
             self.assertEqual(code, -signal.SIGKILL)
         else:
             self.assertEqual(code, signal.SIGTERM)
-        assert not p.is_running()
+        self.assertProcessGone(p)
 
     def test_cpu_percent(self):
         p = psutil.Process()
@@ -1213,7 +1237,7 @@ class TestProcess(PsutilTestCase):
         p.wait()
         if WINDOWS:
             call_until(psutil.pids, "%s not in ret" % p.pid)
-        assert not p.is_running()
+        self.assertProcessGone(p)
 
         if WINDOWS:
             with self.assertRaises(psutil.NoSuchProcess):
@@ -1371,8 +1395,16 @@ class TestProcess(PsutilTestCase):
             self.assertRaises(psutil.NoSuchProcess, psutil.Process, 0)
             return
 
-        # test all methods
         p = psutil.Process(0)
+        exc = psutil.AccessDenied if WINDOWS else ValueError
+        self.assertRaises(exc, p.wait)
+        self.assertRaises(exc, p.terminate)
+        self.assertRaises(exc, p.suspend)
+        self.assertRaises(exc, p.resume)
+        self.assertRaises(exc, p.kill)
+        self.assertRaises(exc, p.send_signal, signal.SIGTERM)
+
+        # test all methods
         for name in psutil._as_dict_attrnames:
             if name == 'pid':
                 continue
@@ -1536,7 +1568,10 @@ class TestPopen(PsutilTestCase):
             self.assertTrue(dir(proc))
             self.assertRaises(AttributeError, getattr, proc, 'foo')
             proc.terminate()
-        proc.wait(timeout=3)
+        if POSIX:
+            self.assertEqual(proc.wait(), -signal.SIGTERM)
+        else:
+            self.assertEqual(proc.wait(), signal.SIGTERM)
 
     def test_ctx_manager(self):
         with psutil.Popen([PYTHON_EXE, "-V"],
diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py
index 01176b7d..4fc6b33f 100644
--- a/psutil/tests/test_testutils.py
+++ b/psutil/tests/test_testutils.py
@@ -249,31 +249,31 @@ class TestProcessUtils(PsutilTestCase):
         # by subprocess.Popen
         p = self.spawn_testproc()
         terminate(p)
-        assert not psutil.pid_exists(p.pid)
+        self.assertProcessGone(p)
         terminate(p)
         # by psutil.Process
         p = psutil.Process(self.spawn_testproc().pid)
         terminate(p)
-        assert not psutil.pid_exists(p.pid)
+        self.assertProcessGone(p)
         terminate(p)
         # by psutil.Popen
         cmd = [PYTHON_EXE, "-c", "import time; time.sleep(60);"]
         p = psutil.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
         terminate(p)
-        assert not psutil.pid_exists(p.pid)
+        self.assertProcessGone(p)
         terminate(p)
         # by PID
         pid = self.spawn_testproc().pid
         terminate(pid)
-        assert not psutil.pid_exists(pid)
+        self.assertProcessGone(p)
         terminate(pid)
         # zombie
         if POSIX:
             parent, zombie = self.spawn_zombie()
             terminate(parent)
             terminate(zombie)
-            assert not psutil.pid_exists(parent.pid)
-            assert not psutil.pid_exists(zombie.pid)
+            self.assertProcessGone(parent)
+            self.assertProcessGone(zombie)
 
 
 class TestNetUtils(PsutilTestCase):
@@ -373,6 +373,7 @@ class TestMemLeakClass(TestMemoryLeak):
         self.assertRaises(ValueError, self.execute, lambda: 0, tolerance=-1)
         self.assertRaises(ValueError, self.execute, lambda: 0, retry_for=-1)
 
+    @retry_on_failure()
     def test_leak(self):
         def fun():
             ls.append("x" * 24 * 1024)