_psposix.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. # Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
  2. # Use of this source code is governed by a BSD-style license that can be
  3. # found in the LICENSE file.
  4. """Routines common to all posix systems."""
  5. import enum
  6. import glob
  7. import os
  8. import signal
  9. import time
  10. from ._common import MACOS
  11. from ._common import TimeoutExpired
  12. from ._common import memoize
  13. from ._common import sdiskusage
  14. from ._common import usage_percent
  15. if MACOS:
  16. from . import _psutil_osx
  17. __all__ = ['pid_exists', 'wait_pid', 'disk_usage', 'get_terminal_map']
  18. def pid_exists(pid):
  19. """Check whether pid exists in the current process table."""
  20. if pid == 0:
  21. # According to "man 2 kill" PID 0 has a special meaning:
  22. # it refers to <<every process in the process group of the
  23. # calling process>> so we don't want to go any further.
  24. # If we get here it means this UNIX platform *does* have
  25. # a process with id 0.
  26. return True
  27. try:
  28. os.kill(pid, 0)
  29. except ProcessLookupError:
  30. return False
  31. except PermissionError:
  32. # EPERM clearly means there's a process to deny access to
  33. return True
  34. # According to "man 2 kill" possible error values are
  35. # (EINVAL, EPERM, ESRCH)
  36. else:
  37. return True
  38. Negsignal = enum.IntEnum(
  39. 'Negsignal', {x.name: -x.value for x in signal.Signals}
  40. )
  41. def negsig_to_enum(num):
  42. """Convert a negative signal value to an enum."""
  43. try:
  44. return Negsignal(num)
  45. except ValueError:
  46. return num
  47. def wait_pid(
  48. pid,
  49. timeout=None,
  50. proc_name=None,
  51. _waitpid=os.waitpid,
  52. _timer=getattr(time, 'monotonic', time.time), # noqa: B008
  53. _min=min,
  54. _sleep=time.sleep,
  55. _pid_exists=pid_exists,
  56. ):
  57. """Wait for a process PID to terminate.
  58. If the process terminated normally by calling exit(3) or _exit(2),
  59. or by returning from main(), the return value is the positive integer
  60. passed to *exit().
  61. If it was terminated by a signal it returns the negated value of the
  62. signal which caused the termination (e.g. -SIGTERM).
  63. If PID is not a children of os.getpid() (current process) just
  64. wait until the process disappears and return None.
  65. If PID does not exist at all return None immediately.
  66. If *timeout* != None and process is still alive raise TimeoutExpired.
  67. timeout=0 is also possible (either return immediately or raise).
  68. """
  69. if pid <= 0:
  70. # see "man waitpid"
  71. msg = "can't wait for PID 0"
  72. raise ValueError(msg)
  73. interval = 0.0001
  74. flags = 0
  75. if timeout is not None:
  76. flags |= os.WNOHANG
  77. stop_at = _timer() + timeout
  78. def sleep(interval):
  79. # Sleep for some time and return a new increased interval.
  80. if timeout is not None:
  81. if _timer() >= stop_at:
  82. raise TimeoutExpired(timeout, pid=pid, name=proc_name)
  83. _sleep(interval)
  84. return _min(interval * 2, 0.04)
  85. # See: https://linux.die.net/man/2/waitpid
  86. while True:
  87. try:
  88. retpid, status = os.waitpid(pid, flags)
  89. except InterruptedError:
  90. interval = sleep(interval)
  91. except ChildProcessError:
  92. # This has two meanings:
  93. # - PID is not a child of os.getpid() in which case
  94. # we keep polling until it's gone
  95. # - PID never existed in the first place
  96. # In both cases we'll eventually return None as we
  97. # can't determine its exit status code.
  98. while _pid_exists(pid):
  99. interval = sleep(interval)
  100. return None
  101. else:
  102. if retpid == 0:
  103. # WNOHANG flag was used and PID is still running.
  104. interval = sleep(interval)
  105. continue
  106. if os.WIFEXITED(status):
  107. # Process terminated normally by calling exit(3) or _exit(2),
  108. # or by returning from main(). The return value is the
  109. # positive integer passed to *exit().
  110. return os.WEXITSTATUS(status)
  111. elif os.WIFSIGNALED(status):
  112. # Process exited due to a signal. Return the negative value
  113. # of that signal.
  114. return negsig_to_enum(-os.WTERMSIG(status))
  115. # elif os.WIFSTOPPED(status):
  116. # # Process was stopped via SIGSTOP or is being traced, and
  117. # # waitpid() was called with WUNTRACED flag. PID is still
  118. # # alive. From now on waitpid() will keep returning (0, 0)
  119. # # until the process state doesn't change.
  120. # # It may make sense to catch/enable this since stopped PIDs
  121. # # ignore SIGTERM.
  122. # interval = sleep(interval)
  123. # continue
  124. # elif os.WIFCONTINUED(status):
  125. # # Process was resumed via SIGCONT and waitpid() was called
  126. # # with WCONTINUED flag.
  127. # interval = sleep(interval)
  128. # continue
  129. else:
  130. # Should never happen.
  131. msg = f"unknown process exit status {status!r}"
  132. raise ValueError(msg)
  133. def disk_usage(path):
  134. """Return disk usage associated with path.
  135. Note: UNIX usually reserves 5% disk space which is not accessible
  136. by user. In this function "total" and "used" values reflect the
  137. total and used disk space whereas "free" and "percent" represent
  138. the "free" and "used percent" user disk space.
  139. """
  140. st = os.statvfs(path)
  141. # Total space which is only available to root (unless changed
  142. # at system level).
  143. total = st.f_blocks * st.f_frsize
  144. # Remaining free space usable by root.
  145. avail_to_root = st.f_bfree * st.f_frsize
  146. # Remaining free space usable by user.
  147. avail_to_user = st.f_bavail * st.f_frsize
  148. # Total space being used in general.
  149. used = total - avail_to_root
  150. if MACOS:
  151. # see: https://github.com/giampaolo/psutil/pull/2152
  152. used = _psutil_osx.disk_usage_used(path, used)
  153. # Total space which is available to user (same as 'total' but
  154. # for the user).
  155. total_user = used + avail_to_user
  156. # User usage percent compared to the total amount of space
  157. # the user can use. This number would be higher if compared
  158. # to root's because the user has less space (usually -5%).
  159. usage_percent_user = usage_percent(used, total_user, round_=1)
  160. # NB: the percentage is -5% than what shown by df due to
  161. # reserved blocks that we are currently not considering:
  162. # https://github.com/giampaolo/psutil/issues/829#issuecomment-223750462
  163. return sdiskusage(
  164. total=total, used=used, free=avail_to_user, percent=usage_percent_user
  165. )
  166. @memoize
  167. def get_terminal_map():
  168. """Get a map of device-id -> path as a dict.
  169. Used by Process.terminal().
  170. """
  171. ret = {}
  172. ls = glob.glob('/dev/tty*') + glob.glob('/dev/pts/*')
  173. for name in ls:
  174. assert name not in ret, name
  175. try:
  176. ret[os.stat(name).st_rdev] = name
  177. except FileNotFoundError:
  178. pass
  179. return ret