diff --git a/HISTORY.rst b/HISTORY.rst index 6f8087db3..0962be3e3 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() + + ``` +- 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=, 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 62ab3f234..2137a1a28 100644 --- a/README.rst +++ b/README.rst @@ -438,7 +438,7 @@ Process management >>> p.terminate() >>> p.kill() >>> p.wait(timeout=3) - 0 + >>> >>> psutil.test() USER PID %CPU %MEM VSZ RSS TTY START TIME COMMAND diff --git a/docs/Makefile b/docs/Makefile index cca5435fa..860a2b0e2 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 133e69fe7..699ea1f16 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() + + + .. 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() `, :meth:`terminate() `, - :meth:`kill() ` and - :meth:`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() `. + 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() ` - 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 cabf0e0e3..7fdbc0fc2 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -232,6 +232,7 @@ _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 @@ def _init(self, pid, _ignore_nsp=False): 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 @@ def __str__(self): 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 @@ def wait(self, timeout=None): """ 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 @@ def wait(self, timeout=None): 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 @@ def __getattribute__(self, name): 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 88213ef8b..2e6711a3b 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 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 c2766e23c..ffb73f5b5 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 wait(proc, 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 term_subproc(proc, 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 flush_popen(proc): 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 @@ def pyrun(self, *args, **kwds): 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 be5ee7898..70203c8e9 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -12,6 +12,7 @@ import errno import multiprocessing import os +import signal import stat import time import traceback @@ -254,9 +255,9 @@ def test_net_if_addrs(self): 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 @@ def environ(self, ret, info): 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 fcc9d5db6..709c77bab 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1362,6 +1362,7 @@ def test_procfs_path(self): 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 4fb8ba5a9..15b589ad6 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -57,20 +57,25 @@ 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 @@ def test_supports_ipv6(self): 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 a9080b266..dbf15f1cf 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -92,8 +92,7 @@ def test_kill(self): 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 @@ def test_terminate(self): 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 @@ def test_wait_timeout_nonblocking(self): 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 @@ def test_halfway_terminated_process(self): 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 @@ def test_pid_0(self): 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 @@ def test_misc(self): 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 01176b7d0..4fc6b33f5 100644 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -249,31 +249,31 @@ def test_terminate(self): # 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 @@ def test_param_err(self): 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)