_psposix.py 7.9 KB

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