_psposix.py 8.1 KB

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