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"""Linux platform implementation."""
6
7
8import base64
9import collections
10import enum
11import errno
12import functools
13import glob
14import os
15import re
16import resource
17import socket
18import struct
19import sys
20import warnings
21from collections import defaultdict
22from collections import namedtuple
23
24from . import _common
25from . import _psposix
26from . import _psutil_linux as cext
27from . import _psutil_posix as cext_posix
28from ._common import ENCODING
29from ._common import NIC_DUPLEX_FULL
30from ._common import NIC_DUPLEX_HALF
31from ._common import NIC_DUPLEX_UNKNOWN
32from ._common import AccessDenied
33from ._common import NoSuchProcess
34from ._common import ZombieProcess
35from ._common import bcat
36from ._common import cat
37from ._common import debug
38from ._common import decode
39from ._common import get_procfs_path
40from ._common import isfile_strict
41from ._common import memoize
42from ._common import memoize_when_activated
43from ._common import open_binary
44from ._common import open_text
45from ._common import parse_environ_block
46from ._common import path_exists_strict
47from ._common import supports_ipv6
48from ._common import usage_percent
49
50# fmt: off
51__extra__all__ = [
52 'PROCFS_PATH',
53 # io prio constants
54 "IOPRIO_CLASS_NONE", "IOPRIO_CLASS_RT", "IOPRIO_CLASS_BE",
55 "IOPRIO_CLASS_IDLE",
56 # connection status constants
57 "CONN_ESTABLISHED", "CONN_SYN_SENT", "CONN_SYN_RECV", "CONN_FIN_WAIT1",
58 "CONN_FIN_WAIT2", "CONN_TIME_WAIT", "CONN_CLOSE", "CONN_CLOSE_WAIT",
59 "CONN_LAST_ACK", "CONN_LISTEN", "CONN_CLOSING",
60]
61
62if hasattr(resource, "prlimit"):
63 __extra__all__.extend(
64 [x for x in dir(cext) if x.startswith('RLIM') and x.isupper()]
65 )
66# fmt: on
67
68
69# =====================================================================
70# --- globals
71# =====================================================================
72
73
74POWER_SUPPLY_PATH = "/sys/class/power_supply"
75HAS_PROC_SMAPS = os.path.exists(f"/proc/{os.getpid()}/smaps")
76HAS_PROC_SMAPS_ROLLUP = os.path.exists(f"/proc/{os.getpid()}/smaps_rollup")
77HAS_PROC_IO_PRIORITY = hasattr(cext, "proc_ioprio_get")
78HAS_CPU_AFFINITY = hasattr(cext, "proc_cpu_affinity_get")
79
80# Number of clock ticks per second
81CLOCK_TICKS = os.sysconf("SC_CLK_TCK")
82PAGESIZE = cext_posix.getpagesize()
83LITTLE_ENDIAN = sys.byteorder == 'little'
84UNSET = object()
85
86# "man iostat" states that sectors are equivalent with blocks and have
87# a size of 512 bytes. Despite this value can be queried at runtime
88# via /sys/block/{DISK}/queue/hw_sector_size and results may vary
89# between 1k, 2k, or 4k... 512 appears to be a magic constant used
90# throughout Linux source code:
91# * https://stackoverflow.com/a/38136179/376587
92# * https://lists.gt.net/linux/kernel/2241060
93# * https://github.com/giampaolo/psutil/issues/1305
94# * https://github.com/torvalds/linux/blob/
95# 4f671fe2f9523a1ea206f63fe60a7c7b3a56d5c7/include/linux/bio.h#L99
96# * https://lkml.org/lkml/2015/8/17/234
97DISK_SECTOR_SIZE = 512
98
99AddressFamily = enum.IntEnum(
100 'AddressFamily', {'AF_LINK': int(socket.AF_PACKET)}
101)
102AF_LINK = AddressFamily.AF_LINK
103
104
105# ioprio_* constants http://linux.die.net/man/2/ioprio_get
106class IOPriority(enum.IntEnum):
107 IOPRIO_CLASS_NONE = 0
108 IOPRIO_CLASS_RT = 1
109 IOPRIO_CLASS_BE = 2
110 IOPRIO_CLASS_IDLE = 3
111
112
113globals().update(IOPriority.__members__)
114
115# See:
116# https://github.com/torvalds/linux/blame/master/fs/proc/array.c
117# ...and (TASK_* constants):
118# https://github.com/torvalds/linux/blob/master/include/linux/sched.h
119PROC_STATUSES = {
120 "R": _common.STATUS_RUNNING,
121 "S": _common.STATUS_SLEEPING,
122 "D": _common.STATUS_DISK_SLEEP,
123 "T": _common.STATUS_STOPPED,
124 "t": _common.STATUS_TRACING_STOP,
125 "Z": _common.STATUS_ZOMBIE,
126 "X": _common.STATUS_DEAD,
127 "x": _common.STATUS_DEAD,
128 "K": _common.STATUS_WAKE_KILL,
129 "W": _common.STATUS_WAKING,
130 "I": _common.STATUS_IDLE,
131 "P": _common.STATUS_PARKED,
132}
133
134# https://github.com/torvalds/linux/blob/master/include/net/tcp_states.h
135TCP_STATUSES = {
136 "01": _common.CONN_ESTABLISHED,
137 "02": _common.CONN_SYN_SENT,
138 "03": _common.CONN_SYN_RECV,
139 "04": _common.CONN_FIN_WAIT1,
140 "05": _common.CONN_FIN_WAIT2,
141 "06": _common.CONN_TIME_WAIT,
142 "07": _common.CONN_CLOSE,
143 "08": _common.CONN_CLOSE_WAIT,
144 "09": _common.CONN_LAST_ACK,
145 "0A": _common.CONN_LISTEN,
146 "0B": _common.CONN_CLOSING,
147}
148
149
150# =====================================================================
151# --- named tuples
152# =====================================================================
153
154
155# fmt: off
156# psutil.virtual_memory()
157svmem = namedtuple(
158 'svmem', ['total', 'available', 'percent', 'used', 'free',
159 'active', 'inactive', 'buffers', 'cached', 'shared', 'slab'])
160# psutil.disk_io_counters()
161sdiskio = namedtuple(
162 'sdiskio', ['read_count', 'write_count',
163 'read_bytes', 'write_bytes',
164 'read_time', 'write_time',
165 'read_merged_count', 'write_merged_count',
166 'busy_time'])
167# psutil.Process().open_files()
168popenfile = namedtuple(
169 'popenfile', ['path', 'fd', 'position', 'mode', 'flags'])
170# psutil.Process().memory_info()
171pmem = namedtuple('pmem', 'rss vms shared text lib data dirty')
172# psutil.Process().memory_full_info()
173pfullmem = namedtuple('pfullmem', pmem._fields + ('uss', 'pss', 'swap'))
174# psutil.Process().memory_maps(grouped=True)
175pmmap_grouped = namedtuple(
176 'pmmap_grouped',
177 ['path', 'rss', 'size', 'pss', 'shared_clean', 'shared_dirty',
178 'private_clean', 'private_dirty', 'referenced', 'anonymous', 'swap'])
179# psutil.Process().memory_maps(grouped=False)
180pmmap_ext = namedtuple(
181 'pmmap_ext', 'addr perms ' + ' '.join(pmmap_grouped._fields))
182# psutil.Process.io_counters()
183pio = namedtuple('pio', ['read_count', 'write_count',
184 'read_bytes', 'write_bytes',
185 'read_chars', 'write_chars'])
186# psutil.Process.cpu_times()
187pcputimes = namedtuple('pcputimes',
188 ['user', 'system', 'children_user', 'children_system',
189 'iowait'])
190# fmt: on
191
192
193# =====================================================================
194# --- utils
195# =====================================================================
196
197
198def readlink(path):
199 """Wrapper around os.readlink()."""
200 assert isinstance(path, str), path
201 path = os.readlink(path)
202 # readlink() might return paths containing null bytes ('\x00')
203 # resulting in "TypeError: must be encoded string without NULL
204 # bytes, not str" errors when the string is passed to other
205 # fs-related functions (os.*, open(), ...).
206 # Apparently everything after '\x00' is garbage (we can have
207 # ' (deleted)', 'new' and possibly others), see:
208 # https://github.com/giampaolo/psutil/issues/717
209 path = path.split('\x00')[0]
210 # Certain paths have ' (deleted)' appended. Usually this is
211 # bogus as the file actually exists. Even if it doesn't we
212 # don't care.
213 if path.endswith(' (deleted)') and not path_exists_strict(path):
214 path = path[:-10]
215 return path
216
217
218def file_flags_to_mode(flags):
219 """Convert file's open() flags into a readable string.
220 Used by Process.open_files().
221 """
222 modes_map = {os.O_RDONLY: 'r', os.O_WRONLY: 'w', os.O_RDWR: 'w+'}
223 mode = modes_map[flags & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR)]
224 if flags & os.O_APPEND:
225 mode = mode.replace('w', 'a', 1)
226 mode = mode.replace('w+', 'r+')
227 # possible values: r, w, a, r+, a+
228 return mode
229
230
231def is_storage_device(name):
232 """Return True if the given name refers to a root device (e.g.
233 "sda", "nvme0n1") as opposed to a logical partition (e.g. "sda1",
234 "nvme0n1p1"). If name is a virtual device (e.g. "loop1", "ram")
235 return True.
236 """
237 # Re-adapted from iostat source code, see:
238 # https://github.com/sysstat/sysstat/blob/
239 # 97912938cd476645b267280069e83b1c8dc0e1c7/common.c#L208
240 # Some devices may have a slash in their name (e.g. cciss/c0d0...).
241 name = name.replace('/', '!')
242 including_virtual = True
243 if including_virtual:
244 path = f"/sys/block/{name}"
245 else:
246 path = f"/sys/block/{name}/device"
247 return os.access(path, os.F_OK)
248
249
250@memoize
251def set_scputimes_ntuple(procfs_path):
252 """Set a namedtuple of variable fields depending on the CPU times
253 available on this Linux kernel version which may be:
254 (user, nice, system, idle, iowait, irq, softirq, [steal, [guest,
255 [guest_nice]]])
256 Used by cpu_times() function.
257 """
258 global scputimes
259 with open_binary(f"{procfs_path}/stat") as f:
260 values = f.readline().split()[1:]
261 fields = ['user', 'nice', 'system', 'idle', 'iowait', 'irq', 'softirq']
262 vlen = len(values)
263 if vlen >= 8:
264 # Linux >= 2.6.11
265 fields.append('steal')
266 if vlen >= 9:
267 # Linux >= 2.6.24
268 fields.append('guest')
269 if vlen >= 10:
270 # Linux >= 3.2.0
271 fields.append('guest_nice')
272 scputimes = namedtuple('scputimes', fields)
273
274
275try:
276 set_scputimes_ntuple("/proc")
277except Exception as err: # noqa: BLE001
278 # Don't want to crash at import time.
279 debug(f"ignoring exception on import: {err!r}")
280 scputimes = namedtuple('scputimes', 'user system idle')(0.0, 0.0, 0.0)
281
282
283# =====================================================================
284# --- system memory
285# =====================================================================
286
287
288def calculate_avail_vmem(mems):
289 """Fallback for kernels < 3.14 where /proc/meminfo does not provide
290 "MemAvailable", see:
291 https://blog.famzah.net/2014/09/24/.
292
293 This code reimplements the algorithm outlined here:
294 https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/
295 commit/?id=34e431b0ae398fc54ea69ff85ec700722c9da773
296
297 We use this function also when "MemAvailable" returns 0 (possibly a
298 kernel bug, see: https://github.com/giampaolo/psutil/issues/1915).
299 In that case this routine matches "free" CLI tool result ("available"
300 column).
301
302 XXX: on recent kernels this calculation may differ by ~1.5% compared
303 to "MemAvailable:", as it's calculated slightly differently.
304 It is still way more realistic than doing (free + cached) though.
305 See:
306 * https://gitlab.com/procps-ng/procps/issues/42
307 * https://github.com/famzah/linux-memavailable-procfs/issues/2
308 """
309 # Note about "fallback" value. According to:
310 # https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/
311 # commit/?id=34e431b0ae398fc54ea69ff85ec700722c9da773
312 # ...long ago "available" memory was calculated as (free + cached),
313 # We use fallback when one of these is missing from /proc/meminfo:
314 # "Active(file)": introduced in 2.6.28 / Dec 2008
315 # "Inactive(file)": introduced in 2.6.28 / Dec 2008
316 # "SReclaimable": introduced in 2.6.19 / Nov 2006
317 # /proc/zoneinfo: introduced in 2.6.13 / Aug 2005
318 free = mems[b'MemFree:']
319 fallback = free + mems.get(b"Cached:", 0)
320 try:
321 lru_active_file = mems[b'Active(file):']
322 lru_inactive_file = mems[b'Inactive(file):']
323 slab_reclaimable = mems[b'SReclaimable:']
324 except KeyError as err:
325 debug(
326 f"{err.args[0]} is missing from /proc/meminfo; using an"
327 " approximation for calculating available memory"
328 )
329 return fallback
330 try:
331 f = open_binary(f"{get_procfs_path()}/zoneinfo")
332 except OSError:
333 return fallback # kernel 2.6.13
334
335 watermark_low = 0
336 with f:
337 for line in f:
338 line = line.strip()
339 if line.startswith(b'low'):
340 watermark_low += int(line.split()[1])
341 watermark_low *= PAGESIZE
342
343 avail = free - watermark_low
344 pagecache = lru_active_file + lru_inactive_file
345 pagecache -= min(pagecache / 2, watermark_low)
346 avail += pagecache
347 avail += slab_reclaimable - min(slab_reclaimable / 2.0, watermark_low)
348 return int(avail)
349
350
351def virtual_memory():
352 """Report virtual memory stats.
353 This implementation mimics procps-ng-3.3.12, aka "free" CLI tool:
354 https://gitlab.com/procps-ng/procps/blob/
355 24fd2605c51fccc375ab0287cec33aa767f06718/proc/sysinfo.c#L778-791
356 The returned values are supposed to match both "free" and "vmstat -s"
357 CLI tools.
358 """
359 missing_fields = []
360 mems = {}
361 with open_binary(f"{get_procfs_path()}/meminfo") as f:
362 for line in f:
363 fields = line.split()
364 mems[fields[0]] = int(fields[1]) * 1024
365
366 # /proc doc states that the available fields in /proc/meminfo vary
367 # by architecture and compile options, but these 3 values are also
368 # returned by sysinfo(2); as such we assume they are always there.
369 total = mems[b'MemTotal:']
370 free = mems[b'MemFree:']
371 try:
372 buffers = mems[b'Buffers:']
373 except KeyError:
374 # https://github.com/giampaolo/psutil/issues/1010
375 buffers = 0
376 missing_fields.append('buffers')
377 try:
378 cached = mems[b"Cached:"]
379 except KeyError:
380 cached = 0
381 missing_fields.append('cached')
382 else:
383 # "free" cmdline utility sums reclaimable to cached.
384 # Older versions of procps used to add slab memory instead.
385 # This got changed in:
386 # https://gitlab.com/procps-ng/procps/commit/
387 # 05d751c4f076a2f0118b914c5e51cfbb4762ad8e
388 cached += mems.get(b"SReclaimable:", 0) # since kernel 2.6.19
389
390 try:
391 shared = mems[b'Shmem:'] # since kernel 2.6.32
392 except KeyError:
393 try:
394 shared = mems[b'MemShared:'] # kernels 2.4
395 except KeyError:
396 shared = 0
397 missing_fields.append('shared')
398
399 try:
400 active = mems[b"Active:"]
401 except KeyError:
402 active = 0
403 missing_fields.append('active')
404
405 try:
406 inactive = mems[b"Inactive:"]
407 except KeyError:
408 try:
409 inactive = (
410 mems[b"Inact_dirty:"]
411 + mems[b"Inact_clean:"]
412 + mems[b"Inact_laundry:"]
413 )
414 except KeyError:
415 inactive = 0
416 missing_fields.append('inactive')
417
418 try:
419 slab = mems[b"Slab:"]
420 except KeyError:
421 slab = 0
422
423 used = total - free - cached - buffers
424 if used < 0:
425 # May be symptomatic of running within a LCX container where such
426 # values will be dramatically distorted over those of the host.
427 used = total - free
428
429 # - starting from 4.4.0 we match free's "available" column.
430 # Before 4.4.0 we calculated it as (free + buffers + cached)
431 # which matched htop.
432 # - free and htop available memory differs as per:
433 # http://askubuntu.com/a/369589
434 # http://unix.stackexchange.com/a/65852/168884
435 # - MemAvailable has been introduced in kernel 3.14
436 try:
437 avail = mems[b'MemAvailable:']
438 except KeyError:
439 avail = calculate_avail_vmem(mems)
440 else:
441 if avail == 0:
442 # Yes, it can happen (probably a kernel bug):
443 # https://github.com/giampaolo/psutil/issues/1915
444 # In this case "free" CLI tool makes an estimate. We do the same,
445 # and it matches "free" CLI tool.
446 avail = calculate_avail_vmem(mems)
447
448 if avail < 0:
449 avail = 0
450 missing_fields.append('available')
451 elif avail > total:
452 # If avail is greater than total or our calculation overflows,
453 # that's symptomatic of running within a LCX container where such
454 # values will be dramatically distorted over those of the host.
455 # https://gitlab.com/procps-ng/procps/blob/
456 # 24fd2605c51fccc375ab0287cec33aa767f06718/proc/sysinfo.c#L764
457 avail = free
458
459 percent = usage_percent((total - avail), total, round_=1)
460
461 # Warn about missing metrics which are set to 0.
462 if missing_fields:
463 msg = "{} memory stats couldn't be determined and {} set to 0".format(
464 ", ".join(missing_fields),
465 "was" if len(missing_fields) == 1 else "were",
466 )
467 warnings.warn(msg, RuntimeWarning, stacklevel=2)
468
469 return svmem(
470 total,
471 avail,
472 percent,
473 used,
474 free,
475 active,
476 inactive,
477 buffers,
478 cached,
479 shared,
480 slab,
481 )
482
483
484def swap_memory():
485 """Return swap memory metrics."""
486 mems = {}
487 with open_binary(f"{get_procfs_path()}/meminfo") as f:
488 for line in f:
489 fields = line.split()
490 mems[fields[0]] = int(fields[1]) * 1024
491 # We prefer /proc/meminfo over sysinfo() syscall so that
492 # psutil.PROCFS_PATH can be used in order to allow retrieval
493 # for linux containers, see:
494 # https://github.com/giampaolo/psutil/issues/1015
495 try:
496 total = mems[b'SwapTotal:']
497 free = mems[b'SwapFree:']
498 except KeyError:
499 _, _, _, _, total, free, unit_multiplier = cext.linux_sysinfo()
500 total *= unit_multiplier
501 free *= unit_multiplier
502
503 used = total - free
504 percent = usage_percent(used, total, round_=1)
505 # get pgin/pgouts
506 try:
507 f = open_binary(f"{get_procfs_path()}/vmstat")
508 except OSError as err:
509 # see https://github.com/giampaolo/psutil/issues/722
510 msg = (
511 "'sin' and 'sout' swap memory stats couldn't "
512 f"be determined and were set to 0 ({err})"
513 )
514 warnings.warn(msg, RuntimeWarning, stacklevel=2)
515 sin = sout = 0
516 else:
517 with f:
518 sin = sout = None
519 for line in f:
520 # values are expressed in 4 kilo bytes, we want
521 # bytes instead
522 if line.startswith(b'pswpin'):
523 sin = int(line.split(b' ')[1]) * 4 * 1024
524 elif line.startswith(b'pswpout'):
525 sout = int(line.split(b' ')[1]) * 4 * 1024
526 if sin is not None and sout is not None:
527 break
528 else:
529 # we might get here when dealing with exotic Linux
530 # flavors, see:
531 # https://github.com/giampaolo/psutil/issues/313
532 msg = "'sin' and 'sout' swap memory stats couldn't "
533 msg += "be determined and were set to 0"
534 warnings.warn(msg, RuntimeWarning, stacklevel=2)
535 sin = sout = 0
536 return _common.sswap(total, used, free, percent, sin, sout)
537
538
539# =====================================================================
540# --- CPU
541# =====================================================================
542
543
544def cpu_times():
545 """Return a named tuple representing the following system-wide
546 CPU times:
547 (user, nice, system, idle, iowait, irq, softirq [steal, [guest,
548 [guest_nice]]])
549 Last 3 fields may not be available on all Linux kernel versions.
550 """
551 procfs_path = get_procfs_path()
552 set_scputimes_ntuple(procfs_path)
553 with open_binary(f"{procfs_path}/stat") as f:
554 values = f.readline().split()
555 fields = values[1 : len(scputimes._fields) + 1]
556 fields = [float(x) / CLOCK_TICKS for x in fields]
557 return scputimes(*fields)
558
559
560def per_cpu_times():
561 """Return a list of namedtuple representing the CPU times
562 for every CPU available on the system.
563 """
564 procfs_path = get_procfs_path()
565 set_scputimes_ntuple(procfs_path)
566 cpus = []
567 with open_binary(f"{procfs_path}/stat") as f:
568 # get rid of the first line which refers to system wide CPU stats
569 f.readline()
570 for line in f:
571 if line.startswith(b'cpu'):
572 values = line.split()
573 fields = values[1 : len(scputimes._fields) + 1]
574 fields = [float(x) / CLOCK_TICKS for x in fields]
575 entry = scputimes(*fields)
576 cpus.append(entry)
577 return cpus
578
579
580def cpu_count_logical():
581 """Return the number of logical CPUs in the system."""
582 try:
583 return os.sysconf("SC_NPROCESSORS_ONLN")
584 except ValueError:
585 # as a second fallback we try to parse /proc/cpuinfo
586 num = 0
587 with open_binary(f"{get_procfs_path()}/cpuinfo") as f:
588 for line in f:
589 if line.lower().startswith(b'processor'):
590 num += 1
591
592 # unknown format (e.g. amrel/sparc architectures), see:
593 # https://github.com/giampaolo/psutil/issues/200
594 # try to parse /proc/stat as a last resort
595 if num == 0:
596 search = re.compile(r'cpu\d')
597 with open_text(f"{get_procfs_path()}/stat") as f:
598 for line in f:
599 line = line.split(' ')[0]
600 if search.match(line):
601 num += 1
602
603 if num == 0:
604 # mimic os.cpu_count()
605 return None
606 return num
607
608
609def cpu_count_cores():
610 """Return the number of CPU cores in the system."""
611 # Method #1
612 ls = set()
613 # These 2 files are the same but */core_cpus_list is newer while
614 # */thread_siblings_list is deprecated and may disappear in the future.
615 # https://www.kernel.org/doc/Documentation/admin-guide/cputopology.rst
616 # https://github.com/giampaolo/psutil/pull/1727#issuecomment-707624964
617 # https://lkml.org/lkml/2019/2/26/41
618 p1 = "/sys/devices/system/cpu/cpu[0-9]*/topology/core_cpus_list"
619 p2 = "/sys/devices/system/cpu/cpu[0-9]*/topology/thread_siblings_list"
620 for path in glob.glob(p1) or glob.glob(p2):
621 with open_binary(path) as f:
622 ls.add(f.read().strip())
623 result = len(ls)
624 if result != 0:
625 return result
626
627 # Method #2
628 mapping = {}
629 current_info = {}
630 with open_binary(f"{get_procfs_path()}/cpuinfo") as f:
631 for line in f:
632 line = line.strip().lower()
633 if not line:
634 # new section
635 try:
636 mapping[current_info[b'physical id']] = current_info[
637 b'cpu cores'
638 ]
639 except KeyError:
640 pass
641 current_info = {}
642 elif line.startswith((b'physical id', b'cpu cores')):
643 # ongoing section
644 key, value = line.split(b'\t:', 1)
645 current_info[key] = int(value)
646
647 result = sum(mapping.values())
648 return result or None # mimic os.cpu_count()
649
650
651def cpu_stats():
652 """Return various CPU stats as a named tuple."""
653 with open_binary(f"{get_procfs_path()}/stat") as f:
654 ctx_switches = None
655 interrupts = None
656 soft_interrupts = None
657 for line in f:
658 if line.startswith(b'ctxt'):
659 ctx_switches = int(line.split()[1])
660 elif line.startswith(b'intr'):
661 interrupts = int(line.split()[1])
662 elif line.startswith(b'softirq'):
663 soft_interrupts = int(line.split()[1])
664 if (
665 ctx_switches is not None
666 and soft_interrupts is not None
667 and interrupts is not None
668 ):
669 break
670 syscalls = 0
671 return _common.scpustats(
672 ctx_switches, interrupts, soft_interrupts, syscalls
673 )
674
675
676def _cpu_get_cpuinfo_freq():
677 """Return current CPU frequency from cpuinfo if available."""
678 with open_binary(f"{get_procfs_path()}/cpuinfo") as f:
679 return [
680 float(line.split(b':', 1)[1])
681 for line in f
682 if line.lower().startswith(b'cpu mhz')
683 ]
684
685
686if os.path.exists("/sys/devices/system/cpu/cpufreq/policy0") or os.path.exists(
687 "/sys/devices/system/cpu/cpu0/cpufreq"
688):
689
690 def cpu_freq():
691 """Return frequency metrics for all CPUs.
692 Contrarily to other OSes, Linux updates these values in
693 real-time.
694 """
695 cpuinfo_freqs = _cpu_get_cpuinfo_freq()
696 paths = glob.glob(
697 "/sys/devices/system/cpu/cpufreq/policy[0-9]*"
698 ) or glob.glob("/sys/devices/system/cpu/cpu[0-9]*/cpufreq")
699 paths.sort(key=lambda x: int(re.search(r"[0-9]+", x).group()))
700 ret = []
701 pjoin = os.path.join
702 for i, path in enumerate(paths):
703 if len(paths) == len(cpuinfo_freqs):
704 # take cached value from cpuinfo if available, see:
705 # https://github.com/giampaolo/psutil/issues/1851
706 curr = cpuinfo_freqs[i] * 1000
707 else:
708 curr = bcat(pjoin(path, "scaling_cur_freq"), fallback=None)
709 if curr is None:
710 # Likely an old RedHat, see:
711 # https://github.com/giampaolo/psutil/issues/1071
712 curr = bcat(pjoin(path, "cpuinfo_cur_freq"), fallback=None)
713 if curr is None:
714 online_path = f"/sys/devices/system/cpu/cpu{i}/online"
715 # if cpu core is offline, set to all zeroes
716 if cat(online_path, fallback=None) == "0\n":
717 ret.append(_common.scpufreq(0.0, 0.0, 0.0))
718 continue
719 msg = "can't find current frequency file"
720 raise NotImplementedError(msg)
721 curr = int(curr) / 1000
722 max_ = int(bcat(pjoin(path, "scaling_max_freq"))) / 1000
723 min_ = int(bcat(pjoin(path, "scaling_min_freq"))) / 1000
724 ret.append(_common.scpufreq(curr, min_, max_))
725 return ret
726
727else:
728
729 def cpu_freq():
730 """Alternate implementation using /proc/cpuinfo.
731 min and max frequencies are not available and are set to None.
732 """
733 return [_common.scpufreq(x, 0.0, 0.0) for x in _cpu_get_cpuinfo_freq()]
734
735
736# =====================================================================
737# --- network
738# =====================================================================
739
740
741net_if_addrs = cext_posix.net_if_addrs
742
743
744class _Ipv6UnsupportedError(Exception):
745 pass
746
747
748class NetConnections:
749 """A wrapper on top of /proc/net/* files, retrieving per-process
750 and system-wide open connections (TCP, UDP, UNIX) similarly to
751 "netstat -an".
752
753 Note: in case of UNIX sockets we're only able to determine the
754 local endpoint/path, not the one it's connected to.
755 According to [1] it would be possible but not easily.
756
757 [1] http://serverfault.com/a/417946
758 """
759
760 def __init__(self):
761 # The string represents the basename of the corresponding
762 # /proc/net/{proto_name} file.
763 tcp4 = ("tcp", socket.AF_INET, socket.SOCK_STREAM)
764 tcp6 = ("tcp6", socket.AF_INET6, socket.SOCK_STREAM)
765 udp4 = ("udp", socket.AF_INET, socket.SOCK_DGRAM)
766 udp6 = ("udp6", socket.AF_INET6, socket.SOCK_DGRAM)
767 unix = ("unix", socket.AF_UNIX, None)
768 self.tmap = {
769 "all": (tcp4, tcp6, udp4, udp6, unix),
770 "tcp": (tcp4, tcp6),
771 "tcp4": (tcp4,),
772 "tcp6": (tcp6,),
773 "udp": (udp4, udp6),
774 "udp4": (udp4,),
775 "udp6": (udp6,),
776 "unix": (unix,),
777 "inet": (tcp4, tcp6, udp4, udp6),
778 "inet4": (tcp4, udp4),
779 "inet6": (tcp6, udp6),
780 }
781 self._procfs_path = None
782
783 def get_proc_inodes(self, pid):
784 inodes = defaultdict(list)
785 for fd in os.listdir(f"{self._procfs_path}/{pid}/fd"):
786 try:
787 inode = readlink(f"{self._procfs_path}/{pid}/fd/{fd}")
788 except (FileNotFoundError, ProcessLookupError):
789 # ENOENT == file which is gone in the meantime;
790 # os.stat(f"/proc/{self.pid}") will be done later
791 # to force NSP (if it's the case)
792 continue
793 except OSError as err:
794 if err.errno == errno.EINVAL:
795 # not a link
796 continue
797 if err.errno == errno.ENAMETOOLONG:
798 # file name too long
799 debug(err)
800 continue
801 raise
802 else:
803 if inode.startswith('socket:['):
804 # the process is using a socket
805 inode = inode[8:][:-1]
806 inodes[inode].append((pid, int(fd)))
807 return inodes
808
809 def get_all_inodes(self):
810 inodes = {}
811 for pid in pids():
812 try:
813 inodes.update(self.get_proc_inodes(pid))
814 except (FileNotFoundError, ProcessLookupError, PermissionError):
815 # os.listdir() is gonna raise a lot of access denied
816 # exceptions in case of unprivileged user; that's fine
817 # as we'll just end up returning a connection with PID
818 # and fd set to None anyway.
819 # Both netstat -an and lsof does the same so it's
820 # unlikely we can do any better.
821 # ENOENT just means a PID disappeared on us.
822 continue
823 return inodes
824
825 @staticmethod
826 def decode_address(addr, family):
827 """Accept an "ip:port" address as displayed in /proc/net/*
828 and convert it into a human readable form, like:
829
830 "0500000A:0016" -> ("10.0.0.5", 22)
831 "0000000000000000FFFF00000100007F:9E49" -> ("::ffff:127.0.0.1", 40521)
832
833 The IP address portion is a little or big endian four-byte
834 hexadecimal number; that is, the least significant byte is listed
835 first, so we need to reverse the order of the bytes to convert it
836 to an IP address.
837 The port is represented as a two-byte hexadecimal number.
838
839 Reference:
840 http://linuxdevcenter.com/pub/a/linux/2000/11/16/LinuxAdmin.html
841 """
842 ip, port = addr.split(':')
843 port = int(port, 16)
844 # this usually refers to a local socket in listen mode with
845 # no end-points connected
846 if not port:
847 return ()
848 ip = ip.encode('ascii')
849 if family == socket.AF_INET:
850 # see: https://github.com/giampaolo/psutil/issues/201
851 if LITTLE_ENDIAN:
852 ip = socket.inet_ntop(family, base64.b16decode(ip)[::-1])
853 else:
854 ip = socket.inet_ntop(family, base64.b16decode(ip))
855 else: # IPv6
856 ip = base64.b16decode(ip)
857 try:
858 # see: https://github.com/giampaolo/psutil/issues/201
859 if LITTLE_ENDIAN:
860 ip = socket.inet_ntop(
861 socket.AF_INET6,
862 struct.pack('>4I', *struct.unpack('<4I', ip)),
863 )
864 else:
865 ip = socket.inet_ntop(
866 socket.AF_INET6,
867 struct.pack('<4I', *struct.unpack('<4I', ip)),
868 )
869 except ValueError:
870 # see: https://github.com/giampaolo/psutil/issues/623
871 if not supports_ipv6():
872 raise _Ipv6UnsupportedError from None
873 raise
874 return _common.addr(ip, port)
875
876 @staticmethod
877 def process_inet(file, family, type_, inodes, filter_pid=None):
878 """Parse /proc/net/tcp* and /proc/net/udp* files."""
879 if file.endswith('6') and not os.path.exists(file):
880 # IPv6 not supported
881 return
882 with open_text(file) as f:
883 f.readline() # skip the first line
884 for lineno, line in enumerate(f, 1):
885 try:
886 _, laddr, raddr, status, _, _, _, _, _, inode = (
887 line.split()[:10]
888 )
889 except ValueError:
890 msg = (
891 f"error while parsing {file}; malformed line"
892 f" {lineno} {line!r}"
893 )
894 raise RuntimeError(msg) from None
895 if inode in inodes:
896 # # We assume inet sockets are unique, so we error
897 # # out if there are multiple references to the
898 # # same inode. We won't do this for UNIX sockets.
899 # if len(inodes[inode]) > 1 and family != socket.AF_UNIX:
900 # raise ValueError("ambiguous inode with multiple "
901 # "PIDs references")
902 pid, fd = inodes[inode][0]
903 else:
904 pid, fd = None, -1
905 if filter_pid is not None and filter_pid != pid:
906 continue
907 else:
908 if type_ == socket.SOCK_STREAM:
909 status = TCP_STATUSES[status]
910 else:
911 status = _common.CONN_NONE
912 try:
913 laddr = NetConnections.decode_address(laddr, family)
914 raddr = NetConnections.decode_address(raddr, family)
915 except _Ipv6UnsupportedError:
916 continue
917 yield (fd, family, type_, laddr, raddr, status, pid)
918
919 @staticmethod
920 def process_unix(file, family, inodes, filter_pid=None):
921 """Parse /proc/net/unix files."""
922 with open_text(file) as f:
923 f.readline() # skip the first line
924 for line in f:
925 tokens = line.split()
926 try:
927 _, _, _, _, type_, _, inode = tokens[0:7]
928 except ValueError:
929 if ' ' not in line:
930 # see: https://github.com/giampaolo/psutil/issues/766
931 continue
932 msg = (
933 f"error while parsing {file}; malformed line {line!r}"
934 )
935 raise RuntimeError(msg) # noqa: B904
936 if inode in inodes: # noqa: SIM108
937 # With UNIX sockets we can have a single inode
938 # referencing many file descriptors.
939 pairs = inodes[inode]
940 else:
941 pairs = [(None, -1)]
942 for pid, fd in pairs:
943 if filter_pid is not None and filter_pid != pid:
944 continue
945 else:
946 path = tokens[-1] if len(tokens) == 8 else ''
947 type_ = _common.socktype_to_enum(int(type_))
948 # XXX: determining the remote endpoint of a
949 # UNIX socket on Linux is not possible, see:
950 # https://serverfault.com/questions/252723/
951 raddr = ""
952 status = _common.CONN_NONE
953 yield (fd, family, type_, path, raddr, status, pid)
954
955 def retrieve(self, kind, pid=None):
956 self._procfs_path = get_procfs_path()
957 if pid is not None:
958 inodes = self.get_proc_inodes(pid)
959 if not inodes:
960 # no connections for this process
961 return []
962 else:
963 inodes = self.get_all_inodes()
964 ret = set()
965 for proto_name, family, type_ in self.tmap[kind]:
966 path = f"{self._procfs_path}/net/{proto_name}"
967 if family in {socket.AF_INET, socket.AF_INET6}:
968 ls = self.process_inet(
969 path, family, type_, inodes, filter_pid=pid
970 )
971 else:
972 ls = self.process_unix(path, family, inodes, filter_pid=pid)
973 for fd, family, type_, laddr, raddr, status, bound_pid in ls:
974 if pid:
975 conn = _common.pconn(
976 fd, family, type_, laddr, raddr, status
977 )
978 else:
979 conn = _common.sconn(
980 fd, family, type_, laddr, raddr, status, bound_pid
981 )
982 ret.add(conn)
983 return list(ret)
984
985
986_net_connections = NetConnections()
987
988
989def net_connections(kind='inet'):
990 """Return system-wide open connections."""
991 return _net_connections.retrieve(kind)
992
993
994def net_io_counters():
995 """Return network I/O statistics for every network interface
996 installed on the system as a dict of raw tuples.
997 """
998 with open_text(f"{get_procfs_path()}/net/dev") as f:
999 lines = f.readlines()
1000 retdict = {}
1001 for line in lines[2:]:
1002 colon = line.rfind(':')
1003 assert colon > 0, repr(line)
1004 name = line[:colon].strip()
1005 fields = line[colon + 1 :].strip().split()
1006
1007 (
1008 # in
1009 bytes_recv,
1010 packets_recv,
1011 errin,
1012 dropin,
1013 _fifoin, # unused
1014 _framein, # unused
1015 _compressedin, # unused
1016 _multicastin, # unused
1017 # out
1018 bytes_sent,
1019 packets_sent,
1020 errout,
1021 dropout,
1022 _fifoout, # unused
1023 _collisionsout, # unused
1024 _carrierout, # unused
1025 _compressedout, # unused
1026 ) = map(int, fields)
1027
1028 retdict[name] = (
1029 bytes_sent,
1030 bytes_recv,
1031 packets_sent,
1032 packets_recv,
1033 errin,
1034 errout,
1035 dropin,
1036 dropout,
1037 )
1038 return retdict
1039
1040
1041def net_if_stats():
1042 """Get NIC stats (isup, duplex, speed, mtu)."""
1043 duplex_map = {
1044 cext.DUPLEX_FULL: NIC_DUPLEX_FULL,
1045 cext.DUPLEX_HALF: NIC_DUPLEX_HALF,
1046 cext.DUPLEX_UNKNOWN: NIC_DUPLEX_UNKNOWN,
1047 }
1048 names = net_io_counters().keys()
1049 ret = {}
1050 for name in names:
1051 try:
1052 mtu = cext_posix.net_if_mtu(name)
1053 flags = cext_posix.net_if_flags(name)
1054 duplex, speed = cext.net_if_duplex_speed(name)
1055 except OSError as err:
1056 # https://github.com/giampaolo/psutil/issues/1279
1057 if err.errno != errno.ENODEV:
1058 raise
1059 debug(err)
1060 else:
1061 output_flags = ','.join(flags)
1062 isup = 'running' in flags
1063 ret[name] = _common.snicstats(
1064 isup, duplex_map[duplex], speed, mtu, output_flags
1065 )
1066 return ret
1067
1068
1069# =====================================================================
1070# --- disks
1071# =====================================================================
1072
1073
1074disk_usage = _psposix.disk_usage
1075
1076
1077def disk_io_counters(perdisk=False):
1078 """Return disk I/O statistics for every disk installed on the
1079 system as a dict of raw tuples.
1080 """
1081
1082 def read_procfs():
1083 # OK, this is a bit confusing. The format of /proc/diskstats can
1084 # have 3 variations.
1085 # On Linux 2.4 each line has always 15 fields, e.g.:
1086 # "3 0 8 hda 8 8 8 8 8 8 8 8 8 8 8"
1087 # On Linux 2.6+ each line *usually* has 14 fields, and the disk
1088 # name is in another position, like this:
1089 # "3 0 hda 8 8 8 8 8 8 8 8 8 8 8"
1090 # ...unless (Linux 2.6) the line refers to a partition instead
1091 # of a disk, in which case the line has less fields (7):
1092 # "3 1 hda1 8 8 8 8"
1093 # 4.18+ has 4 fields added:
1094 # "3 0 hda 8 8 8 8 8 8 8 8 8 8 8 0 0 0 0"
1095 # 5.5 has 2 more fields.
1096 # See:
1097 # https://www.kernel.org/doc/Documentation/iostats.txt
1098 # https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats
1099 with open_text(f"{get_procfs_path()}/diskstats") as f:
1100 lines = f.readlines()
1101 for line in lines:
1102 fields = line.split()
1103 flen = len(fields)
1104 # fmt: off
1105 if flen == 15:
1106 # Linux 2.4
1107 name = fields[3]
1108 reads = int(fields[2])
1109 (reads_merged, rbytes, rtime, writes, writes_merged,
1110 wbytes, wtime, _, busy_time, _) = map(int, fields[4:14])
1111 elif flen == 14 or flen >= 18:
1112 # Linux 2.6+, line referring to a disk
1113 name = fields[2]
1114 (reads, reads_merged, rbytes, rtime, writes, writes_merged,
1115 wbytes, wtime, _, busy_time, _) = map(int, fields[3:14])
1116 elif flen == 7:
1117 # Linux 2.6+, line referring to a partition
1118 name = fields[2]
1119 reads, rbytes, writes, wbytes = map(int, fields[3:])
1120 rtime = wtime = reads_merged = writes_merged = busy_time = 0
1121 else:
1122 msg = f"not sure how to interpret line {line!r}"
1123 raise ValueError(msg)
1124 yield (name, reads, writes, rbytes, wbytes, rtime, wtime,
1125 reads_merged, writes_merged, busy_time)
1126 # fmt: on
1127
1128 def read_sysfs():
1129 for block in os.listdir('/sys/block'):
1130 for root, _, files in os.walk(os.path.join('/sys/block', block)):
1131 if 'stat' not in files:
1132 continue
1133 with open_text(os.path.join(root, 'stat')) as f:
1134 fields = f.read().strip().split()
1135 name = os.path.basename(root)
1136 # fmt: off
1137 (reads, reads_merged, rbytes, rtime, writes, writes_merged,
1138 wbytes, wtime, _, busy_time) = map(int, fields[:10])
1139 yield (name, reads, writes, rbytes, wbytes, rtime,
1140 wtime, reads_merged, writes_merged, busy_time)
1141 # fmt: on
1142
1143 if os.path.exists(f"{get_procfs_path()}/diskstats"):
1144 gen = read_procfs()
1145 elif os.path.exists('/sys/block'):
1146 gen = read_sysfs()
1147 else:
1148 msg = (
1149 f"{get_procfs_path()}/diskstats nor /sys/block are available on"
1150 " this system"
1151 )
1152 raise NotImplementedError(msg)
1153
1154 retdict = {}
1155 for entry in gen:
1156 # fmt: off
1157 (name, reads, writes, rbytes, wbytes, rtime, wtime, reads_merged,
1158 writes_merged, busy_time) = entry
1159 if not perdisk and not is_storage_device(name):
1160 # perdisk=False means we want to calculate totals so we skip
1161 # partitions (e.g. 'sda1', 'nvme0n1p1') and only include
1162 # base disk devices (e.g. 'sda', 'nvme0n1'). Base disks
1163 # include a total of all their partitions + some extra size
1164 # of their own:
1165 # $ cat /proc/diskstats
1166 # 259 0 sda 10485760 ...
1167 # 259 1 sda1 5186039 ...
1168 # 259 1 sda2 5082039 ...
1169 # See:
1170 # https://github.com/giampaolo/psutil/pull/1313
1171 continue
1172
1173 rbytes *= DISK_SECTOR_SIZE
1174 wbytes *= DISK_SECTOR_SIZE
1175 retdict[name] = (reads, writes, rbytes, wbytes, rtime, wtime,
1176 reads_merged, writes_merged, busy_time)
1177 # fmt: on
1178
1179 return retdict
1180
1181
1182class RootFsDeviceFinder:
1183 """disk_partitions() may return partitions with device == "/dev/root"
1184 or "rootfs". This container class uses different strategies to try to
1185 obtain the real device path. Resources:
1186 https://bootlin.com/blog/find-root-device/
1187 https://www.systutorials.com/how-to-find-the-disk-where-root-is-on-in-bash-on-linux/.
1188 """
1189
1190 __slots__ = ['major', 'minor']
1191
1192 def __init__(self):
1193 dev = os.stat("/").st_dev
1194 self.major = os.major(dev)
1195 self.minor = os.minor(dev)
1196
1197 def ask_proc_partitions(self):
1198 with open_text(f"{get_procfs_path()}/partitions") as f:
1199 for line in f.readlines()[2:]:
1200 fields = line.split()
1201 if len(fields) < 4: # just for extra safety
1202 continue
1203 major = int(fields[0]) if fields[0].isdigit() else None
1204 minor = int(fields[1]) if fields[1].isdigit() else None
1205 name = fields[3]
1206 if major == self.major and minor == self.minor:
1207 if name: # just for extra safety
1208 return f"/dev/{name}"
1209
1210 def ask_sys_dev_block(self):
1211 path = f"/sys/dev/block/{self.major}:{self.minor}/uevent"
1212 with open_text(path) as f:
1213 for line in f:
1214 if line.startswith("DEVNAME="):
1215 name = line.strip().rpartition("DEVNAME=")[2]
1216 if name: # just for extra safety
1217 return f"/dev/{name}"
1218
1219 def ask_sys_class_block(self):
1220 needle = f"{self.major}:{self.minor}"
1221 files = glob.iglob("/sys/class/block/*/dev")
1222 for file in files:
1223 try:
1224 f = open_text(file)
1225 except FileNotFoundError: # race condition
1226 continue
1227 else:
1228 with f:
1229 data = f.read().strip()
1230 if data == needle:
1231 name = os.path.basename(os.path.dirname(file))
1232 return f"/dev/{name}"
1233
1234 def find(self):
1235 path = None
1236 if path is None:
1237 try:
1238 path = self.ask_proc_partitions()
1239 except OSError as err:
1240 debug(err)
1241 if path is None:
1242 try:
1243 path = self.ask_sys_dev_block()
1244 except OSError as err:
1245 debug(err)
1246 if path is None:
1247 try:
1248 path = self.ask_sys_class_block()
1249 except OSError as err:
1250 debug(err)
1251 # We use exists() because the "/dev/*" part of the path is hard
1252 # coded, so we want to be sure.
1253 if path is not None and os.path.exists(path):
1254 return path
1255
1256
1257def disk_partitions(all=False):
1258 """Return mounted disk partitions as a list of namedtuples."""
1259 fstypes = set()
1260 procfs_path = get_procfs_path()
1261 if not all:
1262 with open_text(f"{procfs_path}/filesystems") as f:
1263 for line in f:
1264 line = line.strip()
1265 if not line.startswith("nodev"):
1266 fstypes.add(line.strip())
1267 else:
1268 # ignore all lines starting with "nodev" except "nodev zfs"
1269 fstype = line.split("\t")[1]
1270 if fstype == "zfs":
1271 fstypes.add("zfs")
1272
1273 # See: https://github.com/giampaolo/psutil/issues/1307
1274 if procfs_path == "/proc" and os.path.isfile('/etc/mtab'):
1275 mounts_path = os.path.realpath("/etc/mtab")
1276 else:
1277 mounts_path = os.path.realpath(f"{procfs_path}/self/mounts")
1278
1279 retlist = []
1280 partitions = cext.disk_partitions(mounts_path)
1281 for partition in partitions:
1282 device, mountpoint, fstype, opts = partition
1283 if device == 'none':
1284 device = ''
1285 if device in {"/dev/root", "rootfs"}:
1286 device = RootFsDeviceFinder().find() or device
1287 if not all:
1288 if not device or fstype not in fstypes:
1289 continue
1290 ntuple = _common.sdiskpart(device, mountpoint, fstype, opts)
1291 retlist.append(ntuple)
1292
1293 return retlist
1294
1295
1296# =====================================================================
1297# --- sensors
1298# =====================================================================
1299
1300
1301def sensors_temperatures():
1302 """Return hardware (CPU and others) temperatures as a dict
1303 including hardware name, label, current, max and critical
1304 temperatures.
1305
1306 Implementation notes:
1307 - /sys/class/hwmon looks like the most recent interface to
1308 retrieve this info, and this implementation relies on it
1309 only (old distros will probably use something else)
1310 - lm-sensors on Ubuntu 16.04 relies on /sys/class/hwmon
1311 - /sys/class/thermal/thermal_zone* is another one but it's more
1312 difficult to parse
1313 """
1314 ret = collections.defaultdict(list)
1315 basenames = glob.glob('/sys/class/hwmon/hwmon*/temp*_*')
1316 # CentOS has an intermediate /device directory:
1317 # https://github.com/giampaolo/psutil/issues/971
1318 # https://github.com/nicolargo/glances/issues/1060
1319 basenames.extend(glob.glob('/sys/class/hwmon/hwmon*/device/temp*_*'))
1320 basenames = sorted({x.split('_')[0] for x in basenames})
1321
1322 # Only add the coretemp hwmon entries if they're not already in
1323 # /sys/class/hwmon/
1324 # https://github.com/giampaolo/psutil/issues/1708
1325 # https://github.com/giampaolo/psutil/pull/1648
1326 basenames2 = glob.glob(
1327 '/sys/devices/platform/coretemp.*/hwmon/hwmon*/temp*_*'
1328 )
1329 repl = re.compile(r"/sys/devices/platform/coretemp.*/hwmon/")
1330 for name in basenames2:
1331 altname = repl.sub('/sys/class/hwmon/', name)
1332 if altname not in basenames:
1333 basenames.append(name)
1334
1335 for base in basenames:
1336 try:
1337 path = base + '_input'
1338 current = float(bcat(path)) / 1000.0
1339 path = os.path.join(os.path.dirname(base), 'name')
1340 unit_name = cat(path).strip()
1341 except (OSError, ValueError):
1342 # A lot of things can go wrong here, so let's just skip the
1343 # whole entry. Sure thing is Linux's /sys/class/hwmon really
1344 # is a stinky broken mess.
1345 # https://github.com/giampaolo/psutil/issues/1009
1346 # https://github.com/giampaolo/psutil/issues/1101
1347 # https://github.com/giampaolo/psutil/issues/1129
1348 # https://github.com/giampaolo/psutil/issues/1245
1349 # https://github.com/giampaolo/psutil/issues/1323
1350 continue
1351
1352 high = bcat(base + '_max', fallback=None)
1353 critical = bcat(base + '_crit', fallback=None)
1354 label = cat(base + '_label', fallback='').strip()
1355
1356 if high is not None:
1357 try:
1358 high = float(high) / 1000.0
1359 except ValueError:
1360 high = None
1361 if critical is not None:
1362 try:
1363 critical = float(critical) / 1000.0
1364 except ValueError:
1365 critical = None
1366
1367 ret[unit_name].append((label, current, high, critical))
1368
1369 # Indication that no sensors were detected in /sys/class/hwmon/
1370 if not basenames:
1371 basenames = glob.glob('/sys/class/thermal/thermal_zone*')
1372 basenames = sorted(set(basenames))
1373
1374 for base in basenames:
1375 try:
1376 path = os.path.join(base, 'temp')
1377 current = float(bcat(path)) / 1000.0
1378 path = os.path.join(base, 'type')
1379 unit_name = cat(path).strip()
1380 except (OSError, ValueError) as err:
1381 debug(err)
1382 continue
1383
1384 trip_paths = glob.glob(base + '/trip_point*')
1385 trip_points = {
1386 '_'.join(os.path.basename(p).split('_')[0:3])
1387 for p in trip_paths
1388 }
1389 critical = None
1390 high = None
1391 for trip_point in trip_points:
1392 path = os.path.join(base, trip_point + "_type")
1393 trip_type = cat(path, fallback='').strip()
1394 if trip_type == 'critical':
1395 critical = bcat(
1396 os.path.join(base, trip_point + "_temp"), fallback=None
1397 )
1398 elif trip_type == 'high':
1399 high = bcat(
1400 os.path.join(base, trip_point + "_temp"), fallback=None
1401 )
1402
1403 if high is not None:
1404 try:
1405 high = float(high) / 1000.0
1406 except ValueError:
1407 high = None
1408 if critical is not None:
1409 try:
1410 critical = float(critical) / 1000.0
1411 except ValueError:
1412 critical = None
1413
1414 ret[unit_name].append(('', current, high, critical))
1415
1416 return dict(ret)
1417
1418
1419def sensors_fans():
1420 """Return hardware fans info (for CPU and other peripherals) as a
1421 dict including hardware label and current speed.
1422
1423 Implementation notes:
1424 - /sys/class/hwmon looks like the most recent interface to
1425 retrieve this info, and this implementation relies on it
1426 only (old distros will probably use something else)
1427 - lm-sensors on Ubuntu 16.04 relies on /sys/class/hwmon
1428 """
1429 ret = collections.defaultdict(list)
1430 basenames = glob.glob('/sys/class/hwmon/hwmon*/fan*_*')
1431 if not basenames:
1432 # CentOS has an intermediate /device directory:
1433 # https://github.com/giampaolo/psutil/issues/971
1434 basenames = glob.glob('/sys/class/hwmon/hwmon*/device/fan*_*')
1435
1436 basenames = sorted({x.split("_")[0] for x in basenames})
1437 for base in basenames:
1438 try:
1439 current = int(bcat(base + '_input'))
1440 except OSError as err:
1441 debug(err)
1442 continue
1443 unit_name = cat(os.path.join(os.path.dirname(base), 'name')).strip()
1444 label = cat(base + '_label', fallback='').strip()
1445 ret[unit_name].append(_common.sfan(label, current))
1446
1447 return dict(ret)
1448
1449
1450def sensors_battery():
1451 """Return battery information.
1452 Implementation note: it appears /sys/class/power_supply/BAT0/
1453 directory structure may vary and provide files with the same
1454 meaning but under different names, see:
1455 https://github.com/giampaolo/psutil/issues/966.
1456 """
1457 null = object()
1458
1459 def multi_bcat(*paths):
1460 """Attempt to read the content of multiple files which may
1461 not exist. If none of them exist return None.
1462 """
1463 for path in paths:
1464 ret = bcat(path, fallback=null)
1465 if ret != null:
1466 try:
1467 return int(ret)
1468 except ValueError:
1469 return ret.strip()
1470 return None
1471
1472 bats = [
1473 x
1474 for x in os.listdir(POWER_SUPPLY_PATH)
1475 if x.startswith('BAT') or 'battery' in x.lower()
1476 ]
1477 if not bats:
1478 return None
1479 # Get the first available battery. Usually this is "BAT0", except
1480 # some rare exceptions:
1481 # https://github.com/giampaolo/psutil/issues/1238
1482 root = os.path.join(POWER_SUPPLY_PATH, min(bats))
1483
1484 # Base metrics.
1485 energy_now = multi_bcat(root + "/energy_now", root + "/charge_now")
1486 power_now = multi_bcat(root + "/power_now", root + "/current_now")
1487 energy_full = multi_bcat(root + "/energy_full", root + "/charge_full")
1488 time_to_empty = multi_bcat(root + "/time_to_empty_now")
1489
1490 # Percent. If we have energy_full the percentage will be more
1491 # accurate compared to reading /capacity file (float vs. int).
1492 if energy_full is not None and energy_now is not None:
1493 try:
1494 percent = 100.0 * energy_now / energy_full
1495 except ZeroDivisionError:
1496 percent = 0.0
1497 else:
1498 percent = int(cat(root + "/capacity", fallback=-1))
1499 if percent == -1:
1500 return None
1501
1502 # Is AC power cable plugged in?
1503 # Note: AC0 is not always available and sometimes (e.g. CentOS7)
1504 # it's called "AC".
1505 power_plugged = None
1506 online = multi_bcat(
1507 os.path.join(POWER_SUPPLY_PATH, "AC0/online"),
1508 os.path.join(POWER_SUPPLY_PATH, "AC/online"),
1509 )
1510 if online is not None:
1511 power_plugged = online == 1
1512 else:
1513 status = cat(root + "/status", fallback="").strip().lower()
1514 if status == "discharging":
1515 power_plugged = False
1516 elif status in {"charging", "full"}:
1517 power_plugged = True
1518
1519 # Seconds left.
1520 # Note to self: we may also calculate the charging ETA as per:
1521 # https://github.com/thialfihar/dotfiles/blob/
1522 # 013937745fd9050c30146290e8f963d65c0179e6/bin/battery.py#L55
1523 if power_plugged:
1524 secsleft = _common.POWER_TIME_UNLIMITED
1525 elif energy_now is not None and power_now is not None:
1526 try:
1527 secsleft = int(energy_now / power_now * 3600)
1528 except ZeroDivisionError:
1529 secsleft = _common.POWER_TIME_UNKNOWN
1530 elif time_to_empty is not None:
1531 secsleft = int(time_to_empty * 60)
1532 if secsleft < 0:
1533 secsleft = _common.POWER_TIME_UNKNOWN
1534 else:
1535 secsleft = _common.POWER_TIME_UNKNOWN
1536
1537 return _common.sbattery(percent, secsleft, power_plugged)
1538
1539
1540# =====================================================================
1541# --- other system functions
1542# =====================================================================
1543
1544
1545def users():
1546 """Return currently connected users as a list of namedtuples."""
1547 retlist = []
1548 rawlist = cext.users()
1549 for item in rawlist:
1550 user, tty, hostname, tstamp, pid = item
1551 nt = _common.suser(user, tty or None, hostname, tstamp, pid)
1552 retlist.append(nt)
1553 return retlist
1554
1555
1556def boot_time():
1557 """Return the system boot time expressed in seconds since the epoch."""
1558 path = f"{get_procfs_path()}/stat"
1559 with open_binary(path) as f:
1560 for line in f:
1561 if line.startswith(b'btime'):
1562 return float(line.strip().split()[1])
1563 msg = f"line 'btime' not found in {path}"
1564 raise RuntimeError(msg)
1565
1566
1567# =====================================================================
1568# --- processes
1569# =====================================================================
1570
1571
1572def pids():
1573 """Returns a list of PIDs currently running on the system."""
1574 path = get_procfs_path().encode(ENCODING)
1575 return [int(x) for x in os.listdir(path) if x.isdigit()]
1576
1577
1578def pid_exists(pid):
1579 """Check for the existence of a unix PID. Linux TIDs are not
1580 supported (always return False).
1581 """
1582 if not _psposix.pid_exists(pid):
1583 return False
1584 else:
1585 # Linux's apparently does not distinguish between PIDs and TIDs
1586 # (thread IDs).
1587 # listdir("/proc") won't show any TID (only PIDs) but
1588 # os.stat("/proc/{tid}") will succeed if {tid} exists.
1589 # os.kill() can also be passed a TID. This is quite confusing.
1590 # In here we want to enforce this distinction and support PIDs
1591 # only, see:
1592 # https://github.com/giampaolo/psutil/issues/687
1593 try:
1594 # Note: already checked that this is faster than using a
1595 # regular expr. Also (a lot) faster than doing
1596 # 'return pid in pids()'
1597 path = f"{get_procfs_path()}/{pid}/status"
1598 with open_binary(path) as f:
1599 for line in f:
1600 if line.startswith(b"Tgid:"):
1601 tgid = int(line.split()[1])
1602 # If tgid and pid are the same then we're
1603 # dealing with a process PID.
1604 return tgid == pid
1605 msg = f"'Tgid' line not found in {path}"
1606 raise ValueError(msg)
1607 except (OSError, ValueError):
1608 return pid in pids()
1609
1610
1611def ppid_map():
1612 """Obtain a {pid: ppid, ...} dict for all running processes in
1613 one shot. Used to speed up Process.children().
1614 """
1615 ret = {}
1616 procfs_path = get_procfs_path()
1617 for pid in pids():
1618 try:
1619 with open_binary(f"{procfs_path}/{pid}/stat") as f:
1620 data = f.read()
1621 except (FileNotFoundError, ProcessLookupError):
1622 pass
1623 except PermissionError as err:
1624 raise AccessDenied(pid) from err
1625 else:
1626 rpar = data.rfind(b')')
1627 dset = data[rpar + 2 :].split()
1628 ppid = int(dset[1])
1629 ret[pid] = ppid
1630 return ret
1631
1632
1633def wrap_exceptions(fun):
1634 """Decorator which translates bare OSError and OSError exceptions
1635 into NoSuchProcess and AccessDenied.
1636 """
1637
1638 @functools.wraps(fun)
1639 def wrapper(self, *args, **kwargs):
1640 pid, name = self.pid, self._name
1641 try:
1642 return fun(self, *args, **kwargs)
1643 except PermissionError as err:
1644 raise AccessDenied(pid, name) from err
1645 except ProcessLookupError as err:
1646 self._raise_if_zombie()
1647 raise NoSuchProcess(pid, name) from err
1648 except FileNotFoundError as err:
1649 self._raise_if_zombie()
1650 # /proc/PID directory may still exist, but the files within
1651 # it may not, indicating the process is gone, see:
1652 # https://github.com/giampaolo/psutil/issues/2418
1653 if not os.path.exists(f"{self._procfs_path}/{pid}/stat"):
1654 raise NoSuchProcess(pid, name) from err
1655 raise
1656
1657 return wrapper
1658
1659
1660class Process:
1661 """Linux process implementation."""
1662
1663 __slots__ = [
1664 "_cache",
1665 "_ctime",
1666 "_name",
1667 "_ppid",
1668 "_procfs_path",
1669 "pid",
1670 ]
1671
1672 def __init__(self, pid):
1673 self.pid = pid
1674 self._name = None
1675 self._ppid = None
1676 self._ctime = None
1677 self._procfs_path = get_procfs_path()
1678
1679 def _is_zombie(self):
1680 # Note: most of the times Linux is able to return info about the
1681 # process even if it's a zombie, and /proc/{pid} will exist.
1682 # There are some exceptions though, like exe(), cmdline() and
1683 # memory_maps(). In these cases /proc/{pid}/{file} exists but
1684 # it's empty. Instead of returning a "null" value we'll raise an
1685 # exception.
1686 try:
1687 data = bcat(f"{self._procfs_path}/{self.pid}/stat")
1688 except OSError:
1689 return False
1690 else:
1691 rpar = data.rfind(b')')
1692 status = data[rpar + 2 : rpar + 3]
1693 return status == b"Z"
1694
1695 def _raise_if_zombie(self):
1696 if self._is_zombie():
1697 raise ZombieProcess(self.pid, self._name, self._ppid)
1698
1699 def _raise_if_not_alive(self):
1700 """Raise NSP if the process disappeared on us."""
1701 # For those C function who do not raise NSP, possibly returning
1702 # incorrect or incomplete result.
1703 os.stat(f"{self._procfs_path}/{self.pid}")
1704
1705 def _readlink(self, path, fallback=UNSET):
1706 # * https://github.com/giampaolo/psutil/issues/503
1707 # os.readlink('/proc/pid/exe') may raise ESRCH (ProcessLookupError)
1708 # instead of ENOENT (FileNotFoundError) when it races.
1709 # * ENOENT may occur also if the path actually exists if PID is
1710 # a low PID (~0-20 range).
1711 # * https://github.com/giampaolo/psutil/issues/2514
1712 try:
1713 return readlink(path)
1714 except (FileNotFoundError, ProcessLookupError):
1715 if os.path.lexists(f"{self._procfs_path}/{self.pid}"):
1716 self._raise_if_zombie()
1717 if fallback is not UNSET:
1718 return fallback
1719 raise
1720
1721 @wrap_exceptions
1722 @memoize_when_activated
1723 def _parse_stat_file(self):
1724 """Parse /proc/{pid}/stat file and return a dict with various
1725 process info.
1726 Using "man proc" as a reference: where "man proc" refers to
1727 position N always subtract 3 (e.g ppid position 4 in
1728 'man proc' == position 1 in here).
1729 The return value is cached in case oneshot() ctx manager is
1730 in use.
1731 """
1732 data = bcat(f"{self._procfs_path}/{self.pid}/stat")
1733 # Process name is between parentheses. It can contain spaces and
1734 # other parentheses. This is taken into account by looking for
1735 # the first occurrence of "(" and the last occurrence of ")".
1736 rpar = data.rfind(b')')
1737 name = data[data.find(b'(') + 1 : rpar]
1738 fields = data[rpar + 2 :].split()
1739
1740 ret = {}
1741 ret['name'] = name
1742 ret['status'] = fields[0]
1743 ret['ppid'] = fields[1]
1744 ret['ttynr'] = fields[4]
1745 ret['utime'] = fields[11]
1746 ret['stime'] = fields[12]
1747 ret['children_utime'] = fields[13]
1748 ret['children_stime'] = fields[14]
1749 ret['create_time'] = fields[19]
1750 ret['cpu_num'] = fields[36]
1751 try:
1752 ret['blkio_ticks'] = fields[39] # aka 'delayacct_blkio_ticks'
1753 except IndexError:
1754 # https://github.com/giampaolo/psutil/issues/2455
1755 debug("can't get blkio_ticks, set iowait to 0")
1756 ret['blkio_ticks'] = 0
1757
1758 return ret
1759
1760 @wrap_exceptions
1761 @memoize_when_activated
1762 def _read_status_file(self):
1763 """Read /proc/{pid}/stat file and return its content.
1764 The return value is cached in case oneshot() ctx manager is
1765 in use.
1766 """
1767 with open_binary(f"{self._procfs_path}/{self.pid}/status") as f:
1768 return f.read()
1769
1770 @wrap_exceptions
1771 @memoize_when_activated
1772 def _read_smaps_file(self):
1773 with open_binary(f"{self._procfs_path}/{self.pid}/smaps") as f:
1774 return f.read().strip()
1775
1776 def oneshot_enter(self):
1777 self._parse_stat_file.cache_activate(self)
1778 self._read_status_file.cache_activate(self)
1779 self._read_smaps_file.cache_activate(self)
1780
1781 def oneshot_exit(self):
1782 self._parse_stat_file.cache_deactivate(self)
1783 self._read_status_file.cache_deactivate(self)
1784 self._read_smaps_file.cache_deactivate(self)
1785
1786 @wrap_exceptions
1787 def name(self):
1788 # XXX - gets changed later and probably needs refactoring
1789 return decode(self._parse_stat_file()['name'])
1790
1791 @wrap_exceptions
1792 def exe(self):
1793 return self._readlink(
1794 f"{self._procfs_path}/{self.pid}/exe", fallback=""
1795 )
1796
1797 @wrap_exceptions
1798 def cmdline(self):
1799 with open_text(f"{self._procfs_path}/{self.pid}/cmdline") as f:
1800 data = f.read()
1801 if not data:
1802 # may happen in case of zombie process
1803 self._raise_if_zombie()
1804 return []
1805 # 'man proc' states that args are separated by null bytes '\0'
1806 # and last char is supposed to be a null byte. Nevertheless
1807 # some processes may change their cmdline after being started
1808 # (via setproctitle() or similar), they are usually not
1809 # compliant with this rule and use spaces instead. Google
1810 # Chrome process is an example. See:
1811 # https://github.com/giampaolo/psutil/issues/1179
1812 sep = '\x00' if data.endswith('\x00') else ' '
1813 if data.endswith(sep):
1814 data = data[:-1]
1815 cmdline = data.split(sep)
1816 # Sometimes last char is a null byte '\0' but the args are
1817 # separated by spaces, see: https://github.com/giampaolo/psutil/
1818 # issues/1179#issuecomment-552984549
1819 if sep == '\x00' and len(cmdline) == 1 and ' ' in data:
1820 cmdline = data.split(' ')
1821 return cmdline
1822
1823 @wrap_exceptions
1824 def environ(self):
1825 with open_text(f"{self._procfs_path}/{self.pid}/environ") as f:
1826 data = f.read()
1827 return parse_environ_block(data)
1828
1829 @wrap_exceptions
1830 def terminal(self):
1831 tty_nr = int(self._parse_stat_file()['ttynr'])
1832 tmap = _psposix.get_terminal_map()
1833 try:
1834 return tmap[tty_nr]
1835 except KeyError:
1836 return None
1837
1838 # May not be available on old kernels.
1839 if os.path.exists(f"/proc/{os.getpid()}/io"):
1840
1841 @wrap_exceptions
1842 def io_counters(self):
1843 fname = f"{self._procfs_path}/{self.pid}/io"
1844 fields = {}
1845 with open_binary(fname) as f:
1846 for line in f:
1847 # https://github.com/giampaolo/psutil/issues/1004
1848 line = line.strip()
1849 if line:
1850 try:
1851 name, value = line.split(b': ')
1852 except ValueError:
1853 # https://github.com/giampaolo/psutil/issues/1004
1854 continue
1855 else:
1856 fields[name] = int(value)
1857 if not fields:
1858 msg = f"{fname} file was empty"
1859 raise RuntimeError(msg)
1860 try:
1861 return pio(
1862 fields[b'syscr'], # read syscalls
1863 fields[b'syscw'], # write syscalls
1864 fields[b'read_bytes'], # read bytes
1865 fields[b'write_bytes'], # write bytes
1866 fields[b'rchar'], # read chars
1867 fields[b'wchar'], # write chars
1868 )
1869 except KeyError as err:
1870 msg = (
1871 f"{err.args[0]!r} field was not found in {fname}; found"
1872 f" fields are {fields!r}"
1873 )
1874 raise ValueError(msg) from None
1875
1876 @wrap_exceptions
1877 def cpu_times(self):
1878 values = self._parse_stat_file()
1879 utime = float(values['utime']) / CLOCK_TICKS
1880 stime = float(values['stime']) / CLOCK_TICKS
1881 children_utime = float(values['children_utime']) / CLOCK_TICKS
1882 children_stime = float(values['children_stime']) / CLOCK_TICKS
1883 iowait = float(values['blkio_ticks']) / CLOCK_TICKS
1884 return pcputimes(utime, stime, children_utime, children_stime, iowait)
1885
1886 @wrap_exceptions
1887 def cpu_num(self):
1888 """What CPU the process is on."""
1889 return int(self._parse_stat_file()['cpu_num'])
1890
1891 @wrap_exceptions
1892 def wait(self, timeout=None):
1893 return _psposix.wait_pid(self.pid, timeout, self._name)
1894
1895 @wrap_exceptions
1896 def create_time(self, monotonic=False):
1897 # The 'starttime' field in /proc/[pid]/stat is expressed in
1898 # jiffies (clock ticks per second), a relative value which
1899 # represents the number of clock ticks that have passed since
1900 # the system booted until the process was created. It never
1901 # changes and is unaffected by system clock updates.
1902 if self._ctime is None:
1903 self._ctime = (
1904 float(self._parse_stat_file()['create_time']) / CLOCK_TICKS
1905 )
1906 if monotonic:
1907 return self._ctime
1908 # Add the boot time, returning time expressed in seconds since
1909 # the epoch. This is subject to system clock updates.
1910 return self._ctime + boot_time()
1911
1912 @wrap_exceptions
1913 def memory_info(self):
1914 # ============================================================
1915 # | FIELD | DESCRIPTION | AKA | TOP |
1916 # ============================================================
1917 # | rss | resident set size | | RES |
1918 # | vms | total program size | size | VIRT |
1919 # | shared | shared pages (from shared mappings) | | SHR |
1920 # | text | text ('code') | trs | CODE |
1921 # | lib | library (unused in Linux 2.6) | lrs | |
1922 # | data | data + stack | drs | DATA |
1923 # | dirty | dirty pages (unused in Linux 2.6) | dt | |
1924 # ============================================================
1925 with open_binary(f"{self._procfs_path}/{self.pid}/statm") as f:
1926 vms, rss, shared, text, lib, data, dirty = (
1927 int(x) * PAGESIZE for x in f.readline().split()[:7]
1928 )
1929 return pmem(rss, vms, shared, text, lib, data, dirty)
1930
1931 if HAS_PROC_SMAPS_ROLLUP or HAS_PROC_SMAPS:
1932
1933 def _parse_smaps_rollup(self):
1934 # /proc/pid/smaps_rollup was added to Linux in 2017. Faster
1935 # than /proc/pid/smaps. It reports higher PSS than */smaps
1936 # (from 1k up to 200k higher; tested against all processes).
1937 # IMPORTANT: /proc/pid/smaps_rollup is weird, because it
1938 # raises ESRCH / ENOENT for many PIDs, even if they're alive
1939 # (also as root). In that case we'll use /proc/pid/smaps as
1940 # fallback, which is slower but has a +50% success rate
1941 # compared to /proc/pid/smaps_rollup.
1942 uss = pss = swap = 0
1943 with open_binary(
1944 f"{self._procfs_path}/{self.pid}/smaps_rollup"
1945 ) as f:
1946 for line in f:
1947 if line.startswith(b"Private_"):
1948 # Private_Clean, Private_Dirty, Private_Hugetlb
1949 uss += int(line.split()[1]) * 1024
1950 elif line.startswith(b"Pss:"):
1951 pss = int(line.split()[1]) * 1024
1952 elif line.startswith(b"Swap:"):
1953 swap = int(line.split()[1]) * 1024
1954 return (uss, pss, swap)
1955
1956 @wrap_exceptions
1957 def _parse_smaps(
1958 self,
1959 # Gets Private_Clean, Private_Dirty, Private_Hugetlb.
1960 _private_re=re.compile(br"\nPrivate.*:\s+(\d+)"),
1961 _pss_re=re.compile(br"\nPss\:\s+(\d+)"),
1962 _swap_re=re.compile(br"\nSwap\:\s+(\d+)"),
1963 ):
1964 # /proc/pid/smaps does not exist on kernels < 2.6.14 or if
1965 # CONFIG_MMU kernel configuration option is not enabled.
1966
1967 # Note: using 3 regexes is faster than reading the file
1968 # line by line.
1969 #
1970 # You might be tempted to calculate USS by subtracting
1971 # the "shared" value from the "resident" value in
1972 # /proc/<pid>/statm. But at least on Linux, statm's "shared"
1973 # value actually counts pages backed by files, which has
1974 # little to do with whether the pages are actually shared.
1975 # /proc/self/smaps on the other hand appears to give us the
1976 # correct information.
1977 smaps_data = self._read_smaps_file()
1978 # Note: smaps file can be empty for certain processes.
1979 # The code below will not crash though and will result to 0.
1980 uss = sum(map(int, _private_re.findall(smaps_data))) * 1024
1981 pss = sum(map(int, _pss_re.findall(smaps_data))) * 1024
1982 swap = sum(map(int, _swap_re.findall(smaps_data))) * 1024
1983 return (uss, pss, swap)
1984
1985 @wrap_exceptions
1986 def memory_full_info(self):
1987 if HAS_PROC_SMAPS_ROLLUP: # faster
1988 try:
1989 uss, pss, swap = self._parse_smaps_rollup()
1990 except (ProcessLookupError, FileNotFoundError):
1991 uss, pss, swap = self._parse_smaps()
1992 else:
1993 uss, pss, swap = self._parse_smaps()
1994 basic_mem = self.memory_info()
1995 return pfullmem(*basic_mem + (uss, pss, swap))
1996
1997 else:
1998 memory_full_info = memory_info
1999
2000 if HAS_PROC_SMAPS:
2001
2002 @wrap_exceptions
2003 def memory_maps(self):
2004 """Return process's mapped memory regions as a list of named
2005 tuples. Fields are explained in 'man proc'; here is an updated
2006 (Apr 2012) version: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/filesystems/proc.txt?id=b76437579d1344b612cf1851ae610c636cec7db0.
2007
2008 /proc/{PID}/smaps does not exist on kernels < 2.6.14 or if
2009 CONFIG_MMU kernel configuration option is not enabled.
2010 """
2011
2012 def get_blocks(lines, current_block):
2013 data = {}
2014 for line in lines:
2015 fields = line.split(None, 5)
2016 if not fields[0].endswith(b':'):
2017 # new block section
2018 yield (current_block.pop(), data)
2019 current_block.append(line)
2020 else:
2021 try:
2022 data[fields[0]] = int(fields[1]) * 1024
2023 except (ValueError, IndexError):
2024 if fields[0].startswith(b'VmFlags:'):
2025 # see issue #369
2026 continue
2027 msg = f"don't know how to interpret line {line!r}"
2028 raise ValueError(msg) from None
2029 yield (current_block.pop(), data)
2030
2031 data = self._read_smaps_file()
2032 # Note: smaps file can be empty for certain processes or for
2033 # zombies.
2034 if not data:
2035 self._raise_if_zombie()
2036 return []
2037 lines = data.split(b'\n')
2038 ls = []
2039 first_line = lines.pop(0)
2040 current_block = [first_line]
2041 for header, data in get_blocks(lines, current_block):
2042 hfields = header.split(None, 5)
2043 try:
2044 addr, perms, _offset, _dev, _inode, path = hfields
2045 except ValueError:
2046 addr, perms, _offset, _dev, _inode, path = hfields + ['']
2047 if not path:
2048 path = '[anon]'
2049 else:
2050 path = decode(path)
2051 path = path.strip()
2052 if path.endswith(' (deleted)') and not path_exists_strict(
2053 path
2054 ):
2055 path = path[:-10]
2056 item = (
2057 decode(addr),
2058 decode(perms),
2059 path,
2060 data.get(b'Rss:', 0),
2061 data.get(b'Size:', 0),
2062 data.get(b'Pss:', 0),
2063 data.get(b'Shared_Clean:', 0),
2064 data.get(b'Shared_Dirty:', 0),
2065 data.get(b'Private_Clean:', 0),
2066 data.get(b'Private_Dirty:', 0),
2067 data.get(b'Referenced:', 0),
2068 data.get(b'Anonymous:', 0),
2069 data.get(b'Swap:', 0),
2070 )
2071 ls.append(item)
2072 return ls
2073
2074 @wrap_exceptions
2075 def cwd(self):
2076 return self._readlink(
2077 f"{self._procfs_path}/{self.pid}/cwd", fallback=""
2078 )
2079
2080 @wrap_exceptions
2081 def num_ctx_switches(
2082 self, _ctxsw_re=re.compile(br'ctxt_switches:\t(\d+)')
2083 ):
2084 data = self._read_status_file()
2085 ctxsw = _ctxsw_re.findall(data)
2086 if not ctxsw:
2087 msg = (
2088 "'voluntary_ctxt_switches' and"
2089 " 'nonvoluntary_ctxt_switches'lines were not found in"
2090 f" {self._procfs_path}/{self.pid}/status; the kernel is"
2091 " probably older than 2.6.23"
2092 )
2093 raise NotImplementedError(msg)
2094 return _common.pctxsw(int(ctxsw[0]), int(ctxsw[1]))
2095
2096 @wrap_exceptions
2097 def num_threads(self, _num_threads_re=re.compile(br'Threads:\t(\d+)')):
2098 # Using a re is faster than iterating over file line by line.
2099 data = self._read_status_file()
2100 return int(_num_threads_re.findall(data)[0])
2101
2102 @wrap_exceptions
2103 def threads(self):
2104 thread_ids = os.listdir(f"{self._procfs_path}/{self.pid}/task")
2105 thread_ids.sort()
2106 retlist = []
2107 hit_enoent = False
2108 for thread_id in thread_ids:
2109 fname = f"{self._procfs_path}/{self.pid}/task/{thread_id}/stat"
2110 try:
2111 with open_binary(fname) as f:
2112 st = f.read().strip()
2113 except (FileNotFoundError, ProcessLookupError):
2114 # no such file or directory or no such process;
2115 # it means thread disappeared on us
2116 hit_enoent = True
2117 continue
2118 # ignore the first two values ("pid (exe)")
2119 st = st[st.find(b')') + 2 :]
2120 values = st.split(b' ')
2121 utime = float(values[11]) / CLOCK_TICKS
2122 stime = float(values[12]) / CLOCK_TICKS
2123 ntuple = _common.pthread(int(thread_id), utime, stime)
2124 retlist.append(ntuple)
2125 if hit_enoent:
2126 self._raise_if_not_alive()
2127 return retlist
2128
2129 @wrap_exceptions
2130 def nice_get(self):
2131 # with open_text(f"{self._procfs_path}/{self.pid}/stat") as f:
2132 # data = f.read()
2133 # return int(data.split()[18])
2134
2135 # Use C implementation
2136 return cext_posix.getpriority(self.pid)
2137
2138 @wrap_exceptions
2139 def nice_set(self, value):
2140 return cext_posix.setpriority(self.pid, value)
2141
2142 # starting from CentOS 6.
2143 if HAS_CPU_AFFINITY:
2144
2145 @wrap_exceptions
2146 def cpu_affinity_get(self):
2147 return cext.proc_cpu_affinity_get(self.pid)
2148
2149 def _get_eligible_cpus(
2150 self, _re=re.compile(br"Cpus_allowed_list:\t(\d+)-(\d+)")
2151 ):
2152 # See: https://github.com/giampaolo/psutil/issues/956
2153 data = self._read_status_file()
2154 match = _re.findall(data)
2155 if match:
2156 return list(range(int(match[0][0]), int(match[0][1]) + 1))
2157 else:
2158 return list(range(len(per_cpu_times())))
2159
2160 @wrap_exceptions
2161 def cpu_affinity_set(self, cpus):
2162 try:
2163 cext.proc_cpu_affinity_set(self.pid, cpus)
2164 except (OSError, ValueError) as err:
2165 if isinstance(err, ValueError) or err.errno == errno.EINVAL:
2166 eligible_cpus = self._get_eligible_cpus()
2167 all_cpus = tuple(range(len(per_cpu_times())))
2168 for cpu in cpus:
2169 if cpu not in all_cpus:
2170 msg = (
2171 f"invalid CPU {cpu!r}; choose between"
2172 f" {eligible_cpus!r}"
2173 )
2174 raise ValueError(msg) from None
2175 if cpu not in eligible_cpus:
2176 msg = (
2177 f"CPU number {cpu} is not eligible; choose"
2178 f" between {eligible_cpus}"
2179 )
2180 raise ValueError(msg) from err
2181 raise
2182
2183 # only starting from kernel 2.6.13
2184 if HAS_PROC_IO_PRIORITY:
2185
2186 @wrap_exceptions
2187 def ionice_get(self):
2188 ioclass, value = cext.proc_ioprio_get(self.pid)
2189 ioclass = IOPriority(ioclass)
2190 return _common.pionice(ioclass, value)
2191
2192 @wrap_exceptions
2193 def ionice_set(self, ioclass, value):
2194 if value is None:
2195 value = 0
2196 if value and ioclass in {
2197 IOPriority.IOPRIO_CLASS_IDLE,
2198 IOPriority.IOPRIO_CLASS_NONE,
2199 }:
2200 msg = f"{ioclass!r} ioclass accepts no value"
2201 raise ValueError(msg)
2202 if value < 0 or value > 7:
2203 msg = "value not in 0-7 range"
2204 raise ValueError(msg)
2205 return cext.proc_ioprio_set(self.pid, ioclass, value)
2206
2207 if hasattr(resource, "prlimit"):
2208
2209 @wrap_exceptions
2210 def rlimit(self, resource_, limits=None):
2211 # If pid is 0 prlimit() applies to the calling process and
2212 # we don't want that. We should never get here though as
2213 # PID 0 is not supported on Linux.
2214 if self.pid == 0:
2215 msg = "can't use prlimit() against PID 0 process"
2216 raise ValueError(msg)
2217 try:
2218 if limits is None:
2219 # get
2220 return resource.prlimit(self.pid, resource_)
2221 else:
2222 # set
2223 if len(limits) != 2:
2224 msg = (
2225 "second argument must be a (soft, hard) "
2226 f"tuple, got {limits!r}"
2227 )
2228 raise ValueError(msg)
2229 resource.prlimit(self.pid, resource_, limits)
2230 except OSError as err:
2231 if err.errno == errno.ENOSYS:
2232 # I saw this happening on Travis:
2233 # https://travis-ci.org/giampaolo/psutil/jobs/51368273
2234 self._raise_if_zombie()
2235 raise
2236
2237 @wrap_exceptions
2238 def status(self):
2239 letter = self._parse_stat_file()['status']
2240 letter = letter.decode()
2241 # XXX is '?' legit? (we're not supposed to return it anyway)
2242 return PROC_STATUSES.get(letter, '?')
2243
2244 @wrap_exceptions
2245 def open_files(self):
2246 retlist = []
2247 files = os.listdir(f"{self._procfs_path}/{self.pid}/fd")
2248 hit_enoent = False
2249 for fd in files:
2250 file = f"{self._procfs_path}/{self.pid}/fd/{fd}"
2251 try:
2252 path = readlink(file)
2253 except (FileNotFoundError, ProcessLookupError):
2254 # ENOENT == file which is gone in the meantime
2255 hit_enoent = True
2256 continue
2257 except OSError as err:
2258 if err.errno == errno.EINVAL:
2259 # not a link
2260 continue
2261 if err.errno == errno.ENAMETOOLONG:
2262 # file name too long
2263 debug(err)
2264 continue
2265 raise
2266 else:
2267 # If path is not an absolute there's no way to tell
2268 # whether it's a regular file or not, so we skip it.
2269 # A regular file is always supposed to be have an
2270 # absolute path though.
2271 if path.startswith('/') and isfile_strict(path):
2272 # Get file position and flags.
2273 file = f"{self._procfs_path}/{self.pid}/fdinfo/{fd}"
2274 try:
2275 with open_binary(file) as f:
2276 pos = int(f.readline().split()[1])
2277 flags = int(f.readline().split()[1], 8)
2278 except (FileNotFoundError, ProcessLookupError):
2279 # fd gone in the meantime; process may
2280 # still be alive
2281 hit_enoent = True
2282 else:
2283 mode = file_flags_to_mode(flags)
2284 ntuple = popenfile(
2285 path, int(fd), int(pos), mode, flags
2286 )
2287 retlist.append(ntuple)
2288 if hit_enoent:
2289 self._raise_if_not_alive()
2290 return retlist
2291
2292 @wrap_exceptions
2293 def net_connections(self, kind='inet'):
2294 ret = _net_connections.retrieve(kind, self.pid)
2295 self._raise_if_not_alive()
2296 return ret
2297
2298 @wrap_exceptions
2299 def num_fds(self):
2300 return len(os.listdir(f"{self._procfs_path}/{self.pid}/fd"))
2301
2302 @wrap_exceptions
2303 def ppid(self):
2304 return int(self._parse_stat_file()['ppid'])
2305
2306 @wrap_exceptions
2307 def uids(self, _uids_re=re.compile(br'Uid:\t(\d+)\t(\d+)\t(\d+)')):
2308 data = self._read_status_file()
2309 real, effective, saved = _uids_re.findall(data)[0]
2310 return _common.puids(int(real), int(effective), int(saved))
2311
2312 @wrap_exceptions
2313 def gids(self, _gids_re=re.compile(br'Gid:\t(\d+)\t(\d+)\t(\d+)')):
2314 data = self._read_status_file()
2315 real, effective, saved = _gids_re.findall(data)[0]
2316 return _common.pgids(int(real), int(effective), int(saved))