_psposix.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  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 errno
  7. import glob
  8. import os
  9. import select
  10. import signal
  11. import time
  12. from . import _ntuples as ntp
  13. from ._common import MACOS
  14. from ._common import TimeoutExpired
  15. from ._common import debug
  16. from ._common import memoize
  17. from ._common import usage_percent
  18. if MACOS:
  19. from . import _psutil_osx
  20. __all__ = ['pid_exists', 'wait_pid', 'disk_usage', 'get_terminal_map']
  21. def pid_exists(pid):
  22. """Check whether pid exists in the current process table."""
  23. if pid == 0:
  24. # According to "man 2 kill" PID 0 has a special meaning:
  25. # it refers to <<every process in the process group of the
  26. # calling process>> so we don't want to go any further.
  27. # If we get here it means this UNIX platform *does* have
  28. # a process with id 0.
  29. return True
  30. try:
  31. os.kill(pid, 0)
  32. except ProcessLookupError:
  33. return False
  34. except PermissionError:
  35. # EPERM clearly means there's a process to deny access to
  36. return True
  37. # According to "man 2 kill" possible error values are
  38. # (EINVAL, EPERM, ESRCH)
  39. else:
  40. return True
  41. Negsignal = enum.IntEnum(
  42. 'Negsignal', {x.name: -x.value for x in signal.Signals}
  43. )
  44. def negsig_to_enum(num):
  45. """Convert a negative signal value to an enum."""
  46. try:
  47. return Negsignal(num)
  48. except ValueError:
  49. return num
  50. def convert_exit_code(status):
  51. """Convert a os.waitpid() status to an exit code."""
  52. if os.WIFEXITED(status):
  53. # Process terminated normally by calling exit(3) or _exit(2),
  54. # or by returning from main(). The return value is the
  55. # positive integer passed to *exit().
  56. return os.WEXITSTATUS(status)
  57. if os.WIFSIGNALED(status):
  58. # Process exited due to a signal. Return the negative value
  59. # of that signal.
  60. return negsig_to_enum(-os.WTERMSIG(status))
  61. # if os.WIFSTOPPED(status):
  62. # # Process was stopped via SIGSTOP or is being traced, and
  63. # # waitpid() was called with WUNTRACED flag. PID is still
  64. # # alive. From now on waitpid() will keep returning (0, 0)
  65. # # until the process state doesn't change.
  66. # # It may make sense to catch/enable this since stopped PIDs
  67. # # ignore SIGTERM.
  68. # interval = sleep(interval)
  69. # continue
  70. # if os.WIFCONTINUED(status):
  71. # # Process was resumed via SIGCONT and waitpid() was called
  72. # # with WCONTINUED flag.
  73. # interval = sleep(interval)
  74. # continue
  75. # Should never happen.
  76. msg = f"unknown process exit status {status!r}"
  77. raise ValueError(msg)
  78. def wait_pid_posix(
  79. pid,
  80. timeout=None,
  81. _waitpid=os.waitpid,
  82. _timer=getattr(time, 'monotonic', time.time), # noqa: B008
  83. _min=min,
  84. _sleep=time.sleep,
  85. _pid_exists=pid_exists,
  86. ):
  87. """Wait for a process PID to terminate.
  88. If the process terminated normally by calling exit(3) or _exit(2),
  89. or by returning from main(), the return value is the positive integer
  90. passed to *exit().
  91. If it was terminated by a signal it returns the negated value of the
  92. signal which caused the termination (e.g. -SIGTERM).
  93. If PID is not a children of os.getpid() (current process) just
  94. wait until the process disappears and return None.
  95. If PID does not exist at all return None immediately.
  96. If timeout is specified and process is still alive raise
  97. TimeoutExpired.
  98. If timeout=0 either return immediately or raise TimeoutExpired
  99. (non-blocking).
  100. """
  101. interval = 0.0001
  102. max_interval = 0.04
  103. flags = 0
  104. stop_at = None
  105. if timeout is not None:
  106. flags |= os.WNOHANG
  107. if timeout != 0:
  108. stop_at = _timer() + timeout
  109. def sleep_or_timeout(interval):
  110. # Sleep for some time and return a new increased interval.
  111. if timeout == 0 or (stop_at is not None and _timer() >= stop_at):
  112. raise TimeoutExpired(timeout)
  113. _sleep(interval)
  114. return _min(interval * 2, max_interval)
  115. # See: https://linux.die.net/man/2/waitpid
  116. while True:
  117. try:
  118. retpid, status = os.waitpid(pid, flags)
  119. except ChildProcessError:
  120. # This has two meanings:
  121. # - PID is not a child of os.getpid() in which case
  122. # we keep polling until it's gone
  123. # - PID never existed in the first place
  124. # In both cases we'll eventually return None as we
  125. # can't determine its exit status code.
  126. while _pid_exists(pid):
  127. interval = sleep_or_timeout(interval)
  128. return None
  129. else:
  130. if retpid == 0:
  131. # WNOHANG flag was used and PID is still running.
  132. interval = sleep_or_timeout(interval)
  133. else:
  134. return convert_exit_code(status)
  135. def _waitpid(pid, timeout):
  136. """Wrapper around os.waitpid(). PID is supposed to be gone already,
  137. it just returns the exit code.
  138. """
  139. try:
  140. retpid, status = os.waitpid(pid, 0)
  141. except ChildProcessError:
  142. # PID is not a child of os.getpid().
  143. return wait_pid_posix(pid, timeout)
  144. else:
  145. assert retpid != 0
  146. return convert_exit_code(status)
  147. def wait_pid_pidfd_open(pid, timeout=None):
  148. """Wait for PID to terminate using pidfd_open() + poll(). Linux >=
  149. 5.3 + Python >= 3.9 only.
  150. """
  151. try:
  152. pidfd = os.pidfd_open(pid, 0)
  153. except OSError as err:
  154. if err.errno == errno.ESRCH:
  155. # No such process. os.waitpid() may still be able to return
  156. # the status code.
  157. return wait_pid_posix(pid, timeout)
  158. if err.errno in {errno.EMFILE, errno.ENFILE, errno.ENODEV}:
  159. # EMFILE, ENFILE: too many open files
  160. # ENODEV: anonymous inode filesystem not supported
  161. debug(f"pidfd_open() failed ({err!r}); use fallback")
  162. return wait_pid_posix(pid, timeout)
  163. raise
  164. try:
  165. # poll() / select() have the advantage of not requiring any
  166. # extra file descriptor, contrary to epoll() / kqueue().
  167. # select() crashes if process opens > 1024 FDs, so we use
  168. # poll().
  169. poller = select.poll()
  170. poller.register(pidfd, select.POLLIN)
  171. timeout_ms = None if timeout is None else int(timeout * 1000)
  172. events = poller.poll(timeout_ms) # wait
  173. if not events:
  174. raise TimeoutExpired(timeout)
  175. return _waitpid(pid, timeout)
  176. finally:
  177. os.close(pidfd)
  178. def wait_pid_kqueue(pid, timeout=None):
  179. """Wait for PID to terminate using kqueue(). macOS and BSD only."""
  180. try:
  181. kq = select.kqueue()
  182. except OSError as err:
  183. if err.errno in {errno.EMFILE, errno.ENFILE}: # too many open files
  184. debug(f"kqueue() failed ({err!r}); use fallback")
  185. return wait_pid_posix(pid, timeout)
  186. raise
  187. try:
  188. kev = select.kevent(
  189. pid,
  190. filter=select.KQ_FILTER_PROC,
  191. flags=select.KQ_EV_ADD | select.KQ_EV_ONESHOT,
  192. fflags=select.KQ_NOTE_EXIT,
  193. )
  194. try:
  195. events = kq.control([kev], 1, timeout) # wait
  196. except OSError as err:
  197. if err.errno in {errno.EACCES, errno.EPERM, errno.ESRCH}:
  198. debug(f"kqueue.control() failed ({err!r}); use fallback")
  199. return wait_pid_posix(pid, timeout)
  200. raise
  201. else:
  202. if not events:
  203. raise TimeoutExpired(timeout)
  204. return _waitpid(pid, timeout)
  205. finally:
  206. kq.close()
  207. @memoize
  208. def can_use_pidfd_open():
  209. # Availability: Linux >= 5.3, Python >= 3.9
  210. if not hasattr(os, "pidfd_open"):
  211. return False
  212. try:
  213. pidfd = os.pidfd_open(os.getpid(), 0)
  214. except OSError as err:
  215. if err.errno in {errno.EMFILE, errno.ENFILE}: # noqa: SIM103
  216. # transitory 'too many open files'
  217. return True
  218. # likely blocked by security policy like SECCOMP (EPERM,
  219. # EACCES, ENOSYS)
  220. return False
  221. else:
  222. os.close(pidfd)
  223. return True
  224. @memoize
  225. def can_use_kqueue():
  226. # Availability: macOS, BSD
  227. names = (
  228. "kqueue",
  229. "KQ_EV_ADD",
  230. "KQ_EV_ONESHOT",
  231. "KQ_FILTER_PROC",
  232. "KQ_NOTE_EXIT",
  233. )
  234. if not all(hasattr(select, x) for x in names):
  235. return False
  236. kq = None
  237. try:
  238. kq = select.kqueue()
  239. kev = select.kevent(
  240. os.getpid(),
  241. filter=select.KQ_FILTER_PROC,
  242. flags=select.KQ_EV_ADD | select.KQ_EV_ONESHOT,
  243. fflags=select.KQ_NOTE_EXIT,
  244. )
  245. kq.control([kev], 1, 0)
  246. return True
  247. except OSError as err:
  248. if err.errno in {errno.EMFILE, errno.ENFILE}: # noqa: SIM103
  249. # transitory 'too many open files'
  250. return True
  251. return False
  252. finally:
  253. if kq is not None:
  254. kq.close()
  255. def wait_pid(pid, timeout=None):
  256. # PID 0 passed to waitpid() waits for any child of the current
  257. # process to change state.
  258. assert pid > 0
  259. if timeout is not None:
  260. assert timeout >= 0
  261. if can_use_pidfd_open():
  262. return wait_pid_pidfd_open(pid, timeout)
  263. elif can_use_kqueue():
  264. return wait_pid_kqueue(pid, timeout)
  265. else:
  266. return wait_pid_posix(pid, timeout)
  267. wait_pid.__doc__ = wait_pid_posix.__doc__
  268. def disk_usage(path):
  269. """Return disk usage associated with path.
  270. Note: UNIX usually reserves 5% disk space which is not accessible
  271. by user. In this function "total" and "used" values reflect the
  272. total and used disk space whereas "free" and "percent" represent
  273. the "free" and "used percent" user disk space.
  274. """
  275. st = os.statvfs(path)
  276. # Total space which is only available to root (unless changed
  277. # at system level).
  278. total = st.f_blocks * st.f_frsize
  279. # Remaining free space usable by root.
  280. avail_to_root = st.f_bfree * st.f_frsize
  281. # Remaining free space usable by user.
  282. avail_to_user = st.f_bavail * st.f_frsize
  283. # Total space being used in general.
  284. used = total - avail_to_root
  285. if MACOS:
  286. # see: https://github.com/giampaolo/psutil/pull/2152
  287. used = _psutil_osx.disk_usage_used(path, used)
  288. # Total space which is available to user (same as 'total' but
  289. # for the user).
  290. total_user = used + avail_to_user
  291. # User usage percent compared to the total amount of space
  292. # the user can use. This number would be higher if compared
  293. # to root's because the user has less space (usually -5%).
  294. usage_percent_user = usage_percent(used, total_user, round_=1)
  295. # NB: the percentage is -5% than what shown by df due to
  296. # reserved blocks that we are currently not considering:
  297. # https://github.com/giampaolo/psutil/issues/829#issuecomment-223750462
  298. return ntp.sdiskusage(
  299. total=total, used=used, free=avail_to_user, percent=usage_percent_user
  300. )
  301. @memoize
  302. def get_terminal_map():
  303. """Get a map of device-id -> path as a dict.
  304. Used by Process.terminal().
  305. """
  306. ret = {}
  307. ls = glob.glob('/dev/tty*') + glob.glob('/dev/pts/*')
  308. for name in ls:
  309. assert name not in ret, name
  310. try:
  311. ret[os.stat(name).st_rdev] = name
  312. except FileNotFoundError:
  313. pass
  314. return ret