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