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 enum
13import functools
14import os
15import socket
16import stat
17import sys
18import threading
19import warnings
20from collections import namedtuple
21from socket import AF_INET
22from socket import SOCK_DGRAM
23from socket import SOCK_STREAM
24
25try:
26 from socket import AF_INET6
27except ImportError:
28 AF_INET6 = None
29try:
30 from socket import AF_UNIX
31except ImportError:
32 AF_UNIX = None
33
34
35PSUTIL_DEBUG = bool(os.getenv('PSUTIL_DEBUG'))
36_DEFAULT = object()
37
38# fmt: off
39__all__ = [
40 # OS constants
41 'FREEBSD', 'BSD', 'LINUX', 'NETBSD', 'OPENBSD', 'MACOS', 'OSX', 'POSIX',
42 'SUNOS', 'WINDOWS',
43 # connection constants
44 'CONN_CLOSE', 'CONN_CLOSE_WAIT', 'CONN_CLOSING', 'CONN_ESTABLISHED',
45 'CONN_FIN_WAIT1', 'CONN_FIN_WAIT2', 'CONN_LAST_ACK', 'CONN_LISTEN',
46 'CONN_NONE', 'CONN_SYN_RECV', 'CONN_SYN_SENT', 'CONN_TIME_WAIT',
47 # net constants
48 'NIC_DUPLEX_FULL', 'NIC_DUPLEX_HALF', 'NIC_DUPLEX_UNKNOWN', # noqa: F822
49 # process status constants
50 'STATUS_DEAD', 'STATUS_DISK_SLEEP', 'STATUS_IDLE', 'STATUS_LOCKED',
51 'STATUS_RUNNING', 'STATUS_SLEEPING', 'STATUS_STOPPED', 'STATUS_SUSPENDED',
52 'STATUS_TRACING_STOP', 'STATUS_WAITING', 'STATUS_WAKE_KILL',
53 'STATUS_WAKING', 'STATUS_ZOMBIE', 'STATUS_PARKED',
54 # other constants
55 'ENCODING', 'ENCODING_ERRS', 'AF_INET6',
56 # named tuples
57 'pconn', 'pcputimes', 'pctxsw', 'pgids', 'pio', 'pionice', 'popenfile',
58 'pthread', 'puids', 'sconn', 'scpustats', 'sdiskio', 'sdiskpart',
59 'sdiskusage', 'snetio', 'snicaddr', 'snicstats', 'sswap', 'suser',
60 # utility functions
61 'conn_tmap', 'deprecated_method', 'isfile_strict', 'memoize',
62 'parse_environ_block', 'path_exists_strict', 'usage_percent',
63 'supports_ipv6', 'sockfam_to_enum', 'socktype_to_enum', "wrap_numbers",
64 'open_text', 'open_binary', 'cat', 'bcat',
65 'bytes2human', 'conn_to_ntuple', 'debug',
66 # shell utils
67 'hilite', 'term_supports_colors', 'print_color',
68]
69# fmt: on
70
71
72# ===================================================================
73# --- OS constants
74# ===================================================================
75
76
77POSIX = os.name == "posix"
78WINDOWS = os.name == "nt"
79LINUX = sys.platform.startswith("linux")
80MACOS = sys.platform.startswith("darwin")
81OSX = MACOS # deprecated alias
82FREEBSD = sys.platform.startswith(("freebsd", "midnightbsd"))
83OPENBSD = sys.platform.startswith("openbsd")
84NETBSD = sys.platform.startswith("netbsd")
85BSD = FREEBSD or OPENBSD or NETBSD
86SUNOS = sys.platform.startswith(("sunos", "solaris"))
87AIX = sys.platform.startswith("aix")
88
89
90# ===================================================================
91# --- API constants
92# ===================================================================
93
94
95# Process.status()
96STATUS_RUNNING = "running"
97STATUS_SLEEPING = "sleeping"
98STATUS_DISK_SLEEP = "disk-sleep"
99STATUS_STOPPED = "stopped"
100STATUS_TRACING_STOP = "tracing-stop"
101STATUS_ZOMBIE = "zombie"
102STATUS_DEAD = "dead"
103STATUS_WAKE_KILL = "wake-kill"
104STATUS_WAKING = "waking"
105STATUS_IDLE = "idle" # Linux, macOS, FreeBSD
106STATUS_LOCKED = "locked" # FreeBSD
107STATUS_WAITING = "waiting" # FreeBSD
108STATUS_SUSPENDED = "suspended" # NetBSD
109STATUS_PARKED = "parked" # Linux
110
111# Process.net_connections() and psutil.net_connections()
112CONN_ESTABLISHED = "ESTABLISHED"
113CONN_SYN_SENT = "SYN_SENT"
114CONN_SYN_RECV = "SYN_RECV"
115CONN_FIN_WAIT1 = "FIN_WAIT1"
116CONN_FIN_WAIT2 = "FIN_WAIT2"
117CONN_TIME_WAIT = "TIME_WAIT"
118CONN_CLOSE = "CLOSE"
119CONN_CLOSE_WAIT = "CLOSE_WAIT"
120CONN_LAST_ACK = "LAST_ACK"
121CONN_LISTEN = "LISTEN"
122CONN_CLOSING = "CLOSING"
123CONN_NONE = "NONE"
124
125
126# net_if_stats()
127class NicDuplex(enum.IntEnum):
128 NIC_DUPLEX_FULL = 2
129 NIC_DUPLEX_HALF = 1
130 NIC_DUPLEX_UNKNOWN = 0
131
132
133globals().update(NicDuplex.__members__)
134
135
136# sensors_battery()
137class BatteryTime(enum.IntEnum):
138 POWER_TIME_UNKNOWN = -1
139 POWER_TIME_UNLIMITED = -2
140
141
142globals().update(BatteryTime.__members__)
143
144# --- others
145
146ENCODING = sys.getfilesystemencoding()
147ENCODING_ERRS = sys.getfilesystemencodeerrors()
148
149
150# ===================================================================
151# --- namedtuples
152# ===================================================================
153
154# --- for system functions
155
156# fmt: off
157# psutil.swap_memory()
158sswap = namedtuple('sswap', ['total', 'used', 'free', 'percent', 'sin',
159 'sout'])
160# psutil.disk_usage()
161sdiskusage = namedtuple('sdiskusage', ['total', 'used', 'free', 'percent'])
162# psutil.disk_io_counters()
163sdiskio = namedtuple('sdiskio', ['read_count', 'write_count',
164 'read_bytes', 'write_bytes',
165 'read_time', 'write_time'])
166# psutil.disk_partitions()
167sdiskpart = namedtuple('sdiskpart', ['device', 'mountpoint', 'fstype', 'opts'])
168# psutil.net_io_counters()
169snetio = namedtuple('snetio', ['bytes_sent', 'bytes_recv',
170 'packets_sent', 'packets_recv',
171 'errin', 'errout',
172 'dropin', 'dropout'])
173# psutil.users()
174suser = namedtuple('suser', ['name', 'terminal', 'host', 'started', 'pid'])
175# psutil.net_connections()
176sconn = namedtuple('sconn', ['fd', 'family', 'type', 'laddr', 'raddr',
177 'status', 'pid'])
178# psutil.net_if_addrs()
179snicaddr = namedtuple('snicaddr',
180 ['family', 'address', 'netmask', 'broadcast', 'ptp'])
181# psutil.net_if_stats()
182snicstats = namedtuple('snicstats',
183 ['isup', 'duplex', 'speed', 'mtu', 'flags'])
184# psutil.cpu_stats()
185scpustats = namedtuple(
186 'scpustats', ['ctx_switches', 'interrupts', 'soft_interrupts', 'syscalls'])
187# psutil.cpu_freq()
188scpufreq = namedtuple('scpufreq', ['current', 'min', 'max'])
189# psutil.sensors_temperatures()
190shwtemp = namedtuple(
191 'shwtemp', ['label', 'current', 'high', 'critical'])
192# psutil.sensors_battery()
193sbattery = namedtuple('sbattery', ['percent', 'secsleft', 'power_plugged'])
194# psutil.sensors_fans()
195sfan = namedtuple('sfan', ['label', 'current'])
196# fmt: on
197
198# --- for Process methods
199
200# psutil.Process.cpu_times()
201pcputimes = namedtuple(
202 'pcputimes', ['user', 'system', 'children_user', 'children_system']
203)
204# psutil.Process.open_files()
205popenfile = namedtuple('popenfile', ['path', 'fd'])
206# psutil.Process.threads()
207pthread = namedtuple('pthread', ['id', 'user_time', 'system_time'])
208# psutil.Process.uids()
209puids = namedtuple('puids', ['real', 'effective', 'saved'])
210# psutil.Process.gids()
211pgids = namedtuple('pgids', ['real', 'effective', 'saved'])
212# psutil.Process.io_counters()
213pio = namedtuple(
214 'pio', ['read_count', 'write_count', 'read_bytes', 'write_bytes']
215)
216# psutil.Process.ionice()
217pionice = namedtuple('pionice', ['ioclass', 'value'])
218# psutil.Process.ctx_switches()
219pctxsw = namedtuple('pctxsw', ['voluntary', 'involuntary'])
220# psutil.Process.net_connections()
221pconn = namedtuple(
222 'pconn', ['fd', 'family', 'type', 'laddr', 'raddr', 'status']
223)
224
225# psutil.net_connections() and psutil.Process.net_connections()
226addr = namedtuple('addr', ['ip', 'port'])
227
228
229# ===================================================================
230# --- Process.net_connections() 'kind' parameter mapping
231# ===================================================================
232
233
234conn_tmap = {
235 "all": ([AF_INET, AF_INET6, AF_UNIX], [SOCK_STREAM, SOCK_DGRAM]),
236 "tcp": ([AF_INET, AF_INET6], [SOCK_STREAM]),
237 "tcp4": ([AF_INET], [SOCK_STREAM]),
238 "udp": ([AF_INET, AF_INET6], [SOCK_DGRAM]),
239 "udp4": ([AF_INET], [SOCK_DGRAM]),
240 "inet": ([AF_INET, AF_INET6], [SOCK_STREAM, SOCK_DGRAM]),
241 "inet4": ([AF_INET], [SOCK_STREAM, SOCK_DGRAM]),
242 "inet6": ([AF_INET6], [SOCK_STREAM, SOCK_DGRAM]),
243}
244
245if AF_INET6 is not None:
246 conn_tmap.update({
247 "tcp6": ([AF_INET6], [SOCK_STREAM]),
248 "udp6": ([AF_INET6], [SOCK_DGRAM]),
249 })
250
251if AF_UNIX is not None and not SUNOS:
252 conn_tmap.update({"unix": ([AF_UNIX], [SOCK_STREAM, SOCK_DGRAM])})
253
254
255# =====================================================================
256# --- Exceptions
257# =====================================================================
258
259
260class Error(Exception):
261 """Base exception class. All other psutil exceptions inherit
262 from this one.
263 """
264
265 __module__ = 'psutil'
266
267 def _infodict(self, attrs):
268 info = collections.OrderedDict()
269 for name in attrs:
270 value = getattr(self, name, None)
271 if value or (name == "pid" and value == 0):
272 info[name] = value
273 return info
274
275 def __str__(self):
276 # invoked on `raise Error`
277 info = self._infodict(("pid", "ppid", "name"))
278 if info:
279 details = "({})".format(
280 ", ".join([f"{k}={v!r}" for k, v in info.items()])
281 )
282 else:
283 details = None
284 return " ".join([x for x in (getattr(self, "msg", ""), details) if x])
285
286 def __repr__(self):
287 # invoked on `repr(Error)`
288 info = self._infodict(("pid", "ppid", "name", "seconds", "msg"))
289 details = ", ".join([f"{k}={v!r}" for k, v in info.items()])
290 return f"psutil.{self.__class__.__name__}({details})"
291
292
293class NoSuchProcess(Error):
294 """Exception raised when a process with a certain PID doesn't
295 or no longer exists.
296 """
297
298 __module__ = 'psutil'
299
300 def __init__(self, pid, name=None, msg=None):
301 Error.__init__(self)
302 self.pid = pid
303 self.name = name
304 self.msg = msg or "process no longer exists"
305
306 def __reduce__(self):
307 return (self.__class__, (self.pid, self.name, self.msg))
308
309
310class ZombieProcess(NoSuchProcess):
311 """Exception raised when querying a zombie process. This is
312 raised on macOS, BSD and Solaris only, and not always: depending
313 on the query the OS may be able to succeed anyway.
314 On Linux all zombie processes are querable (hence this is never
315 raised). Windows doesn't have zombie processes.
316 """
317
318 __module__ = 'psutil'
319
320 def __init__(self, pid, name=None, ppid=None, msg=None):
321 NoSuchProcess.__init__(self, pid, name, msg)
322 self.ppid = ppid
323 self.msg = msg or "PID still exists but it's a zombie"
324
325 def __reduce__(self):
326 return (self.__class__, (self.pid, self.name, self.ppid, self.msg))
327
328
329class AccessDenied(Error):
330 """Exception raised when permission to perform an action is denied."""
331
332 __module__ = 'psutil'
333
334 def __init__(self, pid=None, name=None, msg=None):
335 Error.__init__(self)
336 self.pid = pid
337 self.name = name
338 self.msg = msg or ""
339
340 def __reduce__(self):
341 return (self.__class__, (self.pid, self.name, self.msg))
342
343
344class TimeoutExpired(Error):
345 """Raised on Process.wait(timeout) if timeout expires and process
346 is still alive.
347 """
348
349 __module__ = 'psutil'
350
351 def __init__(self, seconds, pid=None, name=None):
352 Error.__init__(self)
353 self.seconds = seconds
354 self.pid = pid
355 self.name = name
356 self.msg = f"timeout after {seconds} seconds"
357
358 def __reduce__(self):
359 return (self.__class__, (self.seconds, self.pid, self.name))
360
361
362# ===================================================================
363# --- utils
364# ===================================================================
365
366
367def usage_percent(used, total, round_=None):
368 """Calculate percentage usage of 'used' against 'total'."""
369 try:
370 ret = (float(used) / total) * 100
371 except ZeroDivisionError:
372 return 0.0
373 else:
374 if round_ is not None:
375 ret = round(ret, round_)
376 return ret
377
378
379def memoize(fun):
380 """A simple memoize decorator for functions supporting (hashable)
381 positional arguments.
382 It also provides a cache_clear() function for clearing the cache:
383
384 >>> @memoize
385 ... def foo()
386 ... return 1
387 ...
388 >>> foo()
389 1
390 >>> foo.cache_clear()
391 >>>
392
393 It supports:
394 - functions
395 - classes (acts as a @singleton)
396 - staticmethods
397 - classmethods
398
399 It does NOT support:
400 - methods
401 """
402
403 @functools.wraps(fun)
404 def wrapper(*args, **kwargs):
405 key = (args, frozenset(sorted(kwargs.items())))
406 try:
407 return cache[key]
408 except KeyError:
409 try:
410 ret = cache[key] = fun(*args, **kwargs)
411 except Exception as err: # noqa: BLE001
412 raise err from None
413 return ret
414
415 def cache_clear():
416 """Clear cache."""
417 cache.clear()
418
419 cache = {}
420 wrapper.cache_clear = cache_clear
421 return wrapper
422
423
424def memoize_when_activated(fun):
425 """A memoize decorator which is disabled by default. It can be
426 activated and deactivated on request.
427 For efficiency reasons it can be used only against class methods
428 accepting no arguments.
429
430 >>> class Foo:
431 ... @memoize
432 ... def foo()
433 ... print(1)
434 ...
435 >>> f = Foo()
436 >>> # deactivated (default)
437 >>> foo()
438 1
439 >>> foo()
440 1
441 >>>
442 >>> # activated
443 >>> foo.cache_activate(self)
444 >>> foo()
445 1
446 >>> foo()
447 >>> foo()
448 >>>
449 """
450
451 @functools.wraps(fun)
452 def wrapper(self):
453 try:
454 # case 1: we previously entered oneshot() ctx
455 ret = self._cache[fun]
456 except AttributeError:
457 # case 2: we never entered oneshot() ctx
458 try:
459 return fun(self)
460 except Exception as err: # noqa: BLE001
461 raise err from None
462 except KeyError:
463 # case 3: we entered oneshot() ctx but there's no cache
464 # for this entry yet
465 try:
466 ret = fun(self)
467 except Exception as err: # noqa: BLE001
468 raise err from None
469 try:
470 self._cache[fun] = ret
471 except AttributeError:
472 # multi-threading race condition, see:
473 # https://github.com/giampaolo/psutil/issues/1948
474 pass
475 return ret
476
477 def cache_activate(proc):
478 """Activate cache. Expects a Process instance. Cache will be
479 stored as a "_cache" instance attribute.
480 """
481 proc._cache = {}
482
483 def cache_deactivate(proc):
484 """Deactivate and clear cache."""
485 try:
486 del proc._cache
487 except AttributeError:
488 pass
489
490 wrapper.cache_activate = cache_activate
491 wrapper.cache_deactivate = cache_deactivate
492 return wrapper
493
494
495def isfile_strict(path):
496 """Same as os.path.isfile() but does not swallow EACCES / EPERM
497 exceptions, see:
498 http://mail.python.org/pipermail/python-dev/2012-June/120787.html.
499 """
500 try:
501 st = os.stat(path)
502 except PermissionError:
503 raise
504 except OSError:
505 return False
506 else:
507 return stat.S_ISREG(st.st_mode)
508
509
510def path_exists_strict(path):
511 """Same as os.path.exists() but does not swallow EACCES / EPERM
512 exceptions. See:
513 http://mail.python.org/pipermail/python-dev/2012-June/120787.html.
514 """
515 try:
516 os.stat(path)
517 except PermissionError:
518 raise
519 except OSError:
520 return False
521 else:
522 return True
523
524
525def supports_ipv6():
526 """Return True if IPv6 is supported on this platform."""
527 if not socket.has_ipv6 or AF_INET6 is None:
528 return False
529 try:
530 with socket.socket(AF_INET6, socket.SOCK_STREAM) as sock:
531 sock.bind(("::1", 0))
532 return True
533 except OSError:
534 return False
535
536
537def parse_environ_block(data):
538 """Parse a C environ block of environment variables into a dictionary."""
539 # The block is usually raw data from the target process. It might contain
540 # trailing garbage and lines that do not look like assignments.
541 ret = {}
542 pos = 0
543
544 # localize global variable to speed up access.
545 WINDOWS_ = WINDOWS
546 while True:
547 next_pos = data.find("\0", pos)
548 # nul byte at the beginning or double nul byte means finish
549 if next_pos <= pos:
550 break
551 # there might not be an equals sign
552 equal_pos = data.find("=", pos, next_pos)
553 if equal_pos > pos:
554 key = data[pos:equal_pos]
555 value = data[equal_pos + 1 : next_pos]
556 # Windows expects environment variables to be uppercase only
557 if WINDOWS_:
558 key = key.upper()
559 ret[key] = value
560 pos = next_pos + 1
561
562 return ret
563
564
565def sockfam_to_enum(num):
566 """Convert a numeric socket family value to an IntEnum member.
567 If it's not a known member, return the numeric value itself.
568 """
569 try:
570 return socket.AddressFamily(num)
571 except ValueError:
572 return num
573
574
575def socktype_to_enum(num):
576 """Convert a numeric socket type value to an IntEnum member.
577 If it's not a known member, return the numeric value itself.
578 """
579 try:
580 return socket.SocketKind(num)
581 except ValueError:
582 return num
583
584
585def conn_to_ntuple(fd, fam, type_, laddr, raddr, status, status_map, pid=None):
586 """Convert a raw connection tuple to a proper ntuple."""
587 if fam in {socket.AF_INET, AF_INET6}:
588 if laddr:
589 laddr = addr(*laddr)
590 if raddr:
591 raddr = addr(*raddr)
592 if type_ == socket.SOCK_STREAM and fam in {AF_INET, AF_INET6}:
593 status = status_map.get(status, CONN_NONE)
594 else:
595 status = CONN_NONE # ignore whatever C returned to us
596 fam = sockfam_to_enum(fam)
597 type_ = socktype_to_enum(type_)
598 if pid is None:
599 return pconn(fd, fam, type_, laddr, raddr, status)
600 else:
601 return sconn(fd, fam, type_, laddr, raddr, status, pid)
602
603
604def broadcast_addr(addr):
605 """Given the address ntuple returned by ``net_if_addrs()``
606 calculates the broadcast address.
607 """
608 import ipaddress
609
610 if not addr.address or not addr.netmask:
611 return None
612 if addr.family == socket.AF_INET:
613 return str(
614 ipaddress.IPv4Network(
615 f"{addr.address}/{addr.netmask}", strict=False
616 ).broadcast_address
617 )
618 if addr.family == socket.AF_INET6:
619 return str(
620 ipaddress.IPv6Network(
621 f"{addr.address}/{addr.netmask}", strict=False
622 ).broadcast_address
623 )
624
625
626def deprecated_method(replacement):
627 """A decorator which can be used to mark a method as deprecated
628 'replcement' is the method name which will be called instead.
629 """
630
631 def outer(fun):
632 msg = (
633 f"{fun.__name__}() is deprecated and will be removed; use"
634 f" {replacement}() instead"
635 )
636 if fun.__doc__ is None:
637 fun.__doc__ = msg
638
639 @functools.wraps(fun)
640 def inner(self, *args, **kwargs):
641 warnings.warn(msg, category=DeprecationWarning, stacklevel=2)
642 return getattr(self, replacement)(*args, **kwargs)
643
644 return inner
645
646 return outer
647
648
649class _WrapNumbers:
650 """Watches numbers so that they don't overflow and wrap
651 (reset to zero).
652 """
653
654 def __init__(self):
655 self.lock = threading.Lock()
656 self.cache = {}
657 self.reminders = {}
658 self.reminder_keys = {}
659
660 def _add_dict(self, input_dict, name):
661 assert name not in self.cache
662 assert name not in self.reminders
663 assert name not in self.reminder_keys
664 self.cache[name] = input_dict
665 self.reminders[name] = collections.defaultdict(int)
666 self.reminder_keys[name] = collections.defaultdict(set)
667
668 def _remove_dead_reminders(self, input_dict, name):
669 """In case the number of keys changed between calls (e.g. a
670 disk disappears) this removes the entry from self.reminders.
671 """
672 old_dict = self.cache[name]
673 gone_keys = set(old_dict.keys()) - set(input_dict.keys())
674 for gone_key in gone_keys:
675 for remkey in self.reminder_keys[name][gone_key]:
676 del self.reminders[name][remkey]
677 del self.reminder_keys[name][gone_key]
678
679 def run(self, input_dict, name):
680 """Cache dict and sum numbers which overflow and wrap.
681 Return an updated copy of `input_dict`.
682 """
683 if name not in self.cache:
684 # This was the first call.
685 self._add_dict(input_dict, name)
686 return input_dict
687
688 self._remove_dead_reminders(input_dict, name)
689
690 old_dict = self.cache[name]
691 new_dict = {}
692 for key in input_dict:
693 input_tuple = input_dict[key]
694 try:
695 old_tuple = old_dict[key]
696 except KeyError:
697 # The input dict has a new key (e.g. a new disk or NIC)
698 # which didn't exist in the previous call.
699 new_dict[key] = input_tuple
700 continue
701
702 bits = []
703 for i in range(len(input_tuple)):
704 input_value = input_tuple[i]
705 old_value = old_tuple[i]
706 remkey = (key, i)
707 if input_value < old_value:
708 # it wrapped!
709 self.reminders[name][remkey] += old_value
710 self.reminder_keys[name][key].add(remkey)
711 bits.append(input_value + self.reminders[name][remkey])
712
713 new_dict[key] = tuple(bits)
714
715 self.cache[name] = input_dict
716 return new_dict
717
718 def cache_clear(self, name=None):
719 """Clear the internal cache, optionally only for function 'name'."""
720 with self.lock:
721 if name is None:
722 self.cache.clear()
723 self.reminders.clear()
724 self.reminder_keys.clear()
725 else:
726 self.cache.pop(name, None)
727 self.reminders.pop(name, None)
728 self.reminder_keys.pop(name, None)
729
730 def cache_info(self):
731 """Return internal cache dicts as a tuple of 3 elements."""
732 with self.lock:
733 return (self.cache, self.reminders, self.reminder_keys)
734
735
736def wrap_numbers(input_dict, name):
737 """Given an `input_dict` and a function `name`, adjust the numbers
738 which "wrap" (restart from zero) across different calls by adding
739 "old value" to "new value" and return an updated dict.
740 """
741 with _wn.lock:
742 return _wn.run(input_dict, name)
743
744
745_wn = _WrapNumbers()
746wrap_numbers.cache_clear = _wn.cache_clear
747wrap_numbers.cache_info = _wn.cache_info
748
749
750# The read buffer size for open() builtin. This (also) dictates how
751# much data we read(2) when iterating over file lines as in:
752# >>> with open(file) as f:
753# ... for line in f:
754# ... ...
755# Default per-line buffer size for binary files is 1K. For text files
756# is 8K. We use a bigger buffer (32K) in order to have more consistent
757# results when reading /proc pseudo files on Linux, see:
758# https://github.com/giampaolo/psutil/issues/2050
759# https://github.com/giampaolo/psutil/issues/708
760FILE_READ_BUFFER_SIZE = 32 * 1024
761
762
763def open_binary(fname):
764 return open(fname, "rb", buffering=FILE_READ_BUFFER_SIZE)
765
766
767def open_text(fname):
768 """Open a file in text mode by using the proper FS encoding and
769 en/decoding error handlers.
770 """
771 # See:
772 # https://github.com/giampaolo/psutil/issues/675
773 # https://github.com/giampaolo/psutil/pull/733
774 fobj = open( # noqa: SIM115
775 fname,
776 buffering=FILE_READ_BUFFER_SIZE,
777 encoding=ENCODING,
778 errors=ENCODING_ERRS,
779 )
780 try:
781 # Dictates per-line read(2) buffer size. Defaults is 8k. See:
782 # https://github.com/giampaolo/psutil/issues/2050#issuecomment-1013387546
783 fobj._CHUNK_SIZE = FILE_READ_BUFFER_SIZE
784 except AttributeError:
785 pass
786 except Exception:
787 fobj.close()
788 raise
789
790 return fobj
791
792
793def cat(fname, fallback=_DEFAULT, _open=open_text):
794 """Read entire file content and return it as a string. File is
795 opened in text mode. If specified, `fallback` is the value
796 returned in case of error, either if the file does not exist or
797 it can't be read().
798 """
799 if fallback is _DEFAULT:
800 with _open(fname) as f:
801 return f.read()
802 else:
803 try:
804 with _open(fname) as f:
805 return f.read()
806 except OSError:
807 return fallback
808
809
810def bcat(fname, fallback=_DEFAULT):
811 """Same as above but opens file in binary mode."""
812 return cat(fname, fallback=fallback, _open=open_binary)
813
814
815def bytes2human(n, format="%(value).1f%(symbol)s"):
816 """Used by various scripts. See: https://code.activestate.com/recipes/578019-bytes-to-human-human-to-bytes-converter/?in=user-4178764.
817
818 >>> bytes2human(10000)
819 '9.8K'
820 >>> bytes2human(100001221)
821 '95.4M'
822 """
823 symbols = ('B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
824 prefix = {}
825 for i, s in enumerate(symbols[1:]):
826 prefix[s] = 1 << (i + 1) * 10
827 for symbol in reversed(symbols[1:]):
828 if abs(n) >= prefix[symbol]:
829 value = float(n) / prefix[symbol]
830 return format % locals()
831 return format % dict(symbol=symbols[0], value=n)
832
833
834def get_procfs_path():
835 """Return updated psutil.PROCFS_PATH constant."""
836 return sys.modules['psutil'].PROCFS_PATH
837
838
839def decode(s):
840 return s.decode(encoding=ENCODING, errors=ENCODING_ERRS)
841
842
843# =====================================================================
844# --- shell utils
845# =====================================================================
846
847
848@memoize
849def term_supports_colors(file=sys.stdout): # pragma: no cover
850 if os.name == 'nt':
851 return True
852 try:
853 import curses
854
855 assert file.isatty()
856 curses.setupterm()
857 assert curses.tigetnum("colors") > 0
858 except Exception: # noqa: BLE001
859 return False
860 else:
861 return True
862
863
864def hilite(s, color=None, bold=False): # pragma: no cover
865 """Return an highlighted version of 'string'."""
866 if not term_supports_colors():
867 return s
868 attr = []
869 colors = dict(
870 blue='34',
871 brown='33',
872 darkgrey='30',
873 green='32',
874 grey='37',
875 lightblue='36',
876 red='91',
877 violet='35',
878 yellow='93',
879 )
880 colors[None] = '29'
881 try:
882 color = colors[color]
883 except KeyError:
884 msg = f"invalid color {color!r}; choose amongst {list(colors.keys())}"
885 raise ValueError(msg) from None
886 attr.append(color)
887 if bold:
888 attr.append('1')
889 return f"\x1b[{';'.join(attr)}m{s}\x1b[0m"
890
891
892def print_color(
893 s, color=None, bold=False, file=sys.stdout
894): # pragma: no cover
895 """Print a colorized version of string."""
896 if not term_supports_colors():
897 print(s, file=file)
898 elif POSIX:
899 print(hilite(s, color, bold), file=file)
900 else:
901 import ctypes
902
903 DEFAULT_COLOR = 7
904 GetStdHandle = ctypes.windll.Kernel32.GetStdHandle
905 SetConsoleTextAttribute = (
906 ctypes.windll.Kernel32.SetConsoleTextAttribute
907 )
908
909 colors = dict(green=2, red=4, brown=6, yellow=6)
910 colors[None] = DEFAULT_COLOR
911 try:
912 color = colors[color]
913 except KeyError:
914 msg = (
915 f"invalid color {color!r}; choose between"
916 f" {list(colors.keys())!r}"
917 )
918 raise ValueError(msg) from None
919 if bold and color <= 7:
920 color += 8
921
922 handle_id = -12 if file is sys.stderr else -11
923 GetStdHandle.restype = ctypes.c_ulong
924 handle = GetStdHandle(handle_id)
925 SetConsoleTextAttribute(handle, color)
926 try:
927 print(s, file=file)
928 finally:
929 SetConsoleTextAttribute(handle, DEFAULT_COLOR)
930
931
932def debug(msg):
933 """If PSUTIL_DEBUG env var is set, print a debug message to stderr."""
934 if PSUTIL_DEBUG:
935 import inspect
936
937 fname, lineno, _, _lines, _index = inspect.getframeinfo(
938 inspect.currentframe().f_back
939 )
940 if isinstance(msg, Exception):
941 if isinstance(msg, OSError):
942 # ...because str(exc) may contain info about the file name
943 msg = f"ignoring {msg}"
944 else:
945 msg = f"ignoring {msg!r}"
946 print( # noqa: T201
947 f"psutil-debug [{fname}:{lineno}]> {msg}", file=sys.stderr
948 )