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