| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363 |
- # Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
- # Use of this source code is governed by a BSD-style license that can be
- # found in the LICENSE file.
- """Routines common to all posix systems."""
- import enum
- import errno
- import glob
- import os
- import select
- import signal
- import time
- from . import _ntuples as ntp
- from ._common import MACOS
- from ._common import TimeoutExpired
- from ._common import debug
- from ._common import memoize
- from ._common import usage_percent
- if MACOS:
- from . import _psutil_osx
- __all__ = ['pid_exists', 'wait_pid', 'disk_usage', 'get_terminal_map']
- def pid_exists(pid):
- """Check whether pid exists in the current process table."""
- if pid == 0:
- # According to "man 2 kill" PID 0 has a special meaning:
- # it refers to <<every process in the process group of the
- # calling process>> so we don't want to go any further.
- # If we get here it means this UNIX platform *does* have
- # a process with id 0.
- return True
- try:
- os.kill(pid, 0)
- except ProcessLookupError:
- return False
- except PermissionError:
- # EPERM clearly means there's a process to deny access to
- return True
- # According to "man 2 kill" possible error values are
- # (EINVAL, EPERM, ESRCH)
- else:
- return True
- Negsignal = enum.IntEnum(
- 'Negsignal', {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
- def convert_exit_code(status):
- """Convert a os.waitpid() status to an exit code."""
- if 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)
- if os.WIFSIGNALED(status):
- # Process exited due to a signal. Return the negative value
- # of that signal.
- return negsig_to_enum(-os.WTERMSIG(status))
- # if 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
- # if os.WIFCONTINUED(status):
- # # Process was resumed via SIGCONT and waitpid() was called
- # # with WCONTINUED flag.
- # interval = sleep(interval)
- # continue
- # Should never happen.
- msg = f"unknown process exit status {status!r}"
- raise ValueError(msg)
- def wait_pid_posix(
- pid,
- timeout=None,
- _waitpid=os.waitpid,
- _timer=getattr(time, 'monotonic', time.time), # noqa: B008
- _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 it was terminated by a signal it returns 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 at all return None immediately.
- If timeout is specified and process is still alive raise
- TimeoutExpired.
- If timeout=0 either return immediately or raise TimeoutExpired
- (non-blocking).
- """
- interval = 0.0001
- max_interval = 0.04
- flags = 0
- stop_at = None
- if timeout is not None:
- flags |= os.WNOHANG
- if timeout != 0:
- stop_at = _timer() + timeout
- def sleep_or_timeout(interval):
- # Sleep for some time and return a new increased interval.
- if timeout == 0 or (stop_at is not None and _timer() >= stop_at):
- raise TimeoutExpired(timeout)
- _sleep(interval)
- return _min(interval * 2, max_interval)
- # See: https://linux.die.net/man/2/waitpid
- while True:
- try:
- retpid, status = os.waitpid(pid, flags)
- except ChildProcessError:
- # This has two meanings:
- # - 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
- # In both cases we'll eventually return None as we
- # can't determine its exit status code.
- while _pid_exists(pid):
- interval = sleep_or_timeout(interval)
- return None
- else:
- if retpid == 0:
- # WNOHANG flag was used and PID is still running.
- interval = sleep_or_timeout(interval)
- else:
- return convert_exit_code(status)
- def _waitpid(pid, timeout):
- """Wrapper around os.waitpid(). PID is supposed to be gone already,
- it just returns the exit code.
- """
- try:
- retpid, status = os.waitpid(pid, 0)
- except ChildProcessError:
- # PID is not a child of os.getpid().
- return wait_pid_posix(pid, timeout)
- else:
- assert retpid != 0
- return convert_exit_code(status)
- def wait_pid_pidfd_open(pid, timeout=None):
- """Wait for PID to terminate using pidfd_open() + poll(). Linux >=
- 5.3 + Python >= 3.9 only.
- """
- try:
- pidfd = os.pidfd_open(pid, 0)
- except OSError as err:
- if err.errno == errno.ESRCH:
- # No such process. os.waitpid() may still be able to return
- # the status code.
- return wait_pid_posix(pid, timeout)
- if err.errno in {errno.EMFILE, errno.ENFILE, errno.ENODEV}:
- # EMFILE, ENFILE: too many open files
- # ENODEV: anonymous inode filesystem not supported
- debug(f"pidfd_open() failed ({err!r}); use fallback")
- return wait_pid_posix(pid, timeout)
- raise
- try:
- # poll() / select() have the advantage of not requiring any
- # extra file descriptor, contrary to epoll() / kqueue().
- # select() crashes if process opens > 1024 FDs, so we use
- # poll().
- poller = select.poll()
- poller.register(pidfd, select.POLLIN)
- timeout_ms = None if timeout is None else int(timeout * 1000)
- events = poller.poll(timeout_ms) # wait
- if not events:
- raise TimeoutExpired(timeout)
- return _waitpid(pid, timeout)
- finally:
- os.close(pidfd)
- def wait_pid_kqueue(pid, timeout=None):
- """Wait for PID to terminate using kqueue(). macOS and BSD only."""
- try:
- kq = select.kqueue()
- except OSError as err:
- if err.errno in {errno.EMFILE, errno.ENFILE}: # too many open files
- debug(f"kqueue() failed ({err!r}); use fallback")
- return wait_pid_posix(pid, timeout)
- raise
- try:
- kev = select.kevent(
- pid,
- filter=select.KQ_FILTER_PROC,
- flags=select.KQ_EV_ADD | select.KQ_EV_ONESHOT,
- fflags=select.KQ_NOTE_EXIT,
- )
- try:
- events = kq.control([kev], 1, timeout) # wait
- except OSError as err:
- if err.errno in {errno.EACCES, errno.EPERM, errno.ESRCH}:
- debug(f"kqueue.control() failed ({err!r}); use fallback")
- return wait_pid_posix(pid, timeout)
- raise
- else:
- if not events:
- raise TimeoutExpired(timeout)
- return _waitpid(pid, timeout)
- finally:
- kq.close()
- @memoize
- def can_use_pidfd_open():
- # Availability: Linux >= 5.3, Python >= 3.9
- if not hasattr(os, "pidfd_open"):
- return False
- try:
- pidfd = os.pidfd_open(os.getpid(), 0)
- except OSError as err:
- if err.errno in {errno.EMFILE, errno.ENFILE}: # noqa: SIM103
- # transitory 'too many open files'
- return True
- # likely blocked by security policy like SECCOMP (EPERM,
- # EACCES, ENOSYS)
- return False
- else:
- os.close(pidfd)
- return True
- @memoize
- def can_use_kqueue():
- # Availability: macOS, BSD
- names = (
- "kqueue",
- "KQ_EV_ADD",
- "KQ_EV_ONESHOT",
- "KQ_FILTER_PROC",
- "KQ_NOTE_EXIT",
- )
- if not all(hasattr(select, x) for x in names):
- return False
- kq = None
- try:
- kq = select.kqueue()
- kev = select.kevent(
- os.getpid(),
- filter=select.KQ_FILTER_PROC,
- flags=select.KQ_EV_ADD | select.KQ_EV_ONESHOT,
- fflags=select.KQ_NOTE_EXIT,
- )
- kq.control([kev], 1, 0)
- return True
- except OSError as err:
- if err.errno in {errno.EMFILE, errno.ENFILE}: # noqa: SIM103
- # transitory 'too many open files'
- return True
- return False
- finally:
- if kq is not None:
- kq.close()
- def wait_pid(pid, timeout=None):
- # PID 0 passed to waitpid() waits for any child of the current
- # process to change state.
- assert pid > 0
- if timeout is not None:
- assert timeout >= 0
- if can_use_pidfd_open():
- return wait_pid_pidfd_open(pid, timeout)
- elif can_use_kqueue():
- return wait_pid_kqueue(pid, timeout)
- else:
- return wait_pid_posix(pid, timeout)
- wait_pid.__doc__ = wait_pid_posix.__doc__
- def disk_usage(path):
- """Return disk usage associated with path.
- Note: UNIX usually reserves 5% disk space which is not accessible
- by user. In this function "total" and "used" values reflect the
- total and used disk space whereas "free" and "percent" represent
- the "free" and "used percent" user disk space.
- """
- st = os.statvfs(path)
- # Total space which is only available to root (unless changed
- # at system level).
- total = st.f_blocks * st.f_frsize
- # Remaining free space usable by root.
- avail_to_root = st.f_bfree * st.f_frsize
- # Remaining free space usable by user.
- avail_to_user = st.f_bavail * st.f_frsize
- # Total space being used in general.
- used = total - avail_to_root
- if MACOS:
- # see: https://github.com/giampaolo/psutil/pull/2152
- used = _psutil_osx.disk_usage_used(path, used)
- # Total space which is available to user (same as 'total' but
- # for the user).
- total_user = used + avail_to_user
- # User usage percent compared to the total amount of space
- # the user can use. This number would be higher if compared
- # to root's because the user has less space (usually -5%).
- usage_percent_user = usage_percent(used, total_user, round_=1)
- # NB: the percentage is -5% than what shown by df due to
- # reserved blocks that we are currently not considering:
- # https://github.com/giampaolo/psutil/issues/829#issuecomment-223750462
- return ntp.sdiskusage(
- total=total, used=used, free=avail_to_user, percent=usage_percent_user
- )
- @memoize
- def get_terminal_map():
- """Get a map of device-id -> path as a dict.
- Used by Process.terminal().
- """
- ret = {}
- ls = glob.glob('/dev/tty*') + glob.glob('/dev/pts/*')
- for name in ls:
- assert name not in ret, name
- try:
- ret[os.stat(name).st_rdev] = name
- except FileNotFoundError:
- pass
- return ret
|