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