Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/psutil/_psposix.py: 60%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

173 statements  

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 if err.errno == errno.ESRCH: 

185 # No such process. os.waitpid() may still be able to return 

186 # the status code. 

187 return wait_pid_posix(pid, timeout) 

188 if err.errno in {errno.EMFILE, errno.ENFILE, errno.ENODEV}: 

189 # EMFILE, ENFILE: too many open files 

190 # ENODEV: anonymous inode filesystem not supported 

191 debug(f"pidfd_open() failed ({err!r}); use fallback") 

192 return wait_pid_posix(pid, timeout) 

193 raise 

194 

195 try: 

196 # poll() / select() have the advantage of not requiring any 

197 # extra file descriptor, contrary to epoll() / kqueue(). 

198 # select() crashes if process opens > 1024 FDs, so we use 

199 # poll(). 

200 poller = select.poll() 

201 poller.register(pidfd, select.POLLIN) 

202 timeout_ms = None if timeout is None else int(timeout * 1000) 

203 events = poller.poll(timeout_ms) # wait 

204 

205 if not events: 

206 raise TimeoutExpired(timeout) 

207 return _waitpid(pid, timeout) 

208 finally: 

209 os.close(pidfd) 

210 

211 

212def wait_pid_kqueue(pid, timeout=None): 

213 """Wait for PID to terminate using kqueue(). macOS and BSD only.""" 

214 try: 

215 kq = select.kqueue() 

216 except OSError as err: 

217 if err.errno in {errno.EMFILE, errno.ENFILE}: # too many open files 

218 debug(f"kqueue() failed ({err!r}); use fallback") 

219 return wait_pid_posix(pid, timeout) 

220 raise 

221 

222 try: 

223 kev = select.kevent( 

224 pid, 

225 filter=select.KQ_FILTER_PROC, 

226 flags=select.KQ_EV_ADD | select.KQ_EV_ONESHOT, 

227 fflags=select.KQ_NOTE_EXIT, 

228 ) 

229 try: 

230 events = kq.control([kev], 1, timeout) # wait 

231 except OSError as err: 

232 if err.errno in {errno.EACCES, errno.EPERM, errno.ESRCH}: 

233 debug(f"kqueue.control() failed ({err!r}); use fallback") 

234 return wait_pid_posix(pid, timeout) 

235 raise 

236 else: 

237 if not events: 

238 raise TimeoutExpired(timeout) 

239 return _waitpid(pid, timeout) 

240 finally: 

241 kq.close() 

242 

243 

244@memoize 

245def can_use_pidfd_open(): 

246 # Availability: Linux >= 5.3, Python >= 3.9 

247 if not hasattr(os, "pidfd_open"): 

248 return False 

249 try: 

250 pidfd = os.pidfd_open(os.getpid(), 0) 

251 except OSError as err: 

252 if err.errno in {errno.EMFILE, errno.ENFILE}: # noqa: SIM103 

253 # transitory 'too many open files' 

254 return True 

255 # likely blocked by security policy like SECCOMP (EPERM, 

256 # EACCES, ENOSYS) 

257 return False 

258 else: 

259 os.close(pidfd) 

260 return True 

261 

262 

263@memoize 

264def can_use_kqueue(): 

265 # Availability: macOS, BSD 

266 names = ( 

267 "kqueue", 

268 "KQ_EV_ADD", 

269 "KQ_EV_ONESHOT", 

270 "KQ_FILTER_PROC", 

271 "KQ_NOTE_EXIT", 

272 ) 

273 if not all(hasattr(select, x) for x in names): 

274 return False 

275 kq = None 

276 try: 

277 kq = select.kqueue() 

278 kev = select.kevent( 

279 os.getpid(), 

280 filter=select.KQ_FILTER_PROC, 

281 flags=select.KQ_EV_ADD | select.KQ_EV_ONESHOT, 

282 fflags=select.KQ_NOTE_EXIT, 

283 ) 

284 kq.control([kev], 1, 0) 

285 return True 

286 except OSError as err: 

287 if err.errno in {errno.EMFILE, errno.ENFILE}: # noqa: SIM103 

288 # transitory 'too many open files' 

289 return True 

290 return False 

291 finally: 

292 if kq is not None: 

293 kq.close() 

294 

295 

296def wait_pid(pid, timeout=None): 

297 # PID 0 passed to waitpid() waits for any child of the current 

298 # process to change state. 

299 assert pid > 0 

300 if timeout is not None: 

301 assert timeout >= 0 

302 

303 if can_use_pidfd_open(): 

304 return wait_pid_pidfd_open(pid, timeout) 

305 elif can_use_kqueue(): 

306 return wait_pid_kqueue(pid, timeout) 

307 else: 

308 return wait_pid_posix(pid, timeout) 

309 

310 

311wait_pid.__doc__ = wait_pid_posix.__doc__ 

312 

313 

314def disk_usage(path): 

315 """Return disk usage associated with path. 

316 Note: UNIX usually reserves 5% disk space which is not accessible 

317 by user. In this function "total" and "used" values reflect the 

318 total and used disk space whereas "free" and "percent" represent 

319 the "free" and "used percent" user disk space. 

320 """ 

321 st = os.statvfs(path) 

322 # Total space which is only available to root (unless changed 

323 # at system level). 

324 total = st.f_blocks * st.f_frsize 

325 # Remaining free space usable by root. 

326 avail_to_root = st.f_bfree * st.f_frsize 

327 # Remaining free space usable by user. 

328 avail_to_user = st.f_bavail * st.f_frsize 

329 # Total space being used in general. 

330 used = total - avail_to_root 

331 if MACOS: 

332 # see: https://github.com/giampaolo/psutil/pull/2152 

333 used = _psutil_osx.disk_usage_used(path, used) 

334 # Total space which is available to user (same as 'total' but 

335 # for the user). 

336 total_user = used + avail_to_user 

337 # User usage percent compared to the total amount of space 

338 # the user can use. This number would be higher if compared 

339 # to root's because the user has less space (usually -5%). 

340 usage_percent_user = usage_percent(used, total_user, round_=1) 

341 

342 # NB: the percentage is -5% than what shown by df due to 

343 # reserved blocks that we are currently not considering: 

344 # https://github.com/giampaolo/psutil/issues/829#issuecomment-223750462 

345 return ntp.sdiskusage( 

346 total=total, used=used, free=avail_to_user, percent=usage_percent_user 

347 ) 

348 

349 

350@memoize 

351def get_terminal_map(): 

352 """Get a map of device-id -> path as a dict. 

353 Used by Process.terminal(). 

354 """ 

355 ret = {} 

356 ls = glob.glob('/dev/tty*') + glob.glob('/dev/pts/*') 

357 for name in ls: 

358 assert name not in ret, name 

359 try: 

360 ret[os.stat(name).st_rdev] = name 

361 except FileNotFoundError: 

362 pass 

363 return ret