diff options
author | Giampaolo Rodola <g.rodola@gmail.com> | 2020-05-02 15:19:53 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-05-03 00:19:53 +0200 |
commit | 42368e6a786415d932cb48d0fd006a0c302ab31b (patch) | |
tree | bb9ed83939f4742971791fa699282a82844ce66b /psutil/_psposix.py | |
parent | 6f2bdc4223789af13156915e930f2fff7d68d46a (diff) | |
download | psutil-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)
Diffstat (limited to 'psutil/_psposix.py')
-rw-r--r-- | psutil/_psposix.py | 126 |
1 files changed, 87 insertions, 39 deletions
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) |