_common.py 28 KB

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