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'))
34_DEFAULT = object()
35
36# fmt: off
37__all__ = [
38 # OS constants
39 'FREEBSD', 'BSD', 'LINUX', 'NETBSD', 'OPENBSD', 'MACOS', 'OSX', 'POSIX',
40 'SUNOS', 'WINDOWS',
41 # other constants
42 'ENCODING', 'ENCODING_ERRS', 'AF_INET6',
43 # utility functions
44 'conn_tmap', 'deprecated_method', 'isfile_strict',
45 'parse_environ_block', 'path_exists_strict', 'usage_percent',
46 'supports_ipv6', 'sockfam_to_enum', 'socktype_to_enum', "wrap_numbers",
47 'open_text', 'open_binary', 'cat', 'bcat',
48 'bytes2human', 'conn_to_ntuple', 'debug',
49 # shell utils
50 'hilite', 'term_supports_colors', 'print_color',
51]
52# fmt: on
53
54
55# ===================================================================
56# --- OS constants
57# ===================================================================
58
59
60POSIX = os.name == "posix"
61WINDOWS = os.name == "nt"
62LINUX = sys.platform.startswith("linux")
63MACOS = sys.platform.startswith("darwin")
64OSX = MACOS # deprecated alias
65FREEBSD = sys.platform.startswith(("freebsd", "midnightbsd"))
66OPENBSD = sys.platform.startswith("openbsd")
67NETBSD = sys.platform.startswith("netbsd")
68BSD = FREEBSD or OPENBSD or NETBSD
69SUNOS = sys.platform.startswith(("sunos", "solaris"))
70AIX = sys.platform.startswith("aix")
71
72ENCODING = sys.getfilesystemencoding()
73ENCODING_ERRS = sys.getfilesystemencodeerrors()
74
75
76# ===================================================================
77# --- Process.net_connections() 'kind' parameter mapping
78# ===================================================================
79
80
81conn_tmap = {
82 "all": ([AF_INET, AF_INET6, AF_UNIX], [SOCK_STREAM, SOCK_DGRAM]),
83 "tcp": ([AF_INET, AF_INET6], [SOCK_STREAM]),
84 "tcp4": ([AF_INET], [SOCK_STREAM]),
85 "udp": ([AF_INET, AF_INET6], [SOCK_DGRAM]),
86 "udp4": ([AF_INET], [SOCK_DGRAM]),
87 "inet": ([AF_INET, AF_INET6], [SOCK_STREAM, SOCK_DGRAM]),
88 "inet4": ([AF_INET], [SOCK_STREAM, SOCK_DGRAM]),
89 "inet6": ([AF_INET6], [SOCK_STREAM, SOCK_DGRAM]),
90}
91
92if AF_INET6 is not None:
93 conn_tmap.update({
94 "tcp6": ([AF_INET6], [SOCK_STREAM]),
95 "udp6": ([AF_INET6], [SOCK_DGRAM]),
96 })
97
98if AF_UNIX is not None and not SUNOS:
99 conn_tmap.update({"unix": ([AF_UNIX], [SOCK_STREAM, SOCK_DGRAM])})
100
101
102# =====================================================================
103# --- Exceptions
104# =====================================================================
105
106
107class Error(Exception):
108 """Base exception class. All other psutil exceptions inherit
109 from this one.
110 """
111
112 __module__ = 'psutil'
113
114 def _infodict(self, attrs):
115 info = {}
116 for name in attrs:
117 value = getattr(self, name, None)
118 if value or (name == "pid" and value == 0):
119 info[name] = value
120 return info
121
122 def __str__(self):
123 # invoked on `raise Error`
124 info = self._infodict(("pid", "ppid", "name"))
125 if info:
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.
412 """
413 import ipaddress
414
415 if not addr.address or not addr.netmask:
416 return None
417 if addr.family == socket.AF_INET:
418 return str(
419 ipaddress.IPv4Network(
420 f"{addr.address}/{addr.netmask}", strict=False
421 ).broadcast_address
422 )
423 if addr.family == socket.AF_INET6:
424 return str(
425 ipaddress.IPv6Network(
426 f"{addr.address}/{addr.netmask}", strict=False
427 ).broadcast_address
428 )
429
430
431def deprecated_method(replacement):
432 """A decorator which can be used to mark a method as deprecated
433 'replcement' is the method name which will be called instead.
434 """
435
436 def outer(fun):
437 msg = (
438 f"{fun.__name__}() is deprecated and will be removed; use"
439 f" {replacement}() instead"
440 )
441 if fun.__doc__ is None:
442 fun.__doc__ = msg
443
444 @functools.wraps(fun)
445 def inner(self, *args, **kwargs):
446 warnings.warn(msg, category=DeprecationWarning, stacklevel=2)
447 return getattr(self, replacement)(*args, **kwargs)
448
449 return inner
450
451 return outer
452
453
454class deprecated_property:
455 """A descriptor which can be used to mark a property as deprecated.
456 'replacement' is the attribute name to use instead. Usage::
457
458 class Foo:
459 bar = deprecated_property("baz")
460 """
461
462 def __init__(self, replacement):
463 self.replacement = replacement
464 self._msg = None
465
466 def __set_name__(self, owner, name):
467 self._msg = (
468 f"{name} is deprecated and will be removed; use"
469 f" {self.replacement} instead"
470 )
471
472 def __get__(self, obj, objtype=None):
473 if obj is None:
474 return self
475 warnings.warn(self._msg, category=DeprecationWarning, stacklevel=2)
476 return getattr(obj, self.replacement)
477
478
479class _WrapNumbers:
480 """Watches numbers so that they don't overflow and wrap
481 (reset to zero).
482 """
483
484 def __init__(self):
485 self.lock = threading.Lock()
486 self.cache = {}
487 self.reminders = {}
488 self.reminder_keys = {}
489
490 def _add_dict(self, input_dict, name):
491 assert name not in self.cache
492 assert name not in self.reminders
493 assert name not in self.reminder_keys
494 self.cache[name] = input_dict
495 self.reminders[name] = collections.defaultdict(int)
496 self.reminder_keys[name] = collections.defaultdict(set)
497
498 def _remove_dead_reminders(self, input_dict, name):
499 """In case the number of keys changed between calls (e.g. a
500 disk disappears) this removes the entry from self.reminders.
501 """
502 old_dict = self.cache[name]
503 gone_keys = set(old_dict) - set(input_dict)
504 for gone_key in gone_keys:
505 for remkey in self.reminder_keys[name][gone_key]:
506 del self.reminders[name][remkey]
507 del self.reminder_keys[name][gone_key]
508
509 def run(self, input_dict, name):
510 """Cache dict and sum numbers which overflow and wrap.
511 Return an updated copy of `input_dict`.
512 """
513 if name not in self.cache:
514 # This was the first call.
515 self._add_dict(input_dict, name)
516 return input_dict
517
518 self._remove_dead_reminders(input_dict, name)
519
520 old_dict = self.cache[name]
521 new_dict = {}
522 for key in input_dict:
523 input_tuple = input_dict[key]
524 try:
525 old_tuple = old_dict[key]
526 except KeyError:
527 # The input dict has a new key (e.g. a new disk or NIC)
528 # which didn't exist in the previous call.
529 new_dict[key] = input_tuple
530 continue
531
532 bits = []
533 for i in range(len(input_tuple)):
534 input_value = input_tuple[i]
535 old_value = old_tuple[i]
536 remkey = (key, i)
537 if input_value < old_value:
538 # it wrapped!
539 self.reminders[name][remkey] += old_value
540 self.reminder_keys[name][key].add(remkey)
541 bits.append(input_value + self.reminders[name][remkey])
542
543 new_dict[key] = tuple(bits)
544
545 self.cache[name] = input_dict
546 return new_dict
547
548 def cache_clear(self, name=None):
549 """Clear the internal cache, optionally only for function 'name'."""
550 with self.lock:
551 if name is None:
552 self.cache.clear()
553 self.reminders.clear()
554 self.reminder_keys.clear()
555 else:
556 self.cache.pop(name, None)
557 self.reminders.pop(name, None)
558 self.reminder_keys.pop(name, None)
559
560 def cache_info(self):
561 """Return internal cache dicts as a tuple of 3 elements."""
562 with self.lock:
563 return (self.cache, self.reminders, self.reminder_keys)
564
565
566def wrap_numbers(input_dict, name):
567 """Given an `input_dict` and a function `name`, adjust the numbers
568 which "wrap" (restart from zero) across different calls by adding
569 "old value" to "new value" and return an updated dict.
570 """
571 with _wn.lock:
572 return _wn.run(input_dict, name)
573
574
575_wn = _WrapNumbers()
576wrap_numbers.cache_clear = _wn.cache_clear
577wrap_numbers.cache_info = _wn.cache_info
578
579
580# The read buffer size for open() builtin. This (also) dictates how
581# much data we read(2) when iterating over file lines as in:
582# >>> with open(file) as f:
583# ... for line in f:
584# ... ...
585# Default per-line buffer size for binary files is 1K. For text files
586# is 8K. We use a bigger buffer (32K) in order to have more consistent
587# results when reading /proc pseudo files on Linux, see:
588# https://github.com/giampaolo/psutil/issues/2050
589# https://github.com/giampaolo/psutil/issues/708
590FILE_READ_BUFFER_SIZE = 32 * 1024
591
592
593def open_binary(fname):
594 return open(fname, "rb", buffering=FILE_READ_BUFFER_SIZE)
595
596
597def open_text(fname):
598 """Open a file in text mode by using the proper FS encoding and
599 en/decoding error handlers.
600 """
601 # See:
602 # https://github.com/giampaolo/psutil/issues/675
603 # https://github.com/giampaolo/psutil/pull/733
604 fobj = open( # noqa: SIM115
605 fname,
606 buffering=FILE_READ_BUFFER_SIZE,
607 encoding=ENCODING,
608 errors=ENCODING_ERRS,
609 )
610 try:
611 # Dictates per-line read(2) buffer size. Defaults is 8k. See:
612 # https://github.com/giampaolo/psutil/issues/2050#issuecomment-1013387546
613 fobj._CHUNK_SIZE = FILE_READ_BUFFER_SIZE
614 except AttributeError:
615 pass
616 except Exception:
617 fobj.close()
618 raise
619
620 return fobj
621
622
623def cat(fname, fallback=_DEFAULT, _open=open_text):
624 """Read entire file content and return it as a string. File is
625 opened in text mode. If specified, `fallback` is the value
626 returned in case of error, either if the file does not exist or
627 it can't be read().
628 """
629 if fallback is _DEFAULT:
630 with _open(fname) as f:
631 return f.read()
632 else:
633 try:
634 with _open(fname) as f:
635 return f.read()
636 except OSError:
637 return fallback
638
639
640def bcat(fname, fallback=_DEFAULT):
641 """Same as above but opens file in binary mode."""
642 return cat(fname, fallback=fallback, _open=open_binary)
643
644
645def bytes2human(n, format="%(value).1f%(symbol)s"):
646 """Used by various scripts. See: https://code.activestate.com/recipes/578019-bytes-to-human-human-to-bytes-converter/?in=user-4178764.
647
648 >>> bytes2human(10000)
649 '9.8K'
650 >>> bytes2human(100001221)
651 '95.4M'
652 """
653 symbols = ('B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
654 prefix = {}
655 for i, s in enumerate(symbols[1:]):
656 prefix[s] = 1 << (i + 1) * 10
657 for symbol in reversed(symbols[1:]):
658 if abs(n) >= prefix[symbol]:
659 value = float(n) / prefix[symbol]
660 return format % locals()
661 return format % dict(symbol=symbols[0], value=n)
662
663
664def get_procfs_path():
665 """Return updated psutil.PROCFS_PATH constant."""
666 return sys.modules['psutil'].PROCFS_PATH
667
668
669def decode(s):
670 return s.decode(encoding=ENCODING, errors=ENCODING_ERRS)
671
672
673# =====================================================================
674# --- shell utils
675# =====================================================================
676
677
678@functools.lru_cache
679def term_supports_colors(force_color=False):
680 if WINDOWS:
681 return False
682 if force_color:
683 return True
684 if not hasattr(sys.stdout, "isatty") or not sys.stdout.isatty():
685 return False
686 try:
687 sys.stdout.fileno()
688 except Exception: # noqa: BLE001
689 return False
690 return True
691
692
693def hilite(s, color=None, bold=False, force_color=False):
694 """Return an highlighted version of 'string'."""
695 if not term_supports_colors(force_color=force_color):
696 return s
697 attr = []
698 colors = dict(
699 blue='34',
700 brown='33',
701 darkgrey='30',
702 green='32',
703 grey='37',
704 lightblue='36',
705 red='31',
706 violet='35',
707 yellow='93',
708 )
709 colors[None] = '29'
710 try:
711 color = colors[color]
712 except KeyError:
713 msg = f"invalid color {color!r}; choose amongst {list(colors)}"
714 raise ValueError(msg) from None
715 attr.append(color)
716 if bold:
717 attr.append('1')
718 return f"\x1b[{';'.join(attr)}m{s}\x1b[0m"
719
720
721def print_color(
722 s, color=None, bold=False, file=sys.stdout
723): # pragma: no cover
724 """Print a colorized version of string."""
725 if term_supports_colors():
726 s = hilite(s, color=color, bold=bold)
727 print(s, file=file, flush=True)
728
729
730def debug(msg):
731 """If PSUTIL_DEBUG env var is set, print a debug message to stderr."""
732 if PSUTIL_DEBUG:
733 import inspect
734
735 fname, lineno, _, _lines, _index = inspect.getframeinfo(
736 inspect.currentframe().f_back
737 )
738 if isinstance(msg, Exception):
739 if isinstance(msg, OSError):
740 # ...because str(exc) may contain info about the file name
741 msg = f"ignoring {msg}"
742 else:
743 msg = f"ignoring {msg!r}"
744 print( # noqa: T201
745 f"psutil-debug [{fname}:{lineno}]> {msg}", file=sys.stderr
746 )