summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGiampaolo Rodola <g.rodola@gmail.com>2020-05-02 15:19:53 -0700
committerGitHub <noreply@github.com>2020-05-03 00:19:53 +0200
commit42368e6a786415d932cb48d0fd006a0c302ab31b (patch)
treebb9ed83939f4742971791fa699282a82844ce66b
parent6f2bdc4223789af13156915e930f2fff7d68d46a (diff)
downloadpsutil-42368e6a786415d932cb48d0fd006a0c302ab31b.tar.gz
Process wait() improvements (#1747)
* `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)
-rw-r--r--HISTORY.rst19
-rw-r--r--README.rst2
-rw-r--r--docs/Makefile5
-rw-r--r--docs/index.rst49
-rw-r--r--psutil/__init__.py46
-rw-r--r--psutil/_psposix.py126
-rw-r--r--psutil/tests/__init__.py38
-rwxr-xr-xpsutil/tests/test_contracts.py19
-rwxr-xr-xpsutil/tests/test_linux.py1
-rwxr-xr-xpsutil/tests/test_misc.py20
-rwxr-xr-xpsutil/tests/test_process.py127
-rw-r--r--psutil/tests/test_testutils.py13
12 files changed, 306 insertions, 159 deletions
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)
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)