Skip to content

Commit

Permalink
Process wait() improvements (#1747)
Browse files Browse the repository at this point in the history
* `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)
  • Loading branch information
giampaolo authored May 2, 2020
1 parent 6f2bdc4 commit 42368e6
Show file tree
Hide file tree
Showing 12 changed files with 306 additions and 159 deletions.
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

0 comments on commit 42368e6

Please sign in to comment.