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