Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Process wait() improvements #1747

Merged
merged 19 commits into from
May 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions docs/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
49 changes: 32 additions & 17 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
================

Expand Down Expand Up @@ -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
Expand Down
46 changes: 25 additions & 21 deletions psutil/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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__,
Expand Down Expand Up @@ -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().
Expand All @@ -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)
Expand All @@ -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):
Expand Down Expand Up @@ -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

Expand Down
Loading