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"""Common objects shared by __init__.py and _ps*.py modules.
6
7Note: this module is imported by setup.py, so it should not import
8psutil or third-party modules.
9"""
10
11import collections
12import functools
13import os
14import socket
15import stat
16import sys
17import threading
18import warnings
19from socket import AF_INET
20from socket import SOCK_DGRAM
21from socket import SOCK_STREAM
22
23try:
24 from socket import AF_INET6
25except ImportError:
26 AF_INET6 = None
27try:
28 from socket import AF_UNIX
29except ImportError:
30 AF_UNIX = None
31
32
33PSUTIL_DEBUG = bool(os.getenv('PSUTIL_DEBUG'))
34PSUTIL_TESTING = bool(os.getenv('PSUTIL_TESTING'))
35_DEFAULT = object()
36
37# fmt: off
38__all__ = [
39 # OS constants
40 'FREEBSD', 'BSD', 'LINUX', 'NETBSD', 'OPENBSD', 'MACOS', 'OSX', 'POSIX',
41 'SUNOS', 'WINDOWS',
42 # other constants
43 'ENCODING', 'ENCODING_ERRS', 'AF_INET6',
44 # utility functions
45 'conn_tmap', 'deprecated_method', 'isfile_strict',
46 'parse_environ_block', 'path_exists_strict', 'usage_percent',
47 'supports_ipv6', 'sockfam_to_enum', 'socktype_to_enum', "wrap_numbers",
48 'open_text', 'open_binary', 'cat', 'bcat',
49 'bytes2human', 'conn_to_ntuple', 'debug', 'warn',
50 # shell utils
51 'hilite', 'term_supports_colors', 'print_color',
52]
53# fmt: on
54
55
56# ===================================================================
57# --- OS constants
58# ===================================================================
59
60
61POSIX = os.name == "posix"
62WINDOWS = os.name == "nt"
63LINUX = sys.platform.startswith("linux")
64MACOS = sys.platform.startswith("darwin")
65OSX = MACOS # deprecated alias
66FREEBSD = sys.platform.startswith(("freebsd", "midnightbsd"))
67OPENBSD = sys.platform.startswith("openbsd")
68NETBSD = sys.platform.startswith("netbsd")
69BSD = FREEBSD or OPENBSD or NETBSD
70SUNOS = sys.platform.startswith(("sunos", "solaris"))
71AIX = sys.platform.startswith("aix")
72
73ENCODING = sys.getfilesystemencoding()
74ENCODING_ERRS = sys.getfilesystemencodeerrors()
75
76
77# ===================================================================
78# --- Process.net_connections() 'kind' parameter mapping
79# ===================================================================
80
81
82conn_tmap = {
83 "all": ([AF_INET, AF_INET6, AF_UNIX], [SOCK_STREAM, SOCK_DGRAM]),
84 "tcp": ([AF_INET, AF_INET6], [SOCK_STREAM]),
85 "tcp4": ([AF_INET], [SOCK_STREAM]),
86 "udp": ([AF_INET, AF_INET6], [SOCK_DGRAM]),
87 "udp4": ([AF_INET], [SOCK_DGRAM]),
88 "inet": ([AF_INET, AF_INET6], [SOCK_STREAM, SOCK_DGRAM]),
89 "inet4": ([AF_INET], [SOCK_STREAM, SOCK_DGRAM]),
90 "inet6": ([AF_INET6], [SOCK_STREAM, SOCK_DGRAM]),
91}
92
93if AF_INET6 is not None:
94 conn_tmap.update({
95 "tcp6": ([AF_INET6], [SOCK_STREAM]),
96 "udp6": ([AF_INET6], [SOCK_DGRAM]),
97 })
98
99if AF_UNIX is not None and not SUNOS:
100 conn_tmap.update({"unix": ([AF_UNIX], [SOCK_STREAM, SOCK_DGRAM])})
101
102
103# =====================================================================
104# --- Exceptions
105# =====================================================================
106
107
108class Error(Exception):
109 """Base exception class. All other psutil exceptions inherit
110 from this one.
111 """
112
113 __module__ = 'psutil'
114
115 def _infodict(self, attrs):
116 info = {}
117 for name in attrs:
118 value = getattr(self, name, None)
119 if value or (name == "pid" and value == 0):
120 info[name] = value
121 return info
122
123 def __str__(self):
124 # invoked on `raise Error`
125 if info := self._infodict(("pid", "ppid", "name")):
126 details = "({})".format(
127 ", ".join([f"{k}={v!r}" for k, v in info.items()])
128 )
129 else:
130 details = None
131 return " ".join([x for x in (getattr(self, "msg", ""), details) if x])
132
133 def __repr__(self):
134 # invoked on `repr(Error)`
135 info = self._infodict(("pid", "ppid", "name", "seconds", "msg"))
136 details = ", ".join([f"{k}={v!r}" for k, v in info.items()])
137 return f"psutil.{self.__class__.__name__}({details})"
138
139
140class NoSuchProcess(Error):
141 """Exception raised when a process with a certain PID doesn't
142 or no longer exists.
143 """
144
145 __module__ = 'psutil'
146
147 def __init__(self, pid, name=None, msg=None):
148 Error.__init__(self)
149 self.pid = pid
150 self.name = name
151 self.msg = msg or "process no longer exists"
152
153 def __reduce__(self):
154 return (self.__class__, (self.pid, self.name, self.msg))
155
156
157class ZombieProcess(NoSuchProcess):
158 """Exception raised when querying a zombie process. This is
159 raised on macOS, BSD and Solaris only, and not always: depending
160 on the query the OS may be able to succeed anyway.
161 On Linux all zombie processes are querable (hence this is never
162 raised). Windows doesn't have zombie processes.
163 """
164
165 __module__ = 'psutil'
166
167 def __init__(self, pid, name=None, ppid=None, msg=None):
168 NoSuchProcess.__init__(self, pid, name, msg)
169 self.ppid = ppid
170 self.msg = msg or "PID still exists but it's a zombie"
171
172 def __reduce__(self):
173 return (self.__class__, (self.pid, self.name, self.ppid, self.msg))
174
175
176class AccessDenied(Error):
177 """Exception raised when permission to perform an action is denied."""
178
179 __module__ = 'psutil'
180
181 def __init__(self, pid=None, name=None, msg=None):
182 Error.__init__(self)
183 self.pid = pid
184 self.name = name
185 self.msg = msg or ""
186
187 def __reduce__(self):
188 return (self.__class__, (self.pid, self.name, self.msg))
189
190
191class TimeoutExpired(Error):
192 """Raised on Process.wait(timeout) if timeout expires and process
193 is still alive.
194 """
195
196 __module__ = 'psutil'
197
198 def __init__(self, seconds, pid=None, name=None):
199 Error.__init__(self)
200 self.seconds = seconds
201 self.pid = pid
202 self.name = name
203 self.msg = f"timeout after {seconds} seconds"
204
205 def __reduce__(self):
206 return (self.__class__, (self.seconds, self.pid, self.name))
207
208
209# ===================================================================
210# --- utils
211# ===================================================================
212
213
214def usage_percent(used, total, round_=None):
215 """Calculate percentage usage of 'used' against 'total'."""
216 try:
217 ret = (float(used) / total) * 100
218 except ZeroDivisionError:
219 return 0.0
220 else:
221 if round_ is not None:
222 ret = round(ret, round_)
223 return ret
224
225
226def memoize_when_activated(fun):
227 """A memoize decorator which is disabled by default. It can be
228 activated and deactivated on request.
229 For efficiency reasons it can be used only against class methods
230 accepting no arguments.
231
232 >>> class Foo:
233 ... @memoize_when_activated
234 ... def foo()
235 ... print(1)
236 ...
237 >>> f = Foo()
238 >>> # deactivated (default)
239 >>> foo()
240 1
241 >>> foo()
242 1
243 >>>
244 >>> # activated
245 >>> foo.cache_activate(self)
246 >>> foo()
247 1
248 >>> foo()
249 >>> foo()
250 >>>
251 """
252
253 @functools.wraps(fun)
254 def wrapper(self):
255 try:
256 # case 1: we previously entered oneshot() ctx
257 ret = self._cache[fun]
258 except AttributeError:
259 # case 2: we never entered oneshot() ctx
260 try:
261 return fun(self)
262 except Exception as err:
263 raise err from None
264 except KeyError:
265 # case 3: we entered oneshot() ctx but there's no cache
266 # for this entry yet
267 try:
268 ret = fun(self)
269 except Exception as err:
270 raise err from None
271 try:
272 self._cache[fun] = ret
273 except AttributeError:
274 # multi-threading race condition, see:
275 # https://github.com/giampaolo/psutil/issues/1948
276 pass
277 return ret
278
279 def cache_activate(proc):
280 """Activate cache. Expects a Process instance. Cache will be
281 stored as a "_cache" instance attribute.
282 """
283 proc._cache = {}
284
285 def cache_deactivate(proc):
286 """Deactivate and clear cache."""
287 try:
288 del proc._cache
289 except AttributeError:
290 pass
291
292 wrapper.cache_activate = cache_activate
293 wrapper.cache_deactivate = cache_deactivate
294 return wrapper
295
296
297def isfile_strict(path):
298 """Same as os.path.isfile() but does not swallow EACCES / EPERM
299 exceptions, see:
300 http://mail.python.org/pipermail/python-dev/2012-June/120787.html.
301 """
302 try:
303 st = os.stat(path)
304 except PermissionError:
305 raise
306 except OSError:
307 return False
308 else:
309 return stat.S_ISREG(st.st_mode)
310
311
312def path_exists_strict(path):
313 """Same as os.path.exists() but does not swallow EACCES / EPERM
314 exceptions. See:
315 http://mail.python.org/pipermail/python-dev/2012-June/120787.html.
316 """
317 try:
318 os.stat(path)
319 except PermissionError:
320 raise
321 except OSError:
322 return False
323 else:
324 return True
325
326
327def supports_ipv6():
328 """Return True if IPv6 is supported on this platform."""
329 if not socket.has_ipv6 or AF_INET6 is None:
330 return False
331 try:
332 with socket.socket(AF_INET6, socket.SOCK_STREAM) as sock:
333 sock.bind(("::1", 0))
334 return True
335 except OSError:
336 return False
337
338
339def parse_environ_block(data):
340 """Parse a C environ block of environment variables into a dictionary."""
341 # The block is usually raw data from the target process. It might contain
342 # trailing garbage and lines that do not look like assignments.
343 ret = {}
344 pos = 0
345
346 # localize global variable to speed up access.
347 WINDOWS_ = WINDOWS
348 while True:
349 next_pos = data.find("\0", pos)
350 # nul byte at the beginning or double nul byte means finish
351 if next_pos <= pos:
352 break
353 # there might not be an equals sign
354 equal_pos = data.find("=", pos, next_pos)
355 if equal_pos > pos:
356 key = data[pos:equal_pos]
357 value = data[equal_pos + 1 : next_pos]
358 # Windows expects environment variables to be uppercase only
359 if WINDOWS_:
360 key = key.upper()
361 ret[key] = value
362 pos = next_pos + 1
363
364 return ret
365
366
367def sockfam_to_enum(num):
368 """Convert a numeric socket family value to an IntEnum member.
369 If it's not a known member, return the numeric value itself.
370 """
371 try:
372 return socket.AddressFamily(num)
373 except ValueError:
374 return num
375
376
377def socktype_to_enum(num):
378 """Convert a numeric socket type value to an IntEnum member.
379 If it's not a known member, return the numeric value itself.
380 """
381 try:
382 return socket.SocketKind(num)
383 except ValueError:
384 return num
385
386
387def conn_to_ntuple(fd, fam, type_, laddr, raddr, status, status_map, pid=None):
388 """Convert a raw connection tuple to a proper ntuple."""
389 from . import _ntuples as ntp
390 from ._enums import ConnectionStatus
391
392 if fam in {socket.AF_INET, AF_INET6}:
393 if laddr:
394 laddr = ntp.addr(*laddr)
395 if raddr:
396 raddr = ntp.addr(*raddr)
397 if type_ == socket.SOCK_STREAM and fam in {AF_INET, AF_INET6}:
398 status = status_map.get(status, ConnectionStatus.CONN_NONE)
399 else:
400 status = ConnectionStatus.CONN_NONE # ignore whatever C returned to us
401 fam = sockfam_to_enum(fam)
402 type_ = socktype_to_enum(type_)
403 if pid is None:
404 return ntp.pconn(fd, fam, type_, laddr, raddr, status)
405 else:
406 return ntp.sconn(fd, fam, type_, laddr, raddr, status, pid)
407
408
409def broadcast_addr(addr):
410 """Given the address ntuple returned by ``net_if_addrs()``
411 calculates the broadcast address. Returns None for a single-host
412 network (/32 or /128), which has no broadcast address.
413 """
414 import ipaddress
415
416 if not addr.address or not addr.netmask:
417 return None
418 if addr.family == socket.AF_INET:
419 net = ipaddress.IPv4Network(
420 f"{addr.address}/{addr.netmask}", strict=False
421 )
422 elif addr.family == socket.AF_INET6:
423 net = ipaddress.IPv6Network(
424 f"{addr.address}/{addr.netmask}", strict=False
425 )
426 else:
427 return None
428 if net.prefixlen == net.max_prefixlen:
429 return None
430 return str(net.broadcast_address)
431
432
433def deprecated_method(replacement):
434 """A decorator which can be used to mark a method as deprecated
435 'replacement' is the method name which will be called instead.
436 """
437
438 def outer(fun):
439 msg = (
440 f"{fun.__name__}() is deprecated and will be removed; use"
441 f" {replacement}() instead"
442 )
443 if fun.__doc__ is None:
444 fun.__doc__ = msg
445
446 @functools.wraps(fun)
447 def inner(self, *args, **kwargs):
448 warnings.warn(msg, category=DeprecationWarning, stacklevel=2)
449 return getattr(self, replacement)(*args, **kwargs)
450
451 return inner
452
453 return outer
454
455
456class _WrapNumbers:
457 """Watches numbers so that they don't overflow and wrap
458 (reset to zero).
459 """
460
461 def __init__(self):
462 self.lock = threading.Lock()
463 self.cache = {}
464 self.reminders = {}
465 self.reminder_keys = {}
466
467 def _add_dict(self, input_dict, name):
468 assert name not in self.cache
469 assert name not in self.reminders
470 assert name not in self.reminder_keys
471 self.cache[name] = input_dict
472 self.reminders[name] = collections.defaultdict(int)
473 self.reminder_keys[name] = collections.defaultdict(set)
474
475 def _remove_dead_reminders(self, input_dict, name):
476 """In case the number of keys changed between calls (e.g. a
477 disk disappears) this removes the entry from self.reminders.
478 """
479 old_dict = self.cache[name]
480 gone_keys = set(old_dict) - set(input_dict)
481 for gone_key in gone_keys:
482 for remkey in self.reminder_keys[name][gone_key]:
483 del self.reminders[name][remkey]
484 del self.reminder_keys[name][gone_key]
485
486 def run(self, input_dict, name):
487 """Cache dict and sum numbers which overflow and wrap.
488 Return an updated copy of `input_dict`.
489 """
490 if name not in self.cache:
491 # This was the first call.
492 self._add_dict(input_dict, name)
493 return input_dict
494
495 self._remove_dead_reminders(input_dict, name)
496
497 old_dict = self.cache[name]
498 new_dict = {}
499 for key in input_dict:
500 input_tuple = input_dict[key]
501 try:
502 old_tuple = old_dict[key]
503 except KeyError:
504 # The input dict has a new key (e.g. a new disk or NIC)
505 # which didn't exist in the previous call.
506 new_dict[key] = input_tuple
507 continue
508
509 bits = []
510 for i in range(len(input_tuple)):
511 input_value = input_tuple[i]
512 old_value = old_tuple[i]
513 remkey = (key, i)
514 if input_value < old_value:
515 # it wrapped!
516 self.reminders[name][remkey] += old_value
517 self.reminder_keys[name][key].add(remkey)
518 bits.append(input_value + self.reminders[name][remkey])
519
520 new_dict[key] = tuple(bits)
521
522 self.cache[name] = input_dict
523 return new_dict
524
525 def cache_clear(self, name=None):
526 """Clear the internal cache, optionally only for function 'name'."""
527 with self.lock:
528 if name is None:
529 self.cache.clear()
530 self.reminders.clear()
531 self.reminder_keys.clear()
532 else:
533 self.cache.pop(name, None)
534 self.reminders.pop(name, None)
535 self.reminder_keys.pop(name, None)
536
537 def cache_info(self):
538 """Return internal cache dicts as a tuple of 3 elements."""
539 with self.lock:
540 return (self.cache, self.reminders, self.reminder_keys)
541
542
543def wrap_numbers(input_dict, name):
544 """Given an `input_dict` and a function `name`, adjust the numbers
545 which "wrap" (restart from zero) across different calls by adding
546 "old value" to "new value" and return an updated dict.
547 """
548 with _wn.lock:
549 return _wn.run(input_dict, name)
550
551
552_wn = _WrapNumbers()
553wrap_numbers.cache_clear = _wn.cache_clear
554wrap_numbers.cache_info = _wn.cache_info
555
556
557# The read buffer size for open() builtin. This (also) dictates how
558# much data we read(2) when iterating over file lines as in:
559# >>> with open(file) as f:
560# ... for line in f:
561# ... ...
562# Default per-line buffer size for binary files is 1K. For text files
563# is 8K. We use a bigger buffer (32K) in order to have more consistent
564# results when reading /proc pseudo files on Linux, see:
565# https://github.com/giampaolo/psutil/issues/2050
566# https://github.com/giampaolo/psutil/issues/708
567FILE_READ_BUFFER_SIZE = 32 * 1024
568
569
570def open_binary(fname):
571 return open(fname, "rb", buffering=FILE_READ_BUFFER_SIZE)
572
573
574def open_text(fname):
575 """Open a file in text mode by using the proper FS encoding and
576 en/decoding error handlers.
577 """
578 # See:
579 # https://github.com/giampaolo/psutil/issues/675
580 # https://github.com/giampaolo/psutil/pull/733
581 fobj = open( # noqa: SIM115
582 fname,
583 buffering=FILE_READ_BUFFER_SIZE,
584 encoding=ENCODING,
585 errors=ENCODING_ERRS,
586 )
587 try:
588 # Dictates per-line read(2) buffer size. Defaults is 8k. See:
589 # https://github.com/giampaolo/psutil/issues/2050#issuecomment-1013387546
590 fobj._CHUNK_SIZE = FILE_READ_BUFFER_SIZE
591 except AttributeError:
592 pass
593 except Exception:
594 fobj.close()
595 raise
596
597 return fobj
598
599
600def cat(fname, fallback=_DEFAULT, _open=open_text):
601 """Read entire file content and return it as a string. File is
602 opened in text mode. If specified, `fallback` is the value
603 returned in case of error, either if the file does not exist or
604 it can't be read().
605 """
606 if fallback is _DEFAULT:
607 with _open(fname) as f:
608 return f.read()
609 else:
610 try:
611 with _open(fname) as f:
612 return f.read()
613 except OSError:
614 return fallback
615
616
617def bcat(fname, fallback=_DEFAULT):
618 """Same as above but opens file in binary mode."""
619 return cat(fname, fallback=fallback, _open=open_binary)
620
621
622def bytes2human(n):
623 """Convert n bytes to a human-readable string.
624
625 >>> bytes2human(10000)
626 '9.8K'
627 >>> bytes2human(100001221)
628 '95.4M'
629 """
630 symbols = ('B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
631 prefix = {}
632 for i, s in enumerate(symbols[1:]):
633 prefix[s] = 1 << (i + 1) * 10
634 for symbol in reversed(symbols[1:]):
635 if abs(n) >= prefix[symbol]:
636 value = float(n) / prefix[symbol]
637 return f"{value:.1f}{symbol}"
638 return f"{float(n):.1f}{symbols[0]}"
639
640
641def get_procfs_path():
642 """Return updated psutil.PROCFS_PATH constant."""
643 return sys.modules['psutil'].PROCFS_PATH
644
645
646def decode(s):
647 return s.decode(encoding=ENCODING, errors=ENCODING_ERRS)
648
649
650# =====================================================================
651# --- shell utils
652# =====================================================================
653
654
655@functools.lru_cache
656def term_supports_colors(force_color=False):
657 if WINDOWS:
658 return False
659 if force_color:
660 return True
661 if not hasattr(sys.stdout, "isatty") or not sys.stdout.isatty():
662 return False
663 try:
664 sys.stdout.fileno()
665 except Exception: # noqa: BLE001
666 return False
667 return True
668
669
670def hilite(s, color=None, bold=False, force_color=False):
671 """Return an highlighted version of 'string'."""
672 if not term_supports_colors(force_color=force_color):
673 return s
674 attr = []
675 colors = dict(
676 blue='34',
677 brown='33',
678 darkgrey='30',
679 green='32',
680 grey='37',
681 lightblue='36',
682 red='31',
683 violet='35',
684 yellow='93',
685 )
686 colors[None] = '29'
687 try:
688 color = colors[color]
689 except KeyError:
690 msg = f"invalid color {color!r}; choose amongst {list(colors)}"
691 raise ValueError(msg) from None
692 attr.append(color)
693 if bold:
694 attr.append('1')
695 return f"\x1b[{';'.join(attr)}m{s}\x1b[0m"
696
697
698def print_color(
699 s, color=None, bold=False, file=sys.stdout
700): # pragma: no cover
701 """Print a colorized version of string."""
702 if term_supports_colors():
703 s = hilite(s, color=color, bold=bold)
704 print(s, file=file, flush=True)
705
706
707def debug(msg):
708 """If PSUTIL_DEBUG env var is set, print a debug message to stderr."""
709 if PSUTIL_DEBUG:
710 import inspect
711
712 fname, lineno, _, _lines, _index = inspect.getframeinfo(
713 inspect.currentframe().f_back
714 )
715 if isinstance(msg, Exception):
716 if isinstance(msg, OSError):
717 # ...because str(exc) may contain info about the file name
718 msg = f"ignoring {msg}"
719 else:
720 msg = f"ignoring {msg!r}"
721 print( # noqa: T201
722 f"psutil-debug [{fname}:{lineno}]> {msg}", file=sys.stderr
723 )
724
725
726def warn(msg):
727 """Emit a RuntimeWarning. Use it for events which are never
728 supposed to happen, and imply a psutil bug.
729 """
730 import inspect
731
732 fname, lineno, _, _lines, _index = inspect.getframeinfo(
733 inspect.currentframe().f_back
734 )
735 msg = f"{msg} (originated from {fname}:{lineno})"
736 if PSUTIL_TESTING:
737 msg = f"CRITICAL: {msg}"
738 raise RuntimeError(msg)
739 try:
740 warnings.warn(msg, RuntimeWarning, stacklevel=2)
741 except RuntimeWarning:
742 pass # -W error: we never want to fail because of a warning