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 if not all:
1222 with open_text(f"{procfs_path}/filesystems") as f:
1223 for line in f:
1224 line = line.strip()
1225 if not line.startswith("nodev"):
1226 fstypes.add(line.strip())
1227 else:
1228 # ignore all lines starting with "nodev" except "nodev zfs"
1229 fstype = line.split("\t")[1]
1230 if fstype == "zfs":
1231 fstypes.add("zfs")
1232
1233 # See: https://github.com/giampaolo/psutil/issues/1307
1234 if procfs_path == "/proc" and os.path.isfile('/etc/mtab'):
1235 mounts_path = os.path.realpath("/etc/mtab")
1236 else:
1237 mounts_path = os.path.realpath(f"{procfs_path}/self/mounts")
1238
1239 retlist = []
1240 partitions = _psutil.disk_partitions(mounts_path)
1241 for partition in partitions:
1242 device, mountpoint, fstype, opts = partition
1243 if device == 'none':
1244 device = ''
1245 if device in {"/dev/root", "rootfs"}:
1246 device = RootFsDeviceFinder().find() or device
1247 if not all:
1248 if not device or fstype not in fstypes:
1249 continue
1250 ntuple = ntp.sdiskpart(device, mountpoint, fstype, opts)
1251 retlist.append(ntuple)
1252
1253 return retlist
1254
1255
1256# =====================================================================
1257# --- sensors
1258# =====================================================================
1259
1260
1261def sensors_temperatures():
1262 """Return hardware (CPU and others) temperatures as a dict
1263 including hardware name, label, current, max and critical
1264 temperatures.
1265
1266 Implementation notes:
1267 - /sys/class/hwmon looks like the most recent interface to
1268 retrieve this info, and this implementation relies on it
1269 only (old distros will probably use something else)
1270 - lm-sensors on Ubuntu 16.04 relies on /sys/class/hwmon
1271 - /sys/class/thermal/thermal_zone* is another one but it's more
1272 difficult to parse
1273 """
1274 ret = collections.defaultdict(list)
1275 basenames = glob.glob('/sys/class/hwmon/hwmon*/temp*_*')
1276 # CentOS has an intermediate /device directory:
1277 # https://github.com/giampaolo/psutil/issues/971
1278 # https://github.com/nicolargo/glances/issues/1060
1279 basenames.extend(glob.glob('/sys/class/hwmon/hwmon*/device/temp*_*'))
1280 basenames = sorted({x.split('_')[0] for x in basenames})
1281
1282 # Only add the coretemp hwmon entries if they're not already in
1283 # /sys/class/hwmon/
1284 # https://github.com/giampaolo/psutil/issues/1708
1285 # https://github.com/giampaolo/psutil/pull/1648
1286 basenames2 = glob.glob(
1287 '/sys/devices/platform/coretemp.*/hwmon/hwmon*/temp*_*'
1288 )
1289 repl = re.compile(r"/sys/devices/platform/coretemp.*/hwmon/")
1290 for name in basenames2:
1291 altname = repl.sub('/sys/class/hwmon/', name)
1292 if altname not in basenames:
1293 basenames.append(name)
1294
1295 for base in basenames:
1296 try:
1297 path = base + '_input'
1298 current = float(bcat(path)) / 1000.0
1299 path = os.path.join(os.path.dirname(base), 'name')
1300 unit_name = cat(path).strip()
1301 except (OSError, ValueError):
1302 # A lot of things can go wrong here, so let's just skip the
1303 # whole entry. Sure thing is Linux's /sys/class/hwmon really
1304 # is a stinky broken mess.
1305 # https://github.com/giampaolo/psutil/issues/1009
1306 # https://github.com/giampaolo/psutil/issues/1101
1307 # https://github.com/giampaolo/psutil/issues/1129
1308 # https://github.com/giampaolo/psutil/issues/1245
1309 # https://github.com/giampaolo/psutil/issues/1323
1310 continue
1311
1312 high = bcat(base + '_max', fallback=None)
1313 critical = bcat(base + '_crit', fallback=None)
1314 label = cat(base + '_label', fallback='').strip()
1315
1316 if high is not None:
1317 try:
1318 high = float(high) / 1000.0
1319 except ValueError:
1320 high = None
1321 if critical is not None:
1322 try:
1323 critical = float(critical) / 1000.0
1324 except ValueError:
1325 critical = None
1326
1327 ret[unit_name].append((label, current, high, critical))
1328
1329 # Indication that no sensors were detected in /sys/class/hwmon/
1330 if not basenames:
1331 basenames = glob.glob('/sys/class/thermal/thermal_zone*')
1332 basenames = sorted(set(basenames))
1333
1334 for base in basenames:
1335 try:
1336 path = os.path.join(base, 'temp')
1337 current = float(bcat(path)) / 1000.0
1338 path = os.path.join(base, 'type')
1339 unit_name = cat(path).strip()
1340 except (OSError, ValueError) as err:
1341 debug(err)
1342 continue
1343
1344 trip_paths = glob.glob(base + '/trip_point*')
1345 trip_points = {
1346 '_'.join(os.path.basename(p).split('_')[0:3])
1347 for p in trip_paths
1348 }
1349 critical = None
1350 high = None
1351 for trip_point in trip_points:
1352 path = os.path.join(base, trip_point + "_type")
1353 trip_type = cat(path, fallback='').strip()
1354 if trip_type == 'critical':
1355 critical = bcat(
1356 os.path.join(base, trip_point + "_temp"), fallback=None
1357 )
1358 elif trip_type == 'high':
1359 high = bcat(
1360 os.path.join(base, trip_point + "_temp"), fallback=None
1361 )
1362
1363 if high is not None:
1364 try:
1365 high = float(high) / 1000.0
1366 except ValueError:
1367 high = None
1368 if critical is not None:
1369 try:
1370 critical = float(critical) / 1000.0
1371 except ValueError:
1372 critical = None
1373
1374 ret[unit_name].append(('', current, high, critical))
1375
1376 return dict(ret)
1377
1378
1379def sensors_fans():
1380 """Return hardware fans info (for CPU and other peripherals) as a
1381 dict including hardware label and current speed.
1382
1383 Implementation notes:
1384 - /sys/class/hwmon looks like the most recent interface to
1385 retrieve this info, and this implementation relies on it
1386 only (old distros will probably use something else)
1387 - lm-sensors on Ubuntu 16.04 relies on /sys/class/hwmon
1388 """
1389 ret = collections.defaultdict(list)
1390 basenames = glob.glob('/sys/class/hwmon/hwmon*/fan*_*')
1391 if not basenames:
1392 # CentOS has an intermediate /device directory:
1393 # https://github.com/giampaolo/psutil/issues/971
1394 basenames = glob.glob('/sys/class/hwmon/hwmon*/device/fan*_*')
1395
1396 basenames = sorted({x.split("_")[0] for x in basenames})
1397 for base in basenames:
1398 try:
1399 current = int(bcat(base + '_input'))
1400 except OSError as err:
1401 debug(err)
1402 continue
1403 unit_name = cat(os.path.join(os.path.dirname(base), 'name')).strip()
1404 label = cat(base + '_label', fallback='').strip()
1405 ret[unit_name].append(ntp.sfan(label, current))
1406
1407 return dict(ret)
1408
1409
1410def sensors_battery():
1411 """Return battery information.
1412 Implementation note: it appears /sys/class/power_supply/BAT0/
1413 directory structure may vary and provide files with the same
1414 meaning but under different names, see:
1415 https://github.com/giampaolo/psutil/issues/966.
1416 """
1417 null = object()
1418
1419 def multi_bcat(*paths):
1420 """Attempt to read the content of multiple files which may
1421 not exist. If none of them exist return None.
1422 """
1423 for path in paths:
1424 ret = bcat(path, fallback=null)
1425 if ret != null:
1426 try:
1427 return int(ret)
1428 except ValueError:
1429 return ret.strip()
1430 return None
1431
1432 bats = [
1433 x
1434 for x in os.listdir(POWER_SUPPLY_PATH)
1435 if x.startswith('BAT') or 'battery' in x.lower()
1436 ]
1437 if not bats:
1438 return None
1439 # Get the first available battery. Usually this is "BAT0", except
1440 # some rare exceptions:
1441 # https://github.com/giampaolo/psutil/issues/1238
1442 root = os.path.join(POWER_SUPPLY_PATH, min(bats))
1443
1444 # Base metrics.
1445 energy_now = multi_bcat(root + "/energy_now", root + "/charge_now")
1446 power_now = multi_bcat(root + "/power_now", root + "/current_now")
1447 energy_full = multi_bcat(root + "/energy_full", root + "/charge_full")
1448 time_to_empty = multi_bcat(root + "/time_to_empty_now")
1449
1450 # Percent. If we have energy_full the percentage will be more
1451 # accurate compared to reading /capacity file (float vs. int).
1452 if energy_full is not None and energy_now is not None:
1453 try:
1454 percent = 100.0 * energy_now / energy_full
1455 except ZeroDivisionError:
1456 percent = 0.0
1457 else:
1458 percent = float(cat(root + "/capacity", fallback=-1))
1459 if percent == -1:
1460 return None
1461
1462 # Is AC power cable plugged in?
1463 # Note: AC0 is not always available and sometimes (e.g. CentOS7)
1464 # it's called "AC".
1465 power_plugged = None
1466 online = multi_bcat(
1467 os.path.join(POWER_SUPPLY_PATH, "AC0/online"),
1468 os.path.join(POWER_SUPPLY_PATH, "AC/online"),
1469 )
1470 if online is not None:
1471 power_plugged = online == 1
1472 else:
1473 status = cat(root + "/status", fallback="").strip().lower()
1474 if status == "discharging":
1475 power_plugged = False
1476 elif status in {"charging", "full"}:
1477 power_plugged = True
1478
1479 # Seconds left.
1480 if power_plugged:
1481 secsleft = BatteryTime.POWER_TIME_UNLIMITED
1482 elif energy_now is not None and power_now is not None:
1483 try:
1484 secsleft = int(energy_now / abs(power_now) * 3600)
1485 except ZeroDivisionError:
1486 secsleft = BatteryTime.POWER_TIME_UNKNOWN
1487 elif time_to_empty is not None:
1488 secsleft = int(time_to_empty * 60)
1489 if secsleft < 0:
1490 secsleft = BatteryTime.POWER_TIME_UNKNOWN
1491 else:
1492 secsleft = BatteryTime.POWER_TIME_UNKNOWN
1493
1494 return ntp.sbattery(percent, secsleft, power_plugged)
1495
1496
1497# =====================================================================
1498# --- other system functions
1499# =====================================================================
1500
1501
1502def users():
1503 """Return currently connected users as a list of named tuples."""
1504 retlist = []
1505 rawlist = _psutil.users()
1506 for item in rawlist:
1507 user, tty, hostname, tstamp, pid = item
1508 nt = ntp.suser(user, tty or None, hostname, tstamp, pid)
1509 retlist.append(nt)
1510 return retlist
1511
1512
1513def boot_time():
1514 """Return the system boot time expressed in seconds since the epoch."""
1515 path = f"{get_procfs_path()}/stat"
1516 with open_binary(path) as f:
1517 for line in f:
1518 if line.startswith(b'btime'):
1519 return float(line.strip().split()[1])
1520 msg = f"line 'btime' not found in {path}"
1521 raise RuntimeError(msg)
1522
1523
1524# =====================================================================
1525# --- processes
1526# =====================================================================
1527
1528
1529def pids():
1530 """Returns a list of PIDs currently running on the system."""
1531 path = get_procfs_path().encode(ENCODING)
1532 return [int(x) for x in os.listdir(path) if x.isdigit()]
1533
1534
1535def pid_exists(pid):
1536 """Check for the existence of a unix PID. Linux TIDs are not
1537 supported (always return False).
1538 """
1539 if not _psposix.pid_exists(pid):
1540 return False
1541 else:
1542 # Linux's apparently does not distinguish between PIDs and TIDs
1543 # (thread IDs).
1544 # listdir("/proc") won't show any TID (only PIDs) but
1545 # os.stat("/proc/{tid}") will succeed if {tid} exists.
1546 # os.kill() can also be passed a TID. This is quite confusing.
1547 # In here we want to enforce this distinction and support PIDs
1548 # only, see:
1549 # https://github.com/giampaolo/psutil/issues/687
1550 try:
1551 # Note: already checked that this is faster than using a
1552 # regular expr. Also (a lot) faster than doing
1553 # 'return pid in pids()'
1554 path = f"{get_procfs_path()}/{pid}/status"
1555 with open_binary(path) as f:
1556 for line in f:
1557 if line.startswith(b"Tgid:"):
1558 tgid = int(line.split()[1])
1559 # If tgid and pid are the same then we're
1560 # dealing with a process PID.
1561 return tgid == pid
1562 msg = f"'Tgid' line not found in {path}"
1563 raise ValueError(msg)
1564 except (OSError, ValueError):
1565 return pid in pids()
1566
1567
1568def ppid_map():
1569 """Obtain a {pid: ppid, ...} dict for all running processes in
1570 one shot. Used to speed up Process.children().
1571 """
1572 ret = {}
1573 procfs_path = get_procfs_path()
1574 for pid in pids():
1575 try:
1576 with open_binary(f"{procfs_path}/{pid}/stat") as f:
1577 data = f.read()
1578 except (FileNotFoundError, ProcessLookupError):
1579 pass
1580 except PermissionError as err:
1581 raise AccessDenied(pid) from err
1582 else:
1583 rpar = data.rfind(b')')
1584 dset = data[rpar + 2 :].split()
1585 ppid = int(dset[1])
1586 ret[pid] = ppid
1587 return ret
1588
1589
1590def wrap_exceptions(fun):
1591 """Decorator which translates bare OSError exceptions into
1592 NoSuchProcess and AccessDenied.
1593 """
1594
1595 @functools.wraps(fun)
1596 def wrapper(self, *args, **kwargs):
1597 pid, name = self.pid, self._name
1598 try:
1599 return fun(self, *args, **kwargs)
1600 except PermissionError as err:
1601 raise AccessDenied(pid, name) from err
1602 except ProcessLookupError as err:
1603 self._raise_if_zombie()
1604 raise NoSuchProcess(pid, name) from err
1605 except FileNotFoundError as err:
1606 self._raise_if_zombie()
1607 # /proc/PID directory may still exist, but the files within
1608 # it may not, indicating the process is gone, see:
1609 # https://github.com/giampaolo/psutil/issues/2418
1610 if not os.path.exists(f"{self._procfs_path}/{pid}/stat"):
1611 raise NoSuchProcess(pid, name) from err
1612 raise
1613
1614 return wrapper
1615
1616
1617class Process:
1618 """Linux process implementation."""
1619
1620 __slots__ = [
1621 "_cache",
1622 "_ctime",
1623 "_name",
1624 "_ppid",
1625 "_procfs_path",
1626 "pid",
1627 ]
1628
1629 def __init__(self, pid):
1630 self.pid = pid
1631 self._name = None
1632 self._ppid = None
1633 self._ctime = None
1634 self._procfs_path = get_procfs_path()
1635
1636 def _is_zombie(self):
1637 # Note: most of the times Linux is able to return info about the
1638 # process even if it's a zombie, and /proc/{pid} will exist.
1639 # There are some exceptions though, like exe(), cmdline() and
1640 # memory_maps(). In these cases /proc/{pid}/{file} exists but
1641 # it's empty. Instead of returning a "null" value we'll raise an
1642 # exception.
1643 try:
1644 data = bcat(f"{self._procfs_path}/{self.pid}/stat")
1645 except OSError:
1646 return False
1647 else:
1648 rpar = data.rfind(b')')
1649 status = data[rpar + 2 : rpar + 3]
1650 return status == b"Z"
1651
1652 def _raise_if_zombie(self):
1653 if self._is_zombie():
1654 raise ZombieProcess(self.pid, self._name, self._ppid)
1655
1656 def _raise_if_not_alive(self):
1657 """Raise NSP if the process disappeared on us."""
1658 # For those C function who do not raise NSP, possibly returning
1659 # incorrect or incomplete result.
1660 os.stat(f"{self._procfs_path}/{self.pid}")
1661
1662 def _readlink(self, path, fallback=UNSET):
1663 # * https://github.com/giampaolo/psutil/issues/503
1664 # os.readlink('/proc/pid/exe') may raise ESRCH (ProcessLookupError)
1665 # instead of ENOENT (FileNotFoundError) when it races.
1666 # * ENOENT may occur also if the path actually exists if PID is
1667 # a low PID (~0-20 range).
1668 # * https://github.com/giampaolo/psutil/issues/2514
1669 try:
1670 return readlink(path)
1671 except (FileNotFoundError, ProcessLookupError):
1672 if os.path.lexists(f"{self._procfs_path}/{self.pid}"):
1673 self._raise_if_zombie()
1674 if fallback is not UNSET:
1675 return fallback
1676 raise
1677
1678 @wrap_exceptions
1679 @memoize_when_activated
1680 def _parse_stat_file(self):
1681 """Parse /proc/{pid}/stat file and return a dict with various
1682 process info.
1683 Using "man proc" as a reference: where "man proc" refers to
1684 position N always subtract 3 (e.g ppid position 4 in
1685 'man proc' == position 1 in here).
1686 The return value is cached in case oneshot() ctx manager is
1687 in use.
1688 """
1689 data = bcat(f"{self._procfs_path}/{self.pid}/stat")
1690 # Process name is between parentheses. It can contain spaces and
1691 # other parentheses. This is taken into account by looking for
1692 # the first occurrence of "(" and the last occurrence of ")".
1693 rpar = data.rfind(b')')
1694 name = data[data.find(b'(') + 1 : rpar]
1695 fields = data[rpar + 2 :].split()
1696
1697 ret = {}
1698 ret['name'] = name
1699 ret['status'] = fields[0]
1700 ret['ppid'] = fields[1]
1701 ret['ttynr'] = fields[4]
1702 ret['minflt'] = fields[7]
1703 ret['majflt'] = fields[9]
1704 ret['utime'] = fields[11]
1705 ret['stime'] = fields[12]
1706 ret['children_utime'] = fields[13]
1707 ret['children_stime'] = fields[14]
1708 ret['create_time'] = fields[19]
1709 ret['cpu_num'] = fields[36]
1710 try:
1711 ret['blkio_ticks'] = fields[39] # aka 'delayacct_blkio_ticks'
1712 except IndexError:
1713 # https://github.com/giampaolo/psutil/issues/2455
1714 debug("can't get blkio_ticks, set iowait to 0")
1715 ret['blkio_ticks'] = 0
1716
1717 return ret
1718
1719 @wrap_exceptions
1720 @memoize_when_activated
1721 def _read_status_file(self):
1722 """Read /proc/{pid}/stat file and return its content.
1723 The return value is cached in case oneshot() ctx manager is
1724 in use.
1725 """
1726 with open_binary(f"{self._procfs_path}/{self.pid}/status") as f:
1727 return f.read()
1728
1729 @wrap_exceptions
1730 @memoize_when_activated
1731 def _read_smaps_file(self):
1732 with open_binary(f"{self._procfs_path}/{self.pid}/smaps") as f:
1733 return f.read().strip()
1734
1735 def oneshot_enter(self):
1736 self._parse_stat_file.cache_activate(self)
1737 self._read_status_file.cache_activate(self)
1738 self._read_smaps_file.cache_activate(self)
1739
1740 def oneshot_exit(self):
1741 self._parse_stat_file.cache_deactivate(self)
1742 self._read_status_file.cache_deactivate(self)
1743 self._read_smaps_file.cache_deactivate(self)
1744
1745 @wrap_exceptions
1746 def name(self):
1747 # XXX - gets changed later and probably needs refactoring
1748 return decode(self._parse_stat_file()['name'])
1749
1750 @wrap_exceptions
1751 def exe(self):
1752 return self._readlink(
1753 f"{self._procfs_path}/{self.pid}/exe", fallback=""
1754 )
1755
1756 @wrap_exceptions
1757 def cmdline(self):
1758 with open_text(f"{self._procfs_path}/{self.pid}/cmdline") as f:
1759 data = f.read()
1760 if not data:
1761 # may happen in case of zombie process
1762 self._raise_if_zombie()
1763 return []
1764 # 'man proc' states that args are separated by null bytes '\0'
1765 # and last char is supposed to be a null byte. Nevertheless
1766 # some processes may change their cmdline after being started
1767 # (via setproctitle() or similar), they are usually not
1768 # compliant with this rule and use spaces instead. Google
1769 # Chrome process is an example. See:
1770 # https://github.com/giampaolo/psutil/issues/1179
1771 sep = '\x00' if data.endswith('\x00') else ' '
1772 if data.endswith(sep):
1773 data = data[:-1]
1774 cmdline = data.split(sep)
1775 # Sometimes last char is a null byte '\0' but the args are
1776 # separated by spaces, see: https://github.com/giampaolo/psutil/
1777 # issues/1179#issuecomment-552984549
1778 if sep == '\x00' and len(cmdline) == 1 and ' ' in data:
1779 cmdline = data.split(' ')
1780 return cmdline
1781
1782 @wrap_exceptions
1783 def environ(self):
1784 with open_text(f"{self._procfs_path}/{self.pid}/environ") as f:
1785 data = f.read()
1786 return parse_environ_block(data)
1787
1788 @wrap_exceptions
1789 def terminal(self):
1790 tty_nr = int(self._parse_stat_file()['ttynr'])
1791 if tty_nr == 0:
1792 return None
1793 return _psposix.get_terminal(tty_nr)
1794
1795 # May not be available on old kernels.
1796 if os.path.exists(f"/proc/{os.getpid()}/io"):
1797
1798 @wrap_exceptions
1799 def io_counters(self):
1800 fname = f"{self._procfs_path}/{self.pid}/io"
1801 fields = {}
1802 with open_binary(fname) as f:
1803 for line in f:
1804 # https://github.com/giampaolo/psutil/issues/1004
1805 line = line.strip()
1806 if line:
1807 try:
1808 name, value = line.split(b': ')
1809 except ValueError:
1810 # https://github.com/giampaolo/psutil/issues/1004
1811 continue
1812 else:
1813 fields[name] = int(value)
1814 if not fields:
1815 msg = f"{fname} file was empty"
1816 raise RuntimeError(msg)
1817 try:
1818 return ntp.pio(
1819 fields[b'syscr'], # read syscalls
1820 fields[b'syscw'], # write syscalls
1821 fields[b'read_bytes'], # read bytes
1822 fields[b'write_bytes'], # write bytes
1823 fields[b'rchar'], # read chars
1824 fields[b'wchar'], # write chars
1825 )
1826 except KeyError as err:
1827 msg = (
1828 f"{err.args[0]!r} field was not found in {fname}; found"
1829 f" fields are {fields!r}"
1830 )
1831 raise ValueError(msg) from None
1832
1833 @wrap_exceptions
1834 def cpu_times(self):
1835 values = self._parse_stat_file()
1836 utime = float(values['utime']) / CLOCK_TICKS
1837 stime = float(values['stime']) / CLOCK_TICKS
1838 children_utime = float(values['children_utime']) / CLOCK_TICKS
1839 children_stime = float(values['children_stime']) / CLOCK_TICKS
1840 iowait = float(values['blkio_ticks']) / CLOCK_TICKS
1841 return ntp.pcputimes(
1842 utime, stime, children_utime, children_stime, iowait
1843 )
1844
1845 @wrap_exceptions
1846 def cpu_num(self):
1847 """What CPU the process is on."""
1848 return int(self._parse_stat_file()['cpu_num'])
1849
1850 @wrap_exceptions
1851 def wait(self, timeout=None):
1852 return _psposix.wait_pid(self.pid, timeout)
1853
1854 @wrap_exceptions
1855 def create_time(self, monotonic=False):
1856 # The 'starttime' field in /proc/[pid]/stat is expressed in
1857 # jiffies (clock ticks per second), a relative value which
1858 # represents the number of clock ticks that have passed since
1859 # the system booted until the process was created. It never
1860 # changes and is unaffected by system clock updates.
1861 if self._ctime is None:
1862 self._ctime = (
1863 float(self._parse_stat_file()['create_time']) / CLOCK_TICKS
1864 )
1865 if monotonic:
1866 return self._ctime
1867 # Add the boot time, returning time expressed in seconds since
1868 # the epoch. This is subject to system clock updates.
1869 return self._ctime + boot_time()
1870
1871 @wrap_exceptions
1872 def memory_info(self):
1873 # ============================================================
1874 # | FIELD | DESCRIPTION | AKA | TOP |
1875 # ============================================================
1876 # | rss | resident set size | | RES |
1877 # | vms | total program size | size | VIRT |
1878 # | shared | shared pages (from shared mappings) | | SHR |
1879 # | text | text ('code') | trs | CODE |
1880 # | lib | library (unused in Linux 2.6) | lrs | |
1881 # | data | data + stack | drs | DATA |
1882 # | dirty | dirty pages (unused in Linux 2.6) | dt | |
1883 # ============================================================
1884 with open_binary(f"{self._procfs_path}/{self.pid}/statm") as f:
1885 vms, rss, shared, text, _lib, data, _dirty = (
1886 int(x) * PAGESIZE for x in f.readline().split()[:7]
1887 )
1888 return ntp.pmem(rss, vms, shared, text, data)
1889
1890 @wrap_exceptions
1891 def memory_info_ex(
1892 self,
1893 _vmpeak_re=re.compile(br"VmPeak:\s+(\d+)"),
1894 _vmhwm_re=re.compile(br"VmHWM:\s+(\d+)"),
1895 _rssanon_re=re.compile(br"RssAnon:\s+(\d+)"),
1896 _rssfile_re=re.compile(br"RssFile:\s+(\d+)"),
1897 _rssshmem_re=re.compile(br"RssShmem:\s+(\d+)"),
1898 _vmswap_re=re.compile(br"VmSwap:\s+(\d+)"),
1899 _hugetlb_re=re.compile(br"HugetlbPages:\s+(\d+)"),
1900 ):
1901 # Read /proc/{pid}/status which provides peak RSS/VMS and a
1902 # cheaper way to get swap (no smaps parsing needed).
1903 # RssAnon/RssFile/RssShmem were added in Linux 4.5;
1904 # VmSwap in 2.6.34; HugetlbPages in 4.4.
1905 data = self._read_status_file()
1906
1907 def parse(regex):
1908 m = regex.search(data)
1909 return int(m.group(1)) * 1024 if m else 0
1910
1911 return {
1912 "peak_rss": parse(_vmhwm_re),
1913 "peak_vms": parse(_vmpeak_re),
1914 "rss_anon": parse(_rssanon_re),
1915 "rss_file": parse(_rssfile_re),
1916 "rss_shmem": parse(_rssshmem_re),
1917 "swap": parse(_vmswap_re),
1918 "hugetlb": parse(_hugetlb_re),
1919 }
1920
1921 if HAS_PROC_SMAPS_ROLLUP or HAS_PROC_SMAPS:
1922
1923 def _parse_smaps_rollup(self):
1924 # /proc/pid/smaps_rollup was added to Linux in 2017. Faster
1925 # than /proc/pid/smaps. It reports higher PSS than */smaps
1926 # (from 1k up to 200k higher; tested against all processes).
1927 # IMPORTANT: /proc/pid/smaps_rollup is weird, because it
1928 # raises ESRCH / ENOENT for many PIDs, even if they're alive
1929 # (also as root). In that case we'll use /proc/pid/smaps as
1930 # fallback, which is slower but has a +50% success rate
1931 # compared to /proc/pid/smaps_rollup.
1932 uss = pss = swap = 0
1933 with open_binary(
1934 f"{self._procfs_path}/{self.pid}/smaps_rollup"
1935 ) as f:
1936 for line in f:
1937 if line.startswith(b"Private_"):
1938 # Private_Clean, Private_Dirty, Private_Hugetlb
1939 uss += int(line.split()[1]) * 1024
1940 elif line.startswith(b"Pss:"):
1941 pss = int(line.split()[1]) * 1024
1942 elif line.startswith(b"Swap:"):
1943 swap = int(line.split()[1]) * 1024
1944 return (uss, pss, swap)
1945
1946 @wrap_exceptions
1947 def _parse_smaps(
1948 self,
1949 # Gets Private_Clean, Private_Dirty, Private_Hugetlb.
1950 _private_re=re.compile(br"\nPrivate.*:\s+(\d+)"),
1951 _pss_re=re.compile(br"\nPss\:\s+(\d+)"),
1952 _swap_re=re.compile(br"\nSwap\:\s+(\d+)"),
1953 ):
1954 # /proc/pid/smaps does not exist on kernels < 2.6.14 or if
1955 # CONFIG_MMU kernel configuration option is not enabled.
1956
1957 # Note: using 3 regexes is faster than reading the file
1958 # line by line.
1959 #
1960 # You might be tempted to calculate USS by subtracting
1961 # the "shared" value from the "resident" value in
1962 # /proc/<pid>/statm. But at least on Linux, statm's "shared"
1963 # value actually counts pages backed by files, which has
1964 # little to do with whether the pages are actually shared.
1965 # /proc/self/smaps on the other hand appears to give us the
1966 # correct information.
1967 smaps_data = self._read_smaps_file()
1968 # Note: smaps file can be empty for certain processes.
1969 # The code below will not crash though and will result to 0.
1970 uss = sum(map(int, _private_re.findall(smaps_data))) * 1024
1971 pss = sum(map(int, _pss_re.findall(smaps_data))) * 1024
1972 swap = sum(map(int, _swap_re.findall(smaps_data))) * 1024
1973 return (uss, pss, swap)
1974
1975 @wrap_exceptions
1976 def memory_footprint(self):
1977 def fetch():
1978 if HAS_PROC_SMAPS_ROLLUP: # faster
1979 try:
1980 return self._parse_smaps_rollup()
1981 except (ProcessLookupError, FileNotFoundError):
1982 pass
1983 return self._parse_smaps()
1984
1985 uss, pss, swap = fetch()
1986 return ntp.pfootprint(uss, pss, swap)
1987
1988 if HAS_PROC_SMAPS:
1989
1990 @wrap_exceptions
1991 def memory_maps(self):
1992 """Return process's mapped memory regions as a list of named
1993 tuples. Fields are explained in 'man proc'; here is an updated
1994 (Apr 2012) version: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/filesystems/proc.txt?id=b76437579d1344b612cf1851ae610c636cec7db0.
1995
1996 /proc/{PID}/smaps does not exist on kernels < 2.6.14 or if
1997 CONFIG_MMU kernel configuration option is not enabled.
1998 """
1999
2000 def get_blocks(lines, current_block):
2001 data = {}
2002 for line in lines:
2003 fields = line.split(None, 5)
2004 if not fields[0].endswith(b':'):
2005 # new block section
2006 yield (current_block.pop(), data)
2007 current_block.append(line)
2008 else:
2009 try:
2010 data[fields[0]] = int(fields[1]) * 1024
2011 except (ValueError, IndexError):
2012 if fields[0].startswith(b'VmFlags:'):
2013 # see issue #369
2014 continue
2015 msg = f"don't know how to interpret line {line!r}"
2016 raise ValueError(msg) from None
2017 yield (current_block.pop(), data)
2018
2019 data = self._read_smaps_file()
2020 # Note: smaps file can be empty for certain processes or for
2021 # zombies.
2022 if not data:
2023 self._raise_if_zombie()
2024 return []
2025 lines = data.split(b'\n')
2026 ls = []
2027 first_line = lines.pop(0)
2028 current_block = [first_line]
2029 for header, data in get_blocks(lines, current_block):
2030 hfields = header.split(None, 5)
2031 try:
2032 addr, perms, _offset, _dev, _inode, path = hfields
2033 except ValueError:
2034 addr, perms, _offset, _dev, _inode, path = hfields + ['']
2035 if not path:
2036 path = '[anon]'
2037 else:
2038 path = decode(path)
2039 path = path.strip()
2040 if path.endswith(' (deleted)') and not path_exists_strict(
2041 path
2042 ):
2043 path = path[:-10]
2044 item = (
2045 decode(addr),
2046 decode(perms),
2047 path,
2048 data.get(b'Rss:', 0),
2049 data.get(b'Size:', 0),
2050 data.get(b'Pss:', 0),
2051 data.get(b'Shared_Clean:', 0),
2052 data.get(b'Shared_Dirty:', 0),
2053 data.get(b'Private_Clean:', 0),
2054 data.get(b'Private_Dirty:', 0),
2055 data.get(b'Referenced:', 0),
2056 data.get(b'Anonymous:', 0),
2057 data.get(b'Swap:', 0),
2058 )
2059 ls.append(item)
2060 return ls
2061
2062 @wrap_exceptions
2063 def page_faults(self):
2064 values = self._parse_stat_file()
2065 return ntp.ppagefaults(int(values['minflt']), int(values['majflt']))
2066
2067 @wrap_exceptions
2068 def cwd(self):
2069 return self._readlink(
2070 f"{self._procfs_path}/{self.pid}/cwd", fallback=""
2071 )
2072
2073 @wrap_exceptions
2074 def num_ctx_switches(
2075 self, _ctxsw_re=re.compile(br'ctxt_switches:\t(\d+)')
2076 ):
2077 data = self._read_status_file()
2078 ctxsw = _ctxsw_re.findall(data)
2079 if not ctxsw:
2080 msg = (
2081 "'voluntary_ctxt_switches' and"
2082 " 'nonvoluntary_ctxt_switches'lines were not found in"
2083 f" {self._procfs_path}/{self.pid}/status; the kernel is"
2084 " probably older than 2.6.23"
2085 )
2086 raise NotImplementedError(msg)
2087 return ntp.pctxsw(int(ctxsw[0]), int(ctxsw[1]))
2088
2089 @wrap_exceptions
2090 def num_threads(self, _num_threads_re=re.compile(br'Threads:\t(\d+)')):
2091 # Using a re is faster than iterating over file line by line.
2092 data = self._read_status_file()
2093 return int(_num_threads_re.findall(data)[0])
2094
2095 @wrap_exceptions
2096 def threads(self):
2097 thread_ids = os.listdir(f"{self._procfs_path}/{self.pid}/task")
2098 thread_ids.sort()
2099 retlist = []
2100 hit_enoent = False
2101 for thread_id in thread_ids:
2102 fname = f"{self._procfs_path}/{self.pid}/task/{thread_id}/stat"
2103 try:
2104 with open_binary(fname) as f:
2105 st = f.read().strip()
2106 except (FileNotFoundError, ProcessLookupError):
2107 # no such file or directory or no such process;
2108 # it means thread disappeared on us
2109 hit_enoent = True
2110 continue
2111 # ignore the first two values ("pid (exe)")
2112 st = st[st.find(b')') + 2 :]
2113 values = st.split(b' ')
2114 utime = float(values[11]) / CLOCK_TICKS
2115 stime = float(values[12]) / CLOCK_TICKS
2116 ntuple = ntp.pthread(int(thread_id), utime, stime)
2117 retlist.append(ntuple)
2118 if hit_enoent:
2119 self._raise_if_not_alive()
2120 return retlist
2121
2122 @wrap_exceptions
2123 def nice_get(self):
2124 # with open_text(f"{self._procfs_path}/{self.pid}/stat") as f:
2125 # data = f.read()
2126 # return int(data.split()[18])
2127
2128 # Use C implementation
2129 return _psutil.proc_priority_get(self.pid)
2130
2131 @wrap_exceptions
2132 def nice_set(self, value):
2133 return _psutil.proc_priority_set(self.pid, value)
2134
2135 # starting from CentOS 6.
2136 if HAS_CPU_AFFINITY:
2137
2138 @wrap_exceptions
2139 def cpu_affinity_get(self):
2140 return _psutil.proc_cpu_affinity_get(self.pid)
2141
2142 def _get_eligible_cpus(
2143 self,
2144 _re=re.compile(
2145 br"^Cpus_allowed_list:[ \t]*([^\r\n]*)", re.MULTILINE
2146 ),
2147 ):
2148 # See: https://github.com/giampaolo/psutil/issues/956
2149 data = self._read_status_file()
2150 if match := _re.search(data):
2151 try:
2152 return _parse_cpulist(decode(match.group(1)))
2153 except ValueError as err:
2154 debug(
2155 f"can't parse Cpus_allowed_list ({err}); falling back"
2156 )
2157 return list(range(len(per_cpu_times())))
2158
2159 @wrap_exceptions
2160 def cpu_affinity_set(self, cpus):
2161 try:
2162 _psutil.proc_cpu_affinity_set(self.pid, cpus)
2163 except (OSError, ValueError) as err:
2164 if isinstance(err, ValueError) or err.errno == errno.EINVAL:
2165 eligible_cpus = self._get_eligible_cpus()
2166 all_cpus = tuple(range(len(per_cpu_times())))
2167 for cpu in cpus:
2168 if cpu not in all_cpus:
2169 msg = (
2170 f"invalid CPU {cpu!r}; choose between"
2171 f" {eligible_cpus!r}"
2172 )
2173 raise ValueError(msg) from None
2174 if cpu not in eligible_cpus:
2175 msg = (
2176 f"CPU number {cpu} is not eligible; choose"
2177 f" between {eligible_cpus}"
2178 )
2179 raise ValueError(msg) from err
2180 raise
2181
2182 # only starting from kernel 2.6.13
2183 if HAS_PROC_IO_PRIORITY:
2184
2185 @wrap_exceptions
2186 def ionice_get(self):
2187 ioclass, value = _psutil.proc_ioprio_get(self.pid)
2188 ioclass = ProcessIOPriority(ioclass)
2189 return ntp.pionice(ioclass, value)
2190
2191 @wrap_exceptions
2192 def ionice_set(self, ioclass, value):
2193 if value is None:
2194 value = 0
2195 if value and ioclass in {
2196 ProcessIOPriority.IOPRIO_CLASS_IDLE,
2197 ProcessIOPriority.IOPRIO_CLASS_NONE,
2198 }:
2199 msg = f"{ioclass!r} ioclass accepts no value"
2200 raise ValueError(msg)
2201 if value < 0 or value > 7:
2202 msg = "value not in 0-7 range"
2203 raise ValueError(msg)
2204 return _psutil.proc_ioprio_set(self.pid, ioclass, value)
2205
2206 if hasattr(resource, "prlimit"):
2207
2208 @wrap_exceptions
2209 def rlimit(self, resource_, limits=None):
2210 # If pid is 0 prlimit() applies to the calling process and
2211 # we don't want that. We should never get here though as
2212 # PID 0 is not supported on Linux.
2213 if self.pid == 0:
2214 msg = "can't use prlimit() against PID 0 process"
2215 raise ValueError(msg)
2216 try:
2217 if limits is None:
2218 # get
2219 soft, hard = resource.prlimit(self.pid, resource_)
2220 # Python 3.15 returns RLIM_INFINITY as the unsigned
2221 # 2**64-1 instead of -1; map it back for consistency.
2222 if soft == RLIM_INFINITY_UNSIGNED:
2223 soft = _psutil.RLIM_INFINITY
2224 if hard == RLIM_INFINITY_UNSIGNED:
2225 hard = _psutil.RLIM_INFINITY
2226 return soft, hard
2227 else:
2228 # set
2229 if len(limits) != 2:
2230 msg = (
2231 "second argument must be a (soft, hard) "
2232 f"tuple, got {limits!r}"
2233 )
2234 raise ValueError(msg)
2235 resource.prlimit(self.pid, resource_, limits)
2236 except OSError as err:
2237 if err.errno == errno.ENOSYS:
2238 # I saw this happening on Travis:
2239 # https://travis-ci.org/giampaolo/psutil/jobs/51368273
2240 self._raise_if_zombie()
2241 raise
2242
2243 @wrap_exceptions
2244 def status(self):
2245 letter = self._parse_stat_file()['status']
2246 letter = letter.decode()
2247 # XXX is '?' legit? (we're not supposed to return it anyway)
2248 return PROC_STATUSES.get(letter, '?')
2249
2250 @wrap_exceptions
2251 def open_files(self):
2252 retlist = []
2253 files = os.listdir(f"{self._procfs_path}/{self.pid}/fd")
2254 hit_enoent = False
2255 for fd in files:
2256 file = f"{self._procfs_path}/{self.pid}/fd/{fd}"
2257 try:
2258 path = readlink(file)
2259 except (FileNotFoundError, ProcessLookupError):
2260 # ENOENT == file which is gone in the meantime
2261 hit_enoent = True
2262 continue
2263 except OSError as err:
2264 if err.errno == errno.EINVAL:
2265 # not a link
2266 continue
2267 if err.errno == errno.ENAMETOOLONG:
2268 # file name too long
2269 debug(err)
2270 continue
2271 raise
2272 else:
2273 # If path is not an absolute there's no way to tell
2274 # whether it's a regular file or not, so we skip it.
2275 # A regular file is always supposed to be have an
2276 # absolute path though.
2277 if path.startswith('/') and isfile_strict(path):
2278 # Get file position and flags.
2279 file = f"{self._procfs_path}/{self.pid}/fdinfo/{fd}"
2280 try:
2281 with open_binary(file) as f:
2282 pos = int(f.readline().split()[1])
2283 flags = int(f.readline().split()[1], 8)
2284 except (FileNotFoundError, ProcessLookupError):
2285 # fd gone in the meantime; process may
2286 # still be alive
2287 hit_enoent = True
2288 else:
2289 mode = file_flags_to_mode(flags)
2290 ntuple = ntp.popenfile(
2291 path, int(fd), int(pos), mode, flags
2292 )
2293 retlist.append(ntuple)
2294 if hit_enoent:
2295 self._raise_if_not_alive()
2296 return retlist
2297
2298 @wrap_exceptions
2299 def net_connections(self, kind='inet'):
2300 ret = _net_connections.retrieve(kind, self.pid)
2301 self._raise_if_not_alive()
2302 return ret
2303
2304 @wrap_exceptions
2305 def num_fds(self):
2306 return len(os.listdir(f"{self._procfs_path}/{self.pid}/fd"))
2307
2308 @wrap_exceptions
2309 def ppid(self):
2310 return int(self._parse_stat_file()['ppid'])
2311
2312 @wrap_exceptions
2313 def uids(self, _uids_re=re.compile(br'Uid:\t(\d+)\t(\d+)\t(\d+)')):
2314 data = self._read_status_file()
2315 real, effective, saved = _uids_re.findall(data)[0]
2316 return ntp.puids(int(real), int(effective), int(saved))
2317
2318 @wrap_exceptions
2319 def gids(self, _gids_re=re.compile(br'Gid:\t(\d+)\t(\d+)\t(\d+)')):
2320 data = self._read_status_file()
2321 real, effective, saved = _gids_re.findall(data)[0]
2322 return ntp.pgids(int(real), int(effective), int(saved))