diff --git a/addon/globalPlugins/soundmanager/psutil/__init__.py b/addon/globalPlugins/soundmanager/psutil/__init__.py index e129965..8138db4 100644 --- a/addon/globalPlugins/soundmanager/psutil/__init__.py +++ b/addon/globalPlugins/soundmanager/psutil/__init__.py @@ -17,7 +17,7 @@ - Sun Solaris - AIX -Works with Python versions from 2.6 to 3.X. +Works with Python versions 2.7 and 3.6+. """ from __future__ import division @@ -25,40 +25,23 @@ import collections import contextlib import datetime -import errno import functools import os import signal import subprocess import sys +import threading import time -import traceback + + try: import pwd except ImportError: pwd = None from . import _common -from ._common import deprecated_method -from ._common import memoize -from ._common import memoize_when_activated -from ._common import wrap_numbers as _wrap_numbers -from ._compat import long -from ._compat import PY3 as _PY3 - -from ._common import STATUS_DEAD -from ._common import STATUS_DISK_SLEEP -from ._common import STATUS_IDLE -from ._common import STATUS_LOCKED -from ._common import STATUS_PARKED -from ._common import STATUS_RUNNING -from ._common import STATUS_SLEEPING -from ._common import STATUS_STOPPED -from ._common import STATUS_TRACING_STOP -from ._common import STATUS_WAITING -from ._common import STATUS_WAKING -from ._common import STATUS_ZOMBIE - +from ._common import AIX +from ._common import BSD from ._common import CONN_CLOSE from ._common import CONN_CLOSE_WAIT from ._common import CONN_CLOSING @@ -71,27 +54,45 @@ from ._common import CONN_SYN_RECV from ._common import CONN_SYN_SENT from ._common import CONN_TIME_WAIT -from ._common import NIC_DUPLEX_FULL -from ._common import NIC_DUPLEX_HALF -from ._common import NIC_DUPLEX_UNKNOWN - -from ._common import AIX -from ._common import BSD from ._common import FREEBSD # NOQA from ._common import LINUX from ._common import MACOS from ._common import NETBSD # NOQA +from ._common import NIC_DUPLEX_FULL +from ._common import NIC_DUPLEX_HALF +from ._common import NIC_DUPLEX_UNKNOWN from ._common import OPENBSD # NOQA from ._common import OSX # deprecated alias from ._common import POSIX # NOQA +from ._common import POWER_TIME_UNKNOWN +from ._common import POWER_TIME_UNLIMITED +from ._common import STATUS_DEAD +from ._common import STATUS_DISK_SLEEP +from ._common import STATUS_IDLE +from ._common import STATUS_LOCKED +from ._common import STATUS_PARKED +from ._common import STATUS_RUNNING +from ._common import STATUS_SLEEPING +from ._common import STATUS_STOPPED +from ._common import STATUS_TRACING_STOP +from ._common import STATUS_WAITING +from ._common import STATUS_WAKING +from ._common import STATUS_ZOMBIE from ._common import SUNOS from ._common import WINDOWS +from ._common import AccessDenied +from ._common import Error +from ._common import NoSuchProcess +from ._common import TimeoutExpired +from ._common import ZombieProcess +from ._common import memoize_when_activated +from ._common import wrap_numbers as _wrap_numbers +from ._compat import PY3 as _PY3 +from ._compat import PermissionError +from ._compat import ProcessLookupError +from ._compat import SubprocessTimeoutExpired as _SubprocessTimeoutExpired +from ._compat import long -from ._exceptions import AccessDenied -from ._exceptions import Error -from ._exceptions import NoSuchProcess -from ._exceptions import TimeoutExpired -from ._exceptions import ZombieProcess if LINUX: # This is public API and it will be retrieved from _pslinux.py @@ -99,49 +100,10 @@ PROCFS_PATH = "/proc" from . import _pslinux as _psplatform - from ._pslinux import IOPRIO_CLASS_BE # NOQA from ._pslinux import IOPRIO_CLASS_IDLE # NOQA from ._pslinux import IOPRIO_CLASS_NONE # NOQA from ._pslinux import IOPRIO_CLASS_RT # NOQA - # Linux >= 2.6.36 - if _psplatform.HAS_PRLIMIT: - from ._psutil_linux import RLIM_INFINITY # NOQA - from ._psutil_linux import RLIMIT_AS # NOQA - from ._psutil_linux import RLIMIT_CORE # NOQA - from ._psutil_linux import RLIMIT_CPU # NOQA - from ._psutil_linux import RLIMIT_DATA # NOQA - from ._psutil_linux import RLIMIT_FSIZE # NOQA - from ._psutil_linux import RLIMIT_LOCKS # NOQA - from ._psutil_linux import RLIMIT_MEMLOCK # NOQA - from ._psutil_linux import RLIMIT_NOFILE # NOQA - from ._psutil_linux import RLIMIT_NPROC # NOQA - from ._psutil_linux import RLIMIT_RSS # NOQA - from ._psutil_linux import RLIMIT_STACK # NOQA - # Kinda ugly but considerably faster than using hasattr() and - # setattr() against the module object (we are at import time: - # speed matters). - from . import _psutil_linux - try: - RLIMIT_MSGQUEUE = _psutil_linux.RLIMIT_MSGQUEUE - except AttributeError: - pass - try: - RLIMIT_NICE = _psutil_linux.RLIMIT_NICE - except AttributeError: - pass - try: - RLIMIT_RTPRIO = _psutil_linux.RLIMIT_RTPRIO - except AttributeError: - pass - try: - RLIMIT_RTTIME = _psutil_linux.RLIMIT_RTTIME - except AttributeError: - pass - try: - RLIMIT_SIGPENDING = _psutil_linux.RLIMIT_SIGPENDING - except AttributeError: - pass elif WINDOWS: from . import _pswindows as _psplatform @@ -152,6 +114,10 @@ from ._psutil_windows import NORMAL_PRIORITY_CLASS # NOQA from ._psutil_windows import REALTIME_PRIORITY_CLASS # NOQA from ._pswindows import CONN_DELETE_TCB # NOQA + from ._pswindows import IOPRIO_HIGH # NOQA + from ._pswindows import IOPRIO_LOW # NOQA + from ._pswindows import IOPRIO_NORMAL # NOQA + from ._pswindows import IOPRIO_VERYLOW # NOQA elif MACOS: from . import _psosx as _psplatform @@ -179,6 +145,7 @@ raise NotImplementedError('platform %s is not supported' % sys.platform) +# fmt: off __all__ = [ # exceptions "Error", "NoSuchProcess", "ZombieProcess", "AccessDenied", @@ -195,6 +162,7 @@ "CONN_ESTABLISHED", "CONN_SYN_SENT", "CONN_SYN_RECV", "CONN_FIN_WAIT1", "CONN_FIN_WAIT2", "CONN_TIME_WAIT", "CONN_CLOSE", "CONN_CLOSE_WAIT", "CONN_LAST_ACK", "CONN_LISTEN", "CONN_CLOSING", "CONN_NONE", + # "CONN_IDLE", "CONN_BOUND", "AF_LINK", @@ -205,6 +173,11 @@ "BSD", "FREEBSD", "LINUX", "NETBSD", "OPENBSD", "MACOS", "OSX", "POSIX", "SUNOS", "WINDOWS", "AIX", + # "RLIM_INFINITY", "RLIMIT_AS", "RLIMIT_CORE", "RLIMIT_CPU", "RLIMIT_DATA", + # "RLIMIT_FSIZE", "RLIMIT_LOCKS", "RLIMIT_MEMLOCK", "RLIMIT_NOFILE", + # "RLIMIT_NPROC", "RLIMIT_RSS", "RLIMIT_STACK", "RLIMIT_MSGQUEUE", + # "RLIMIT_NICE", "RLIMIT_RTPRIO", "RLIMIT_RTTIME", "RLIMIT_SIGPENDING", + # classes "Process", "Popen", @@ -212,23 +185,41 @@ "pid_exists", "pids", "process_iter", "wait_procs", # proc "virtual_memory", "swap_memory", # memory "cpu_times", "cpu_percent", "cpu_times_percent", "cpu_count", # cpu - "cpu_stats", # "cpu_freq", + "cpu_stats", # "cpu_freq", "getloadavg" "net_io_counters", "net_connections", "net_if_addrs", # network "net_if_stats", "disk_io_counters", "disk_partitions", "disk_usage", # disk # "sensors_temperatures", "sensors_battery", "sensors_fans" # sensors "users", "boot_time", # others ] +# fmt: on + + __all__.extend(_psplatform.__extra__all__) + +# Linux, FreeBSD +if hasattr(_psplatform.Process, "rlimit"): + # Populate global namespace with RLIM* constants. + from . import _psutil_posix + + _globals = globals() + _name = None + for _name in dir(_psutil_posix): + if _name.startswith('RLIM') and _name.isupper(): + _globals[_name] = getattr(_psutil_posix, _name) + __all__.append(_name) + del _globals, _name + +AF_LINK = _psplatform.AF_LINK + __author__ = "Giampaolo Rodola'" -__version__ = "5.4.7" +__version__ = "5.9.8" version_info = tuple([int(num) for num in __version__.split('.')]) -AF_LINK = _psplatform.AF_LINK -POWER_TIME_UNLIMITED = _common.POWER_TIME_UNLIMITED -POWER_TIME_UNKNOWN = _common.POWER_TIME_UNKNOWN -_TOTAL_PHYMEM = None -_timer = getattr(time, 'monotonic', time.time) +_timer = getattr(time, 'monotonic', time.time) +_TOTAL_PHYMEM = None +_LOWEST_PID = None +_SENTINEL = object() # Sanity check in case the user messed up with psutil installation # or did something weird with sys.path. In this case we might end @@ -236,18 +227,25 @@ # was compiled for a different version of psutil. # We want to prevent that by failing sooner rather than later. # See: https://github.com/giampaolo/psutil/issues/564 -if (int(__version__.replace('.', '')) != - getattr(_psplatform.cext, 'version', None)): - msg = "version conflict: %r C extension module was built for another " \ - "version of psutil" % getattr(_psplatform.cext, "__file__") +if int(__version__.replace('.', '')) != getattr( + _psplatform.cext, 'version', None +): + msg = "version conflict: %r C extension " % _psplatform.cext.__file__ + msg += "module was built for another version of psutil" if hasattr(_psplatform.cext, 'version'): msg += " (%s instead of %s)" % ( - '.'.join([x for x in str(_psplatform.cext.version)]), __version__) + '.'.join([x for x in str(_psplatform.cext.version)]), + __version__, + ) else: msg += " (different than %s)" % __version__ msg += "; you may try to 'pip uninstall psutil', manually remove %s" % ( - getattr(_psplatform.cext, "__file__", - "the existing psutil install directory")) + getattr( + _psplatform.cext, + "__file__", + "the existing psutil install directory", + ) + ) msg += " or clean the virtual env somehow, then reinstall" raise ImportError(msg) @@ -260,7 +258,8 @@ if hasattr(_psplatform, 'ppid_map'): # Faster version (Windows and Linux). _ppid_map = _psplatform.ppid_map -else: +else: # pragma: no cover + def _ppid_map(): """Return a {pid: ppid, ...} dict for all running processes in one shot. Used to speed up Process.children(). @@ -274,26 +273,11 @@ def _ppid_map(): return ret -def _assert_pid_not_reused(fun): - """Decorator which raises NoSuchProcess in case a process is no - longer running or its PID has been reused. - """ - @functools.wraps(fun) - def wrapper(self, *args, **kwargs): - if not self.is_running(): - raise NoSuchProcess(self.pid, self._name) - return fun(self, *args, **kwargs) - return wrapper - - def _pprint_secs(secs): """Format seconds in a human readable form.""" now = time.time() secs_ago = int(now - secs) - if secs_ago < 60 * 60 * 24: - fmt = "%H:%M:%S" - else: - fmt = "%Y-%m-%d %H:%M:%S" + fmt = "%H:%M:%S" if secs_ago < 60 * 60 * 24 else "%Y-%m-%d %H:%M:%S" return datetime.datetime.fromtimestamp(secs).strftime(fmt) @@ -302,7 +286,7 @@ def _pprint_secs(secs): # ===================================================================== -class Process(object): +class Process(object): # noqa: UP004 """Represents an OS process with the given PID. If PID is omitted current process PID (os.getpid()) is used. Raise NoSuchProcess if PID does not exist. @@ -332,7 +316,7 @@ class Process(object): - use is_running() before querying the process - if you're continuously iterating over a set of Process instances use process_iter() which pre-emptively checks - process identity for every yielded instance + process identity for every yielded instance """ def __init__(self, pid=None): @@ -343,17 +327,25 @@ def _init(self, pid, _ignore_nsp=False): pid = os.getpid() else: if not _PY3 and not isinstance(pid, (int, long)): - raise TypeError('pid must be an integer (got %r)' % pid) + msg = "pid must be an integer (got %r)" % pid + raise TypeError(msg) if pid < 0: - raise ValueError('pid must be a positive integer (got %s)' - % pid) + msg = "pid must be a positive integer (got %s)" % pid + raise ValueError(msg) + try: + _psplatform.cext.check_pid_range(pid) + except OverflowError: + msg = "process PID out of range (got %s)" % pid + raise NoSuchProcess(pid, msg=msg) + self._pid = pid self._name = None self._exe = None self._create_time = None self._gone = False + self._pid_reused = False self._hash = None - self._oneshot_inctx = False + self._lock = threading.RLock() # used for caching on Windows only (on POSIX ppid may change) self._ppid = None # platform-specific modules define an _psplatform.Process @@ -361,6 +353,7 @@ def _init(self, pid, _ignore_nsp=False): self._proc = _psplatform.Process(pid) self._last_sys_cpu_times = None self._last_proc_cpu_times = None + self._exitcode = _SENTINEL # cache creation time for later use in is_running() method try: self.create_time() @@ -375,36 +368,40 @@ def _init(self, pid, _ignore_nsp=False): pass except NoSuchProcess: if not _ignore_nsp: - msg = 'no process found with pid %s' % pid - raise NoSuchProcess(pid, None, msg) + msg = "process PID not found" + raise NoSuchProcess(pid, msg=msg) else: self._gone = True - # This pair is supposed to indentify a Process instance + # This pair is supposed to identify a Process instance # univocally over time (the PID alone is not enough as # it might refer to a process whose PID has been reused). # This will be used later in __eq__() and is_running(). self._ident = (self.pid, self._create_time) def __str__(self): - try: - info = collections.OrderedDict() - except AttributeError: - info = {} # Python 2.6 + info = collections.OrderedDict() info["pid"] = self.pid - try: - info["name"] = self.name() - if self._create_time: + if self._name: + info['name'] = self._name + with self.oneshot(): + try: + info["name"] = self.name() + info["status"] = self.status() + except ZombieProcess: + info["status"] = "zombie" + except NoSuchProcess: + info["status"] = "terminated" + except AccessDenied: + pass + if self._exitcode not in (_SENTINEL, None): + info["exitcode"] = self._exitcode + if self._create_time is not None: info['started'] = _pprint_secs(self._create_time) - except ZombieProcess: - info["status"] = "zombie" - except NoSuchProcess: - info["status"] = "terminated" - except AccessDenied: - pass - return "%s.%s(%s)" % ( - self.__class__.__module__, - self.__class__.__name__, - ", ".join(["%s=%r" % (k, v) for k, v in info.items()])) + return "%s.%s(%s)" % ( + self.__class__.__module__, + self.__class__.__name__, + ", ".join(["%s=%r" % (k, v) for k, v in info.items()]), + ) __repr__ = __str__ @@ -413,6 +410,20 @@ def __eq__(self, other): # on PID and creation time. if not isinstance(other, Process): return NotImplemented + if OPENBSD or NETBSD: # pragma: no cover + # Zombie processes on Open/NetBSD have a creation time of + # 0.0. This covers the case when a process started normally + # (so it has a ctime), then it turned into a zombie. It's + # important to do this because is_running() depends on + # __eq__. + pid1, ctime1 = self._ident + pid2, ctime2 = other._ident + if pid1 == pid2: + if ctime1 and not ctime2: + try: + return self.status() == STATUS_ZOMBIE + except Error: + pass return self._ident == other._ident def __ne__(self, other): @@ -423,6 +434,18 @@ def __hash__(self): self._hash = hash(self._ident) return self._hash + def _raise_if_pid_reused(self): + """Raises NoSuchProcess in case process PID has been reused.""" + if not self.is_running() and self._pid_reused: + # We may directly raise NSP in here already if PID is just + # not running, but I prefer NSP to be raised naturally by + # the actual Process API call. This way unit tests will tell + # us if the API is broken (aka don't raise NSP when it + # should). We also remain consistent with all other "get" + # APIs which don't use _raise_if_pid_reused(). + msg = "process no longer exists and its PID has been reused" + raise NoSuchProcess(self.pid, self._name, msg=msg) + @property def pid(self): """The process PID.""" @@ -457,40 +480,45 @@ def oneshot(self): ... >>> """ - if self._oneshot_inctx: - # NOOP: this covers the use case where the user enters the - # context twice. Since as_dict() internally uses oneshot() - # I expect that the code below will be a pretty common - # "mistake" that the user will make, so let's guard - # against that: - # - # >>> with p.oneshot(): - # ... p.as_dict() - # ... - yield - else: - self._oneshot_inctx = True - try: - # cached in case cpu_percent() is used - self.cpu_times.cache_activate() - # cached in case memory_percent() is used - self.memory_info.cache_activate() - # cached in case parent() is used - self.ppid.cache_activate() - # cached in case username() is used - if POSIX: - self.uids.cache_activate() - # specific implementation cache - self._proc.oneshot_enter() + with self._lock: + if hasattr(self, "_cache"): + # NOOP: this covers the use case where the user enters the + # context twice: + # + # >>> with p.oneshot(): + # ... with p.oneshot(): + # ... + # + # Also, since as_dict() internally uses oneshot() + # I expect that the code below will be a pretty common + # "mistake" that the user will make, so let's guard + # against that: + # + # >>> with p.oneshot(): + # ... p.as_dict() + # ... yield - finally: - self.cpu_times.cache_deactivate() - self.memory_info.cache_deactivate() - self.ppid.cache_deactivate() - if POSIX: - self.uids.cache_deactivate() - self._proc.oneshot_exit() - self._oneshot_inctx = False + else: + try: + # cached in case cpu_percent() is used + self.cpu_times.cache_activate(self) + # cached in case memory_percent() is used + self.memory_info.cache_activate(self) + # cached in case parent() is used + self.ppid.cache_activate(self) + # cached in case username() is used + if POSIX: + self.uids.cache_activate(self) + # specific implementation cache + self._proc.oneshot_enter() + yield + finally: + self.cpu_times.cache_deactivate(self) + self.memory_info.cache_deactivate(self) + self.ppid.cache_deactivate(self) + if POSIX: + self.uids.cache_deactivate(self) + self._proc.oneshot_exit() def as_dict(self, attrs=None, ad_value=None): """Utility method returning process information as a @@ -506,15 +534,18 @@ def as_dict(self, attrs=None, ad_value=None): valid_names = _as_dict_attrnames if attrs is not None: if not isinstance(attrs, (list, tuple, set, frozenset)): - raise TypeError("invalid attrs type %s" % type(attrs)) + msg = "invalid attrs type %s" % type(attrs) + raise TypeError(msg) attrs = set(attrs) invalid_names = attrs - valid_names if invalid_names: - raise ValueError("invalid attr name%s %s" % ( + msg = "invalid attr name%s %s" % ( "s" if len(invalid_names) > 1 else "", - ", ".join(map(repr, invalid_names)))) + ", ".join(map(repr, invalid_names)), + ) + raise ValueError(msg) - retdict = dict() + retdict = {} ls = attrs or valid_names with self.oneshot(): for name in ls: @@ -541,6 +572,9 @@ def parent(self): checking whether PID has been reused. If no parent is known return None. """ + lowest_pid = _LOWEST_PID if _LOWEST_PID is not None else pids()[0] + if self.pid == lowest_pid: + return None ppid = self.ppid() if ppid is not None: ctime = self.create_time() @@ -552,12 +586,23 @@ def parent(self): except NoSuchProcess: pass + def parents(self): + """Return the parents of this process as a list of Process + instances. If no parents are known return an empty list. + """ + parents = [] + proc = self.parent() + while proc is not None: + parents.append(proc) + proc = proc.parent() + return parents + def is_running(self): """Return whether this process is running. It also checks if PID has been reused by another process in which case return False. """ - if self._gone: + if self._gone or self._pid_reused: return False try: # Checking if PID is alive is not enough as the PID might @@ -565,7 +610,8 @@ def is_running(self): # verify process identity. # Process identity / uniqueness over time is guaranteed by # (PID + creation time) and that is verified in __eq__. - return self == Process(self.pid) + self._pid_reused = self != Process(self.pid) + return not self._pid_reused except ZombieProcess: # We should never get here as it's already handled in # Process.__init__; here just for extra safety. @@ -588,6 +634,7 @@ def ppid(self): # XXX should we check creation time here rather than in # Process.parent()? + self._raise_if_pid_reused() if POSIX: return self._proc.ppid() else: # pragma: no cover @@ -609,7 +656,12 @@ def name(self): # Examples are "gnome-keyring-d" vs. "gnome-keyring-daemon". try: cmdline = self.cmdline() - except AccessDenied: + except (AccessDenied, ZombieProcess): + # Just pass and return the truncated name: it's better + # than nothing. Note: there are actual cases where a + # zombie process can return a name() but not a + # cmdline(), see: + # https://github.com/giampaolo/psutil/issues/2239 pass else: if cmdline: @@ -625,6 +677,7 @@ def exe(self): May also be an empty string. The return value is cached after first call. """ + def guess_it(fallback): # try to guess exe from cmdline[0] in absence of a native # exe representation @@ -634,9 +687,11 @@ def guess_it(fallback): # Attempt to guess only in case of an absolute path. # It is not safe otherwise as the process might have # changed cwd. - if (os.path.isabs(exe) and - os.path.isfile(exe) and - os.access(exe, os.X_OK)): + if ( + os.path.isabs(exe) + and os.path.isfile(exe) + and os.access(exe, os.X_OK) + ): return exe if isinstance(fallback, AccessDenied): raise fallback @@ -677,8 +732,8 @@ def username(self): if POSIX: if pwd is None: # might happen if python was installed from sources - raise ImportError( - "requires pwd module shipped with standard python") + msg = "requires pwd module shipped with standard python" + raise ImportError(msg) real_uid = self.uids().real try: return pwd.getpwuid(real_uid).pw_name @@ -690,7 +745,7 @@ def username(self): def create_time(self): """The process creation time as a floating point number - expressed in seconds since the epoch, in UTC. + expressed in seconds since the epoch. The return value is cached after first call. """ if self._create_time is None: @@ -706,8 +761,7 @@ def nice(self, value=None): if value is None: return self._proc.nice_get() else: - if not self.is_running(): - raise NoSuchProcess(self.pid, self._name) + self._raise_if_pid_reused() self._proc.nice_set(value) if POSIX: @@ -749,7 +803,7 @@ def io_counters(self): """ return self._proc.io_counters() - # Linux and Windows >= Vista only + # Linux and Windows if hasattr(_psplatform.Process, "ionice_get"): def ionice(self, ioclass=None, value=None): @@ -766,12 +820,14 @@ def ionice(self, ioclass=None, value=None): """ if ioclass is None: if value is not None: - raise ValueError("'ioclass' argument must be specified") + msg = "'ioclass' argument must be specified" + raise ValueError(msg) return self._proc.ionice_get() else: + self._raise_if_pid_reused() return self._proc.ionice_set(ioclass, value) - # Linux only + # Linux / FreeBSD only if hasattr(_psplatform.Process, "rlimit"): def rlimit(self, resource, limits=None): @@ -779,15 +835,14 @@ def rlimit(self, resource, limits=None): tuple. *resource* is one of the RLIMIT_* constants. - *limits* is supposed to be a (soft, hard) tuple. + *limits* is supposed to be a (soft, hard) tuple. See "man prlimit" for further info. - Available on Linux only. + Available on Linux and FreeBSD only. """ - if limits is None: - return self._proc.rlimit(resource) - else: - return self._proc.rlimit(resource, limits) + if limits is not None: + self._raise_if_pid_reused() + return self._proc.rlimit(resource, limits) # Windows, Linux and FreeBSD only if hasattr(_psplatform.Process, "cpu_affinity_get"): @@ -800,12 +855,10 @@ def cpu_affinity(self, cpus=None): (and set). (Windows, Linux and BSD only). """ - # Automatically remove duplicates both on get and - # set (for get it's not really necessary, it's - # just for extra safety). if cpus is None: - return list(set(self._proc.cpu_affinity_get())) + return sorted(set(self._proc.cpu_affinity_get())) else: + self._raise_if_pid_reused() if not cpus: if hasattr(self._proc, "_get_eligible_cpus"): cpus = self._proc._get_eligible_cpus() @@ -826,12 +879,13 @@ def cpu_num(self): """ return self._proc.cpu_num() - # Linux, macOS and Windows only + # All platforms has it, but maybe not in the future. if hasattr(_psplatform.Process, "environ"): def environ(self): """The environment variables of the process as a dict. Note: this - might not reflect changes made after the process started. """ + might not reflect changes made after the process started. + """ return self._proc.environ() if WINDOWS: @@ -862,7 +916,6 @@ def threads(self): """ return self._proc.threads() - @_assert_pid_not_reused def children(self, recursive=False): """Return the children of this process as a list of Process instances, pre-emptively checking whether PID has been reused. @@ -889,6 +942,7 @@ def children(self, recursive=False): process Y won't be listed as the reference to process A is lost. """ + self._raise_if_pid_reused() ppid_map = _ppid_map() ret = [] if not recursive: @@ -969,7 +1023,8 @@ def cpu_percent(self, interval=None): """ blocking = interval is not None and interval > 0.0 if interval is not None and interval < 0: - raise ValueError("interval is not positive (got %r)" % interval) + msg = "interval is not positive (got %r)" % interval + raise ValueError(msg) num_cpus = cpu_count() or 1 def timer(): @@ -1001,7 +1056,7 @@ def timer(): # This is the utilization split evenly between all CPUs. # E.g. a busy loop process on a 2-CPU-cores system at this # point is reported as 50% instead of 100%. - overall_cpus_percent = ((delta_proc / delta_time) * 100) + overall_cpus_percent = (delta_proc / delta_time) * 100 except ZeroDivisionError: # interval was too low return 0.0 @@ -1040,13 +1095,13 @@ def memory_info(self): """Return a namedtuple with variable fields depending on the platform, representing memory information about the process. - The "portable" fields available on all plaforms are `rss` and `vms`. + The "portable" fields available on all platforms are `rss` and `vms`. All numbers are expressed in bytes. """ return self._proc.memory_info() - @deprecated_method(replacement="memory_info") + @_common.deprecated_method(replacement="memory_info") def memory_info_ex(self): return self.memory_info() @@ -1078,10 +1133,16 @@ def memory_percent(self, memtype="rss"): """ valid_types = list(_psplatform.pfullmem._fields) if memtype not in valid_types: - raise ValueError("invalid memtype %r; valid types are %r" % ( - memtype, tuple(valid_types))) - fun = self.memory_info if memtype in _psplatform.pmem._fields else \ - self.memory_full_info + msg = "invalid memtype %r; valid types are %r" % ( + memtype, + tuple(valid_types), + ) + raise ValueError(msg) + fun = ( + self.memory_info + if memtype in _psplatform.pmem._fields + else self.memory_full_info + ) metrics = fun() value = getattr(metrics, memtype) @@ -1089,14 +1150,15 @@ def memory_percent(self, memtype="rss"): total_phymem = _TOTAL_PHYMEM or virtual_memory().total if not total_phymem > 0: # we should never get here - raise ValueError( - "can't calculate process memory percent because " - "total physical system memory is not positive (%r)" - % total_phymem) + msg = ( + "can't calculate process memory percent because total physical" + " system memory is not positive (%r)" % (total_phymem) + ) + raise ValueError(msg) return (value / float(total_phymem)) * 100 if hasattr(_psplatform.Process, "memory_maps"): - # Available everywhere except OpenBSD and NetBSD. + def memory_maps(self, grouped=True): """Return process' mapped memory regions as a list of namedtuples whose fields are variable depending on the platform. @@ -1158,30 +1220,31 @@ def connections(self, kind='inet'): # --- signals if POSIX: + def _send_signal(self, sig): assert not self.pid < 0, self.pid + self._raise_if_pid_reused() if self.pid == 0: # see "man 2 kill" - raise ValueError( + msg = ( "preventing sending signal to process with PID 0 as it " "would affect every process in the process group of the " - "calling process (os.getpid()) instead of PID 0") + "calling process (os.getpid()) instead of PID 0" + ) + raise ValueError(msg) try: os.kill(self.pid, sig) - except OSError as err: - if err.errno == errno.ESRCH: - if OPENBSD and pid_exists(self.pid): - # We do this because os.kill() lies in case of - # zombie processes. - raise ZombieProcess(self.pid, self._name, self._ppid) - else: - self._gone = True - raise NoSuchProcess(self.pid, self._name) - if err.errno in (errno.EPERM, errno.EACCES): - raise AccessDenied(self.pid, self._name) - raise + except ProcessLookupError: + if OPENBSD and pid_exists(self.pid): + # We do this because os.kill() lies in case of + # zombie processes. + raise ZombieProcess(self.pid, self._name, self._ppid) + else: + self._gone = True + raise NoSuchProcess(self.pid, self._name) + except PermissionError: + raise AccessDenied(self.pid, self._name) - @_assert_pid_not_reused def send_signal(self, sig): """Send a signal *sig* to process pre-emptively checking whether PID has been reused (see signal module constants) . @@ -1191,29 +1254,23 @@ def send_signal(self, sig): if POSIX: self._send_signal(sig) else: # pragma: no cover - if sig == signal.SIGTERM: - self._proc.kill() - # py >= 2.7 - elif sig in (getattr(signal, "CTRL_C_EVENT", object()), - getattr(signal, "CTRL_BREAK_EVENT", object())): - self._proc.send_signal(sig) - else: - raise ValueError( - "only SIGTERM, CTRL_C_EVENT and CTRL_BREAK_EVENT signals " - "are supported on Windows") + self._raise_if_pid_reused() + if sig != signal.SIGTERM and not self.is_running(): + msg = "process no longer exists" + raise NoSuchProcess(self.pid, self._name, msg=msg) + self._proc.send_signal(sig) - @_assert_pid_not_reused def suspend(self): """Suspend process execution with SIGSTOP pre-emptively checking whether PID has been reused. - On Windows this has the effect ot suspending all process threads. + On Windows this has the effect of suspending all process threads. """ if POSIX: self._send_signal(signal.SIGSTOP) else: # pragma: no cover + self._raise_if_pid_reused() self._proc.suspend() - @_assert_pid_not_reused def resume(self): """Resume process execution with SIGCONT pre-emptively checking whether PID has been reused. @@ -1222,9 +1279,9 @@ def resume(self): if POSIX: self._send_signal(signal.SIGCONT) else: # pragma: no cover + self._raise_if_pid_reused() self._proc.resume() - @_assert_pid_not_reused def terminate(self): """Terminate the process with SIGTERM pre-emptively checking whether PID has been reused. @@ -1233,9 +1290,9 @@ def terminate(self): if POSIX: self._send_signal(signal.SIGTERM) else: # pragma: no cover + self._raise_if_pid_reused() self._proc.kill() - @_assert_pid_not_reused def kill(self): """Kill the current process with SIGKILL pre-emptively checking whether PID has been reused. @@ -1243,11 +1300,14 @@ def kill(self): if POSIX: self._send_signal(signal.SIGKILL) else: # pragma: no cover + self._raise_if_pid_reused() self._proc.kill() def wait(self, timeout=None): """Wait for process to terminate and, if process is a children of os.getpid(), also return its exit code, else None. + On Windows there's no such limitation (exit code is always + returned). If the process is already terminated immediately return None instead of raising NoSuchProcess. @@ -1258,8 +1318,22 @@ def wait(self, timeout=None): To wait for multiple Process(es) use psutil.wait_procs(). """ if timeout is not None and not timeout >= 0: - raise ValueError("timeout must be a positive integer") - return self._proc.wait(timeout) + msg = "timeout must be a positive integer" + raise ValueError(msg) + if self._exitcode is not _SENTINEL: + return self._exitcode + self._exitcode = self._proc.wait(timeout) + return self._exitcode + + +# The valid attr names which can be processed by Process.as_dict(). +# fmt: off +_as_dict_attrnames = set( + [x for x in dir(Process) if not x.startswith('_') and x not in + {'send_signal', 'suspend', 'resume', 'terminate', 'kill', 'wait', + 'is_running', 'as_dict', 'parent', 'parents', 'children', 'rlimit', + 'memory_info_ex', 'oneshot'}]) +# fmt: on # ===================================================================== @@ -1268,11 +1342,17 @@ def wait(self, timeout=None): class Popen(Process): - """A more convenient interface to stdlib subprocess.Popen class. - It starts a sub process and deals with it exactly as when using - subprocess.Popen class but in addition also provides all the - properties and methods of psutil.Process class as a unified - interface: + """Same as subprocess.Popen, but in addition it provides all + psutil.Process methods in a single class. + For the following methods which are common to both classes, psutil + implementation takes precedence: + + * send_signal() + * terminate() + * kill() + + This is done in order to avoid killing another process in case its + PID has been reused, fixing BPO-6973. >>> import psutil >>> from subprocess import PIPE @@ -1284,22 +1364,11 @@ class Popen(Process): >>> p.username() 'giampaolo' >>> p.communicate() - ('hi\n', None) + ('hi', None) >>> p.terminate() >>> p.wait(timeout=2) 0 >>> - - For method names common to both classes such as kill(), terminate() - and wait(), psutil.Process implementation takes precedence. - - Unlike subprocess.Popen this class pre-emptively checks whether PID - has been reused on send_signal(), terminate() and kill() so that - you don't accidentally terminate another process, fixing - http://bugs.python.org/issue6973. - - For a complete documentation refer to: - http://docs.python.org/library/subprocess.html """ def __init__(self, *args, **kwargs): @@ -1340,25 +1409,20 @@ def __getattribute__(self, name): try: return object.__getattribute__(self.__subproc, name) except AttributeError: - raise AttributeError("%s instance has no attribute '%s'" - % (self.__class__.__name__, name)) + msg = "%s instance has no attribute '%s'" % ( + self.__class__.__name__, + name, + ) + raise AttributeError(msg) def wait(self, timeout=None): if self.__subproc.returncode is not None: return self.__subproc.returncode - ret = super(Popen, self).wait(timeout) + ret = super(Popen, self).wait(timeout) # noqa self.__subproc.returncode = ret return ret -# The valid attr names which can be processed by Process.as_dict(). -_as_dict_attrnames = set( - [x for x in dir(Process) if not x.startswith('_') and x not in - ['send_signal', 'suspend', 'resume', 'terminate', 'kill', 'wait', - 'is_running', 'as_dict', 'parent', 'children', 'rlimit', - 'memory_info_ex', 'oneshot']]) - - # ===================================================================== # --- system processes related functions # ===================================================================== @@ -1366,7 +1430,10 @@ def wait(self, timeout=None): def pids(): """Return a list of current running PIDs.""" - return _psplatform.pids() + global _LOWEST_PID + ret = sorted(_psplatform.pids()) + _LOWEST_PID = ret[0] + return ret def pid_exists(pid): @@ -1411,53 +1478,60 @@ def process_iter(attrs=None, ad_value=None): If *attrs* is an empty list it will retrieve all process info (slow). """ + global _pmap + def add(pid): proc = Process(pid) if attrs is not None: proc.info = proc.as_dict(attrs=attrs, ad_value=ad_value) - _pmap[proc.pid] = proc + pmap[proc.pid] = proc return proc def remove(pid): - _pmap.pop(pid, None) + pmap.pop(pid, None) + pmap = _pmap.copy() a = set(pids()) - b = set(_pmap.keys()) + b = set(pmap.keys()) new_pids = a - b gone_pids = b - a - for pid in gone_pids: remove(pid) - for pid, proc in sorted(list(_pmap.items()) + - list(dict.fromkeys(new_pids).items())): - try: - if proc is None: # new process - yield add(pid) - else: - # use is_running() to check whether PID has been reused by - # another process in which case yield a new Process instance - if proc.is_running(): - if attrs is not None: - proc.info = proc.as_dict( - attrs=attrs, ad_value=ad_value) - yield proc - else: + try: + ls = sorted(list(pmap.items()) + list(dict.fromkeys(new_pids).items())) + for pid, proc in ls: + try: + if proc is None: # new process yield add(pid) - except NoSuchProcess: - remove(pid) - except AccessDenied: - # Process creation time can't be determined hence there's - # no way to tell whether the pid of the cached process - # has been reused. Just return the cached version. - if proc is None and pid in _pmap: - try: - yield _pmap[pid] - except KeyError: - # If we get here it is likely that 2 threads were - # using process_iter(). - pass - else: - raise + else: + # use is_running() to check whether PID has been + # reused by another process in which case yield a + # new Process instance + if proc.is_running(): + if attrs is not None: + proc.info = proc.as_dict( + attrs=attrs, ad_value=ad_value + ) + yield proc + else: + yield add(pid) + except NoSuchProcess: + remove(pid) + except AccessDenied: + # Process creation time can't be determined hence there's + # no way to tell whether the pid of the cached process + # has been reused. Just return the cached version. + if proc is None and pid in pmap: + try: + yield pmap[pid] + except KeyError: + # If we get here it is likely that 2 threads were + # using process_iter(). + pass + else: + raise + finally: + _pmap = pmap def wait_procs(procs, timeout=None, callback=None): @@ -1496,13 +1570,17 @@ def wait_procs(procs, timeout=None, callback=None): >>> for p in alive: ... p.kill() """ + def check_gone(proc, timeout): try: returncode = proc.wait(timeout=timeout) except TimeoutExpired: pass + except _SubprocessTimeoutExpired: + pass else: if returncode is not None or not proc.is_running(): + # Set new Process instance attribute. proc.returncode = returncode gone.add(proc) if callback is not None: @@ -1514,7 +1592,8 @@ def check_gone(proc, timeout): gone = set() alive = set(procs) if callback is not None and not callable(callback): - raise TypeError("callback %r is not a callable" % callable) + msg = "callback %r is not a callable" % callback + raise TypeError(msg) if timeout is not None: deadline = _timer() + timeout @@ -1570,7 +1649,7 @@ def cpu_count(logical=True): if logical: ret = _psplatform.cpu_count_logical() else: - ret = _psplatform.cpu_count_physical() + ret = _psplatform.cpu_count_cores() if ret is not None and ret < 1: ret = None return ret @@ -1605,18 +1684,18 @@ def cpu_times(percpu=False): try: - _last_cpu_times = cpu_times() -except Exception: + _last_cpu_times = {threading.current_thread().ident: cpu_times()} +except Exception: # noqa: BLE001 # Don't want to crash at import time. - _last_cpu_times = None - traceback.print_exc() + _last_cpu_times = {} try: - _last_per_cpu_times = cpu_times(percpu=True) -except Exception: + _last_per_cpu_times = { + threading.current_thread().ident: cpu_times(percpu=True) + } +except Exception: # noqa: BLE001 # Don't want to crash at import time. - _last_per_cpu_times = None - traceback.print_exc() + _last_per_cpu_times = {} def _cpu_tot_time(times): @@ -1710,15 +1789,14 @@ def cpu_percent(interval=None, percpu=False): 2.9 >>> """ - global _last_cpu_times - global _last_per_cpu_times + tid = threading.current_thread().ident blocking = interval is not None and interval > 0.0 if interval is not None and interval < 0: - raise ValueError("interval is not positive (got %r)" % interval) + msg = "interval is not positive (got %r)" % interval + raise ValueError(msg) def calculate(t1, t2): times_delta = _cpu_times_deltas(t1, t2) - all_delta = _cpu_tot_time(times_delta) busy_delta = _cpu_busy_time(times_delta) @@ -1735,14 +1813,9 @@ def calculate(t1, t2): t1 = cpu_times() time.sleep(interval) else: - t1 = _last_cpu_times - if t1 is None: - # Something bad happened at import time. We'll - # get a meaningful result on the next call. See: - # https://github.com/giampaolo/psutil/pull/715 - t1 = cpu_times() - _last_cpu_times = cpu_times() - return calculate(t1, _last_cpu_times) + t1 = _last_cpu_times.get(tid) or cpu_times() + _last_cpu_times[tid] = cpu_times() + return calculate(t1, _last_cpu_times[tid]) # per-cpu usage else: ret = [] @@ -1750,23 +1823,17 @@ def calculate(t1, t2): tot1 = cpu_times(percpu=True) time.sleep(interval) else: - tot1 = _last_per_cpu_times - if tot1 is None: - # Something bad happened at import time. We'll - # get a meaningful result on the next call. See: - # https://github.com/giampaolo/psutil/pull/715 - tot1 = cpu_times(percpu=True) - _last_per_cpu_times = cpu_times(percpu=True) - for t1, t2 in zip(tot1, _last_per_cpu_times): + tot1 = _last_per_cpu_times.get(tid) or cpu_times(percpu=True) + _last_per_cpu_times[tid] = cpu_times(percpu=True) + for t1, t2 in zip(tot1, _last_per_cpu_times[tid]): ret.append(calculate(t1, t2)) return ret -# Use separate global vars for cpu_times_percent() so that it's -# independent from cpu_percent() and they can both be used within -# the same program. -_last_cpu_times_2 = _last_cpu_times -_last_per_cpu_times_2 = _last_per_cpu_times +# Use a separate dict for cpu_times_percent(), so it's independent from +# cpu_percent() and they can both be used within the same program. +_last_cpu_times_2 = _last_cpu_times.copy() +_last_per_cpu_times_2 = _last_per_cpu_times.copy() def cpu_times_percent(interval=None, percpu=False): @@ -1782,11 +1849,11 @@ def cpu_times_percent(interval=None, percpu=False): *interval* and *percpu* arguments have the same meaning as in cpu_percent(). """ - global _last_cpu_times_2 - global _last_per_cpu_times_2 + tid = threading.current_thread().ident blocking = interval is not None and interval > 0.0 if interval is not None and interval < 0: - raise ValueError("interval is not positive (got %r)" % interval) + msg = "interval is not positive (got %r)" % interval + raise ValueError(msg) def calculate(t1, t2): nums = [] @@ -1811,14 +1878,9 @@ def calculate(t1, t2): t1 = cpu_times() time.sleep(interval) else: - t1 = _last_cpu_times_2 - if t1 is None: - # Something bad happened at import time. We'll - # get a meaningful result on the next call. See: - # https://github.com/giampaolo/psutil/pull/715 - t1 = cpu_times() - _last_cpu_times_2 = cpu_times() - return calculate(t1, _last_cpu_times_2) + t1 = _last_cpu_times_2.get(tid) or cpu_times() + _last_cpu_times_2[tid] = cpu_times() + return calculate(t1, _last_cpu_times_2[tid]) # per-cpu usage else: ret = [] @@ -1826,14 +1888,9 @@ def calculate(t1, t2): tot1 = cpu_times(percpu=True) time.sleep(interval) else: - tot1 = _last_per_cpu_times_2 - if tot1 is None: - # Something bad happened at import time. We'll - # get a meaningful result on the next call. See: - # https://github.com/giampaolo/psutil/pull/715 - tot1 = cpu_times(percpu=True) - _last_per_cpu_times_2 = cpu_times(percpu=True) - for t1, t2 in zip(tot1, _last_per_cpu_times_2): + tot1 = _last_per_cpu_times_2.get(tid) or cpu_times(percpu=True) + _last_per_cpu_times_2[tid] = cpu_times(percpu=True) + for t1, t2 in zip(tot1, _last_per_cpu_times_2[tid]): ret.append(calculate(t1, t2)) return ret @@ -1846,7 +1903,7 @@ def cpu_stats(): if hasattr(_psplatform, "cpu_freq"): def cpu_freq(percpu=False): - """Return CPU frequency as a nameduple including current, + """Return CPU frequency as a namedtuple including current, min and max frequency expressed in Mhz. If *percpu* is True and the system supports per-cpu frequency @@ -1864,18 +1921,41 @@ def cpu_freq(percpu=False): return ret[0] else: currs, mins, maxs = 0.0, 0.0, 0.0 + set_none = False for cpu in ret: currs += cpu.current + # On Linux if /proc/cpuinfo is used min/max are set + # to None. + if LINUX and cpu.min is None: + set_none = True + continue mins += cpu.min maxs += cpu.max + current = currs / num_cpus - min_ = mins / num_cpus - max_ = maxs / num_cpus + + if set_none: + min_ = max_ = None + else: + min_ = mins / num_cpus + max_ = maxs / num_cpus + return _common.scpufreq(current, min_, max_) __all__.append("cpu_freq") +if hasattr(os, "getloadavg") or hasattr(_psplatform, "getloadavg"): + # Perform this hasattr check once on import time to either use the + # platform based code or proxy straight from the os module. + if hasattr(os, "getloadavg"): + getloadavg = os.getloadavg + else: + getloadavg = _psplatform.getloadavg + + __all__.append("getloadavg") + + # ===================================================================== # --- system memory related functions # ===================================================================== @@ -1901,7 +1981,7 @@ def virtual_memory(): - used: memory used, calculated differently depending on the platform and designed for informational purposes only: - macOS: active + inactive + wired + macOS: active + wired BSD: active + wired + cached Linux: total - free @@ -1978,7 +2058,25 @@ def disk_partitions(all=False): If *all* parameter is False return physical devices only and ignore all others. """ - return _psplatform.disk_partitions(all) + + def pathconf(path, name): + try: + return os.pathconf(path, name) + except (OSError, AttributeError): + pass + + ret = _psplatform.disk_partitions(all) + if POSIX: + new = [] + for item in ret: + nt = item._replace( + maxfile=pathconf(item.mountpoint, 'PC_NAME_MAX'), + maxpath=pathconf(item.mountpoint, 'PC_PATH_MAX'), + ) + new.append(nt) + return new + else: + return ret def disk_io_counters(perdisk=False, nowrap=True): @@ -2025,11 +2123,12 @@ def disk_io_counters(perdisk=False, nowrap=True): rawdict[disk] = nt(*fields) return rawdict else: - return nt(*[sum(x) for x in zip(*rawdict.values())]) + return nt(*(sum(x) for x in zip(*rawdict.values()))) disk_io_counters.cache_clear = functools.partial( - _wrap_numbers.cache_clear, 'psutil.disk_io_counters') + _wrap_numbers.cache_clear, 'psutil.disk_io_counters' +) disk_io_counters.cache_clear.__doc__ = "Clears nowrap argument cache" @@ -2061,7 +2160,7 @@ def net_io_counters(pernic=False, nowrap=True): and wrap (restart from 0) and add "old value" to "new value" so that the returned numbers will always be increasing or remain the same, but never decrease. - "disk_io_counters.cache_clear()" can be used to invalidate the + "net_io_counters.cache_clear()" can be used to invalidate the cache. """ rawdict = _psplatform.net_io_counters() @@ -2078,7 +2177,8 @@ def net_io_counters(pernic=False, nowrap=True): net_io_counters.cache_clear = functools.partial( - _wrap_numbers.cache_clear, 'psutil.net_io_counters') + _wrap_numbers.cache_clear, 'psutil.net_io_counters' +) net_io_counters.cache_clear.__doc__ = "Clears nowrap argument cache" @@ -2129,7 +2229,7 @@ def net_if_addrs(): Note: you can have more than one address of the same family associated with each interface. """ - has_enums = sys.version_info >= (3, 4) + has_enums = _PY3 if has_enums: import socket rawlist = _psplatform.net_if_addrs() @@ -2142,8 +2242,10 @@ def net_if_addrs(): except ValueError: if WINDOWS and fam == -1: fam = _psplatform.AF_LINK - elif (hasattr(_psplatform, "AF_LINK") and - _psplatform.AF_LINK == fam): + elif ( + hasattr(_psplatform, "AF_LINK") + and fam == _psplatform.AF_LINK + ): # Linux defines AF_LINK as an alias for AF_PACKET. # We re-set the family here so that repr(family) # will show AF_LINK rather than AF_PACKET @@ -2190,6 +2292,7 @@ def sensors_temperatures(fahrenheit=False): All temperatures are expressed in celsius unless *fahrenheit* is set to True. """ + def convert(n): if n is not None: return (float(n) * 9 / 5) + 32 if fahrenheit else n @@ -2210,14 +2313,15 @@ def convert(n): high = critical ret[name].append( - _common.shwtemp(label, current, high, critical)) + _common.shwtemp(label, current, high, critical) + ) return dict(ret) __all__.append("sensors_temperatures") -# Linux, macOS +# Linux if hasattr(_psplatform, "sensors_fans"): def sensors_fans(): @@ -2296,20 +2400,29 @@ def win_service_get(name): # ===================================================================== -def test(): # pragma: no cover - """List info of all currently running processes emulating ps aux - output. +def _set_debug(value): + """Enable or disable PSUTIL_DEBUG option, which prints debugging + messages to stderr. """ + import psutil._common + + psutil._common.PSUTIL_DEBUG = bool(value) + _psplatform.cext.set_debug(bool(value)) + + +def test(): # pragma: no cover + from ._common import bytes2human + from ._compat import get_terminal_size + today_day = datetime.date.today() - templ = "%-10s %5s %4s %7s %7s %-13s %5s %7s %s" - attrs = ['pid', 'memory_percent', 'name', 'cpu_times', 'create_time', - 'memory_info'] - if POSIX: - attrs.append('uids') - attrs.append('terminal') - print(templ % ("USER", "PID", "%MEM", "VSZ", "RSS", "TTY", "START", "TIME", - "COMMAND")) - for p in process_iter(attrs=attrs, ad_value=''): + # fmt: off + templ = "%-10s %5s %5s %7s %7s %5s %6s %6s %6s %s" + attrs = ['pid', 'memory_percent', 'name', 'cmdline', 'cpu_times', + 'create_time', 'memory_info', 'status', 'nice', 'username'] + print(templ % ("USER", "PID", "%MEM", "VSZ", "RSS", "NICE", # NOQA + "STATUS", "START", "TIME", "CMDLINE")) + # fmt: on + for p in process_iter(attrs, ad_value=None): if p.info['create_time']: ctime = datetime.datetime.fromtimestamp(p.info['create_time']) if ctime.date() == today_day: @@ -2318,35 +2431,62 @@ def test(): # pragma: no cover ctime = ctime.strftime("%b%d") else: ctime = '' - cputime = time.strftime("%M:%S", - time.localtime(sum(p.info['cpu_times']))) - try: - user = p.username() - except Error: - user = '' - if WINDOWS and '\\' in user: + if p.info['cpu_times']: + cputime = time.strftime( + "%M:%S", time.localtime(sum(p.info['cpu_times'])) + ) + else: + cputime = '' + + user = p.info['username'] or '' + if not user and POSIX: + try: + user = p.uids()[0] + except Error: + pass + if user and WINDOWS and '\\' in user: user = user.split('\\')[1] - vms = p.info['memory_info'] and \ - int(p.info['memory_info'].vms / 1024) or '?' - rss = p.info['memory_info'] and \ - int(p.info['memory_info'].rss / 1024) or '?' - memp = p.info['memory_percent'] and \ - round(p.info['memory_percent'], 1) or '?' - print(templ % ( + user = user[:9] + vms = ( + bytes2human(p.info['memory_info'].vms) + if p.info['memory_info'] is not None + else '' + ) + rss = ( + bytes2human(p.info['memory_info'].rss) + if p.info['memory_info'] is not None + else '' + ) + memp = ( + round(p.info['memory_percent'], 1) + if p.info['memory_percent'] is not None + else '' + ) + nice = int(p.info['nice']) if p.info['nice'] else '' + if p.info['cmdline']: + cmdline = ' '.join(p.info['cmdline']) + else: + cmdline = p.info['name'] + status = p.info['status'][:5] if p.info['status'] else '' + + line = templ % ( user[:10], p.info['pid'], memp, vms, rss, - p.info.get('terminal', '') or '?', + nice, + status, ctime, cputime, - p.info['name'].strip() or '?')) + cmdline, + ) + print(line[: get_terminal_size()[0]]) # NOQA -del memoize, memoize_when_activated, division, deprecated_method +del memoize_when_activated, division if sys.version_info[0] < 3: - del num, x + del num, x # noqa if __name__ == "__main__": test() diff --git a/addon/globalPlugins/soundmanager/psutil/__init__.pyc b/addon/globalPlugins/soundmanager/psutil/__init__.pyc deleted file mode 100644 index 17efbc8..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/__init__.pyc and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/__pycache__/__init__.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..506286b Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/__pycache__/__init__.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/__pycache__/_common.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/__pycache__/_common.cpython-311.pyc new file mode 100644 index 0000000..f085444 Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/__pycache__/_common.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/__pycache__/_compat.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/__pycache__/_compat.cpython-311.pyc new file mode 100644 index 0000000..046dfd2 Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/__pycache__/_compat.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/__pycache__/_psaix.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/__pycache__/_psaix.cpython-311.pyc new file mode 100644 index 0000000..c7f13a4 Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/__pycache__/_psaix.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/__pycache__/_psbsd.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/__pycache__/_psbsd.cpython-311.pyc new file mode 100644 index 0000000..92ba6ee Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/__pycache__/_psbsd.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/__pycache__/_pslinux.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/__pycache__/_pslinux.cpython-311.pyc new file mode 100644 index 0000000..4d2b21d Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/__pycache__/_pslinux.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/__pycache__/_psosx.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/__pycache__/_psosx.cpython-311.pyc new file mode 100644 index 0000000..3ea86bb Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/__pycache__/_psosx.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/__pycache__/_psposix.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/__pycache__/_psposix.cpython-311.pyc new file mode 100644 index 0000000..590d0ef Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/__pycache__/_psposix.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/__pycache__/_pssunos.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/__pycache__/_pssunos.cpython-311.pyc new file mode 100644 index 0000000..33e45a4 Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/__pycache__/_pssunos.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/__pycache__/_pswindows.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/__pycache__/_pswindows.cpython-311.pyc new file mode 100644 index 0000000..818be55 Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/__pycache__/_pswindows.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/_common.py b/addon/globalPlugins/soundmanager/psutil/_common.py index 2cc3939..6989fea 100644 --- a/addon/globalPlugins/soundmanager/psutil/_common.py +++ b/addon/globalPlugins/soundmanager/psutil/_common.py @@ -8,7 +8,9 @@ # psutil or third-party modules. from __future__ import division +from __future__ import print_function +import collections import contextlib import errno import functools @@ -18,11 +20,12 @@ import sys import threading import warnings -from collections import defaultdict from collections import namedtuple from socket import AF_INET from socket import SOCK_DGRAM from socket import SOCK_STREAM + + try: from socket import AF_INET6 except ImportError: @@ -32,19 +35,23 @@ except ImportError: AF_UNIX = None -if sys.version_info >= (3, 4): + +# can't take it from _common.py as this script is imported by setup.py +PY3 = sys.version_info[0] >= 3 +if PY3: import enum else: enum = None -# can't take it from _common.py as this script is imported by setup.py -PY3 = sys.version_info[0] == 3 +PSUTIL_DEBUG = bool(os.getenv('PSUTIL_DEBUG')) +_DEFAULT = object() + +# fmt: off __all__ = [ - # constants + # OS constants 'FREEBSD', 'BSD', 'LINUX', 'NETBSD', 'OPENBSD', 'MACOS', 'OSX', 'POSIX', 'SUNOS', 'WINDOWS', - 'ENCODING', 'ENCODING_ERRS', 'AF_INET6', # connection constants 'CONN_CLOSE', 'CONN_CLOSE_WAIT', 'CONN_CLOSING', 'CONN_ESTABLISHED', 'CONN_FIN_WAIT1', 'CONN_FIN_WAIT2', 'CONN_LAST_ACK', 'CONN_LISTEN', @@ -56,6 +63,8 @@ 'STATUS_RUNNING', 'STATUS_SLEEPING', 'STATUS_STOPPED', 'STATUS_SUSPENDED', 'STATUS_TRACING_STOP', 'STATUS_WAITING', 'STATUS_WAKE_KILL', 'STATUS_WAKING', 'STATUS_ZOMBIE', 'STATUS_PARKED', + # other constants + 'ENCODING', 'ENCODING_ERRS', 'AF_INET6', # named tuples 'pconn', 'pcputimes', 'pctxsw', 'pgids', 'pio', 'pionice', 'popenfile', 'pthread', 'puids', 'sconn', 'scpustats', 'sdiskio', 'sdiskpart', @@ -64,7 +73,12 @@ 'conn_tmap', 'deprecated_method', 'isfile_strict', 'memoize', 'parse_environ_block', 'path_exists_strict', 'usage_percent', 'supports_ipv6', 'sockfam_to_enum', 'socktype_to_enum', "wrap_numbers", + 'open_text', 'open_binary', 'cat', 'bcat', + 'bytes2human', 'conn_to_ntuple', 'debug', + # shell utils + 'hilite', 'term_supports_colors', 'print_color', ] +# fmt: on # =================================================================== @@ -77,7 +91,7 @@ LINUX = sys.platform.startswith("linux") MACOS = sys.platform.startswith("darwin") OSX = MACOS # deprecated alias -FREEBSD = sys.platform.startswith("freebsd") +FREEBSD = sys.platform.startswith(("freebsd", "midnightbsd")) OPENBSD = sys.platform.startswith("openbsd") NETBSD = sys.platform.startswith("netbsd") BSD = FREEBSD or OPENBSD or NETBSD @@ -126,6 +140,7 @@ NIC_DUPLEX_HALF = 1 NIC_DUPLEX_UNKNOWN = 0 else: + class NicDuplex(enum.IntEnum): NIC_DUPLEX_FULL = 2 NIC_DUPLEX_HALF = 1 @@ -138,6 +153,7 @@ class NicDuplex(enum.IntEnum): POWER_TIME_UNKNOWN = -1 POWER_TIME_UNLIMITED = -2 else: + class BatteryTime(enum.IntEnum): POWER_TIME_UNKNOWN = -1 POWER_TIME_UNLIMITED = -2 @@ -162,6 +178,7 @@ class BatteryTime(enum.IntEnum): # --- for system functions +# fmt: off # psutil.swap_memory() sswap = namedtuple('sswap', ['total', 'used', 'free', 'percent', 'sin', 'sout']) @@ -172,7 +189,8 @@ class BatteryTime(enum.IntEnum): 'read_bytes', 'write_bytes', 'read_time', 'write_time']) # psutil.disk_partitions() -sdiskpart = namedtuple('sdiskpart', ['device', 'mountpoint', 'fstype', 'opts']) +sdiskpart = namedtuple('sdiskpart', ['device', 'mountpoint', 'fstype', 'opts', + 'maxfile', 'maxpath']) # psutil.net_io_counters() snetio = namedtuple('snetio', ['bytes_sent', 'bytes_recv', 'packets_sent', 'packets_recv', @@ -187,7 +205,8 @@ class BatteryTime(enum.IntEnum): snicaddr = namedtuple('snicaddr', ['family', 'address', 'netmask', 'broadcast', 'ptp']) # psutil.net_if_stats() -snicstats = namedtuple('snicstats', ['isup', 'duplex', 'speed', 'mtu']) +snicstats = namedtuple('snicstats', + ['isup', 'duplex', 'speed', 'mtu', 'flags']) # psutil.cpu_stats() scpustats = namedtuple( 'scpustats', ['ctx_switches', 'interrupts', 'soft_interrupts', 'syscalls']) @@ -200,12 +219,14 @@ class BatteryTime(enum.IntEnum): sbattery = namedtuple('sbattery', ['percent', 'secsleft', 'power_plugged']) # psutil.sensors_fans() sfan = namedtuple('sfan', ['label', 'current']) +# fmt: on # --- for Process methods # psutil.Process.cpu_times() -pcputimes = namedtuple('pcputimes', - ['user', 'system', 'children_user', 'children_system']) +pcputimes = namedtuple( + 'pcputimes', ['user', 'system', 'children_user', 'children_system'] +) # psutil.Process.open_files() popenfile = namedtuple('popenfile', ['path', 'fd']) # psutil.Process.threads() @@ -215,15 +236,17 @@ class BatteryTime(enum.IntEnum): # psutil.Process.gids() pgids = namedtuple('pgids', ['real', 'effective', 'saved']) # psutil.Process.io_counters() -pio = namedtuple('pio', ['read_count', 'write_count', - 'read_bytes', 'write_bytes']) +pio = namedtuple( + 'pio', ['read_count', 'write_count', 'read_bytes', 'write_bytes'] +) # psutil.Process.ionice() pionice = namedtuple('pionice', ['ioclass', 'value']) # psutil.Process.ctx_switches() pctxsw = namedtuple('pctxsw', ['voluntary', 'involuntary']) # psutil.Process.connections() -pconn = namedtuple('pconn', ['fd', 'family', 'type', 'laddr', 'raddr', - 'status']) +pconn = namedtuple( + 'pconn', ['fd', 'family', 'type', 'laddr', 'raddr', 'status'] +) # psutil.connections() and psutil.Process.connections() addr = namedtuple('addr', ['ip', 'port']) @@ -252,11 +275,104 @@ class BatteryTime(enum.IntEnum): }) if AF_UNIX is not None: - conn_tmap.update({ - "unix": ([AF_UNIX], [SOCK_STREAM, SOCK_DGRAM]), - }) + conn_tmap.update({"unix": ([AF_UNIX], [SOCK_STREAM, SOCK_DGRAM])}) + + +# ===================================================================== +# --- Exceptions +# ===================================================================== + + +class Error(Exception): + """Base exception class. All other psutil exceptions inherit + from this one. + """ + + __module__ = 'psutil' + + def _infodict(self, attrs): + info = collections.OrderedDict() + for name in attrs: + value = getattr(self, name, None) + if value: # noqa + info[name] = value + elif name == "pid" and value == 0: + info[name] = value + return info + + def __str__(self): + # invoked on `raise Error` + info = self._infodict(("pid", "ppid", "name")) + if info: + details = "(%s)" % ", ".join( + ["%s=%r" % (k, v) for k, v in info.items()] + ) + else: + details = None + return " ".join([x for x in (getattr(self, "msg", ""), details) if x]) + + def __repr__(self): + # invoked on `repr(Error)` + info = self._infodict(("pid", "ppid", "name", "seconds", "msg")) + details = ", ".join(["%s=%r" % (k, v) for k, v in info.items()]) + return "psutil.%s(%s)" % (self.__class__.__name__, details) + + +class NoSuchProcess(Error): + """Exception raised when a process with a certain PID doesn't + or no longer exists. + """ + + __module__ = 'psutil' + + def __init__(self, pid, name=None, msg=None): + Error.__init__(self) + self.pid = pid + self.name = name + self.msg = msg or "process no longer exists" + + +class ZombieProcess(NoSuchProcess): + """Exception raised when querying a zombie process. This is + raised on macOS, BSD and Solaris only, and not always: depending + on the query the OS may be able to succeed anyway. + On Linux all zombie processes are querable (hence this is never + raised). Windows doesn't have zombie processes. + """ + + __module__ = 'psutil' + + def __init__(self, pid, name=None, ppid=None, msg=None): + NoSuchProcess.__init__(self, pid, name, msg) + self.ppid = ppid + self.msg = msg or "PID still exists but it's a zombie" -del AF_INET, AF_UNIX, SOCK_STREAM, SOCK_DGRAM + +class AccessDenied(Error): + """Exception raised when permission to perform an action is denied.""" + + __module__ = 'psutil' + + def __init__(self, pid=None, name=None, msg=None): + Error.__init__(self) + self.pid = pid + self.name = name + self.msg = msg or "" + + +class TimeoutExpired(Error): + """Raised on Process.wait(timeout) if timeout expires and process + is still alive. + """ + + __module__ = 'psutil' + + def __init__(self, seconds, pid=None, name=None): + Error.__init__(self) + self.seconds = seconds + self.pid = pid + self.name = name + self.msg = "timeout after %s seconds" % seconds # =================================================================== @@ -264,15 +380,35 @@ class BatteryTime(enum.IntEnum): # =================================================================== +# This should be in _compat.py rather than here, but does not work well +# with setup.py importing this module via a sys.path trick. +if PY3: + if isinstance(__builtins__, dict): # cpython + exec_ = __builtins__["exec"] + else: # pypy + exec_ = getattr(__builtins__, "exec") # noqa + + exec_("""def raise_from(value, from_value): + try: + raise value from from_value + finally: + value = None + """) +else: + + def raise_from(value, from_value): + raise value + + def usage_percent(used, total, round_=None): """Calculate percentage usage of 'used' against 'total'.""" try: - ret = (used / total) * 100 + ret = (float(used) / total) * 100 except ZeroDivisionError: - ret = 0.0 if isinstance(used, float) or isinstance(total, float) else 0 - if round_ is not None: - return round(ret, round_) + return 0.0 else: + if round_ is not None: + ret = round(ret, round_) return ret @@ -289,14 +425,27 @@ def memoize(fun): 1 >>> foo.cache_clear() >>> + + It supports: + - functions + - classes (acts as a @singleton) + - staticmethods + - classmethods + + It does NOT support: + - methods """ + @functools.wraps(fun) def wrapper(*args, **kwargs): key = (args, frozenset(sorted(kwargs.items()))) try: return cache[key] except KeyError: - ret = cache[key] = fun(*args, **kwargs) + try: + ret = cache[key] = fun(*args, **kwargs) + except Exception as err: # noqa: BLE001 + raise raise_from(err, None) return ret def cache_clear(): @@ -327,35 +476,53 @@ def memoize_when_activated(fun): 1 >>> >>> # activated - >>> foo.cache_activate() + >>> foo.cache_activate(self) >>> foo() 1 >>> foo() >>> foo() >>> """ + @functools.wraps(fun) def wrapper(self): - if not wrapper.cache_activated: - return fun(self) - else: + try: + # case 1: we previously entered oneshot() ctx + ret = self._cache[fun] + except AttributeError: + # case 2: we never entered oneshot() ctx try: - ret = cache[fun] - except KeyError: - ret = cache[fun] = fun(self) - return ret + return fun(self) + except Exception as err: # noqa: BLE001 + raise raise_from(err, None) + except KeyError: + # case 3: we entered oneshot() ctx but there's no cache + # for this entry yet + try: + ret = fun(self) + except Exception as err: # noqa: BLE001 + raise raise_from(err, None) + try: + self._cache[fun] = ret + except AttributeError: + # multi-threading race condition, see: + # https://github.com/giampaolo/psutil/issues/1948 + pass + return ret - def cache_activate(): - """Activate cache.""" - wrapper.cache_activated = True + def cache_activate(proc): + """Activate cache. Expects a Process instance. Cache will be + stored as a "_cache" instance attribute. + """ + proc._cache = {} - def cache_deactivate(): + def cache_deactivate(proc): """Deactivate and clear cache.""" - wrapper.cache_activated = False - cache.clear() + try: + del proc._cache + except AttributeError: + pass - cache = {} - wrapper.cache_activated = False wrapper.cache_activate = cache_activate wrapper.cache_deactivate = cache_deactivate return wrapper @@ -364,7 +531,7 @@ def cache_deactivate(): def isfile_strict(path): """Same as os.path.isfile() but does not swallow EACCES / EPERM exceptions, see: - http://mail.python.org/pipermail/python-dev/2012-June/120787.html + http://mail.python.org/pipermail/python-dev/2012-June/120787.html. """ try: st = os.stat(path) @@ -378,8 +545,8 @@ def isfile_strict(path): def path_exists_strict(path): """Same as os.path.exists() but does not swallow EACCES / EPERM - exceptions, see: - http://mail.python.org/pipermail/python-dev/2012-June/120787.html + exceptions. See: + http://mail.python.org/pipermail/python-dev/2012-June/120787.html. """ try: os.stat(path) @@ -423,7 +590,7 @@ def parse_environ_block(data): equal_pos = data.find("=", pos, next_pos) if equal_pos > pos: key = data[pos:equal_pos] - value = data[equal_pos + 1:next_pos] + value = data[equal_pos + 1 : next_pos] # Windows expects environment variables to be uppercase only if WINDOWS_: key = key.upper() @@ -442,7 +609,7 @@ def sockfam_to_enum(num): else: # pragma: no cover try: return socket.AddressFamily(num) - except (ValueError, AttributeError): + except ValueError: return num @@ -454,26 +621,50 @@ def socktype_to_enum(num): return num else: # pragma: no cover try: - return socket.AddressType(num) - except (ValueError, AttributeError): + return socket.SocketKind(num) + except ValueError: return num +def conn_to_ntuple(fd, fam, type_, laddr, raddr, status, status_map, pid=None): + """Convert a raw connection tuple to a proper ntuple.""" + if fam in (socket.AF_INET, AF_INET6): + if laddr: + laddr = addr(*laddr) + if raddr: + raddr = addr(*raddr) + if type_ == socket.SOCK_STREAM and fam in (AF_INET, AF_INET6): + status = status_map.get(status, CONN_NONE) + else: + status = CONN_NONE # ignore whatever C returned to us + fam = sockfam_to_enum(fam) + type_ = socktype_to_enum(type_) + if pid is None: + return pconn(fd, fam, type_, laddr, raddr, status) + else: + return sconn(fd, fam, type_, laddr, raddr, status, pid) + + def deprecated_method(replacement): """A decorator which can be used to mark a method as deprecated 'replcement' is the method name which will be called instead. """ + def outer(fun): msg = "%s() is deprecated and will be removed; use %s() instead" % ( - fun.__name__, replacement) + fun.__name__, + replacement, + ) if fun.__doc__ is None: fun.__doc__ = msg @functools.wraps(fun) def inner(self, *args, **kwargs): - warnings.warn(msg, category=FutureWarning, stacklevel=2) + warnings.warn(msg, category=DeprecationWarning, stacklevel=2) return getattr(self, replacement)(*args, **kwargs) + return inner + return outer @@ -493,8 +684,8 @@ def _add_dict(self, input_dict, name): assert name not in self.reminders assert name not in self.reminder_keys self.cache[name] = input_dict - self.reminders[name] = defaultdict(int) - self.reminder_keys[name] = defaultdict(set) + self.reminders[name] = collections.defaultdict(int) + self.reminder_keys[name] = collections.defaultdict(set) def _remove_dead_reminders(self, input_dict, name): """In case the number of keys changed between calls (e.g. a @@ -509,7 +700,7 @@ def _remove_dead_reminders(self, input_dict, name): def run(self, input_dict, name): """Cache dict and sum numbers which overflow and wrap. - Return an updated copy of `input_dict` + Return an updated copy of `input_dict`. """ if name not in self.cache: # This was the first call. @@ -520,7 +711,7 @@ def run(self, input_dict, name): old_dict = self.cache[name] new_dict = {} - for key in input_dict.keys(): + for key in input_dict: input_tuple = input_dict[key] try: old_tuple = old_dict[key] @@ -576,3 +767,217 @@ def wrap_numbers(input_dict, name): _wn = _WrapNumbers() wrap_numbers.cache_clear = _wn.cache_clear wrap_numbers.cache_info = _wn.cache_info + + +# The read buffer size for open() builtin. This (also) dictates how +# much data we read(2) when iterating over file lines as in: +# >>> with open(file) as f: +# ... for line in f: +# ... ... +# Default per-line buffer size for binary files is 1K. For text files +# is 8K. We use a bigger buffer (32K) in order to have more consistent +# results when reading /proc pseudo files on Linux, see: +# https://github.com/giampaolo/psutil/issues/2050 +# On Python 2 this also speeds up the reading of big files: +# (namely /proc/{pid}/smaps and /proc/net/*): +# https://github.com/giampaolo/psutil/issues/708 +FILE_READ_BUFFER_SIZE = 32 * 1024 + + +def open_binary(fname): + return open(fname, "rb", buffering=FILE_READ_BUFFER_SIZE) + + +def open_text(fname): + """On Python 3 opens a file in text mode by using fs encoding and + a proper en/decoding errors handler. + On Python 2 this is just an alias for open(name, 'rt'). + """ + if not PY3: + return open(fname, buffering=FILE_READ_BUFFER_SIZE) + + # See: + # https://github.com/giampaolo/psutil/issues/675 + # https://github.com/giampaolo/psutil/pull/733 + fobj = open( + fname, + buffering=FILE_READ_BUFFER_SIZE, + encoding=ENCODING, + errors=ENCODING_ERRS, + ) + try: + # Dictates per-line read(2) buffer size. Defaults is 8k. See: + # https://github.com/giampaolo/psutil/issues/2050#issuecomment-1013387546 + fobj._CHUNK_SIZE = FILE_READ_BUFFER_SIZE + except AttributeError: + pass + except Exception: + fobj.close() + raise + + return fobj + + +def cat(fname, fallback=_DEFAULT, _open=open_text): + """Read entire file content and return it as a string. File is + opened in text mode. If specified, `fallback` is the value + returned in case of error, either if the file does not exist or + it can't be read(). + """ + if fallback is _DEFAULT: + with _open(fname) as f: + return f.read() + else: + try: + with _open(fname) as f: + return f.read() + except (IOError, OSError): + return fallback + + +def bcat(fname, fallback=_DEFAULT): + """Same as above but opens file in binary mode.""" + return cat(fname, fallback=fallback, _open=open_binary) + + +def bytes2human(n, format="%(value).1f%(symbol)s"): + """Used by various scripts. See: http://goo.gl/zeJZl. + + >>> bytes2human(10000) + '9.8K' + >>> bytes2human(100001221) + '95.4M' + """ + symbols = ('B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') + prefix = {} + for i, s in enumerate(symbols[1:]): + prefix[s] = 1 << (i + 1) * 10 + for symbol in reversed(symbols[1:]): + if abs(n) >= prefix[symbol]: + value = float(n) / prefix[symbol] + return format % locals() + return format % dict(symbol=symbols[0], value=n) + + +def get_procfs_path(): + """Return updated psutil.PROCFS_PATH constant.""" + return sys.modules['psutil'].PROCFS_PATH + + +if PY3: + + def decode(s): + return s.decode(encoding=ENCODING, errors=ENCODING_ERRS) + +else: + + def decode(s): + return s + + +# ===================================================================== +# --- shell utils +# ===================================================================== + + +@memoize +def term_supports_colors(file=sys.stdout): # pragma: no cover + if os.name == 'nt': + return True + try: + import curses + + assert file.isatty() + curses.setupterm() + assert curses.tigetnum("colors") > 0 + except Exception: # noqa: BLE001 + return False + else: + return True + + +def hilite(s, color=None, bold=False): # pragma: no cover + """Return an highlighted version of 'string'.""" + if not term_supports_colors(): + return s + attr = [] + colors = dict( + blue='34', + brown='33', + darkgrey='30', + green='32', + grey='37', + lightblue='36', + red='91', + violet='35', + yellow='93', + ) + colors[None] = '29' + try: + color = colors[color] + except KeyError: + raise ValueError( + "invalid color %r; choose between %s" % (list(colors.keys())) + ) + attr.append(color) + if bold: + attr.append('1') + return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), s) + + +def print_color( + s, color=None, bold=False, file=sys.stdout +): # pragma: no cover + """Print a colorized version of string.""" + if not term_supports_colors(): + print(s, file=file) # NOQA + elif POSIX: + print(hilite(s, color, bold), file=file) # NOQA + else: + import ctypes + + DEFAULT_COLOR = 7 + GetStdHandle = ctypes.windll.Kernel32.GetStdHandle + SetConsoleTextAttribute = ( + ctypes.windll.Kernel32.SetConsoleTextAttribute + ) + + colors = dict(green=2, red=4, brown=6, yellow=6) + colors[None] = DEFAULT_COLOR + try: + color = colors[color] + except KeyError: + raise ValueError( + "invalid color %r; choose between %r" + % (color, list(colors.keys())) + ) + if bold and color <= 7: + color += 8 + + handle_id = -12 if file is sys.stderr else -11 + GetStdHandle.restype = ctypes.c_ulong + handle = GetStdHandle(handle_id) + SetConsoleTextAttribute(handle, color) + try: + print(s, file=file) # NOQA + finally: + SetConsoleTextAttribute(handle, DEFAULT_COLOR) + + +def debug(msg): + """If PSUTIL_DEBUG env var is set, print a debug message to stderr.""" + if PSUTIL_DEBUG: + import inspect + + fname, lineno, _, lines, index = inspect.getframeinfo( + inspect.currentframe().f_back + ) + if isinstance(msg, Exception): + if isinstance(msg, (OSError, IOError, EnvironmentError)): + # ...because str(exc) may contain info about the file name + msg = "ignoring %s" % msg + else: + msg = "ignoring %r" % msg + print( # noqa + "psutil-debug [%s:%s]> %s" % (fname, lineno, msg), file=sys.stderr + ) diff --git a/addon/globalPlugins/soundmanager/psutil/_common.pyc b/addon/globalPlugins/soundmanager/psutil/_common.pyc deleted file mode 100644 index 0654529..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/_common.pyc and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/_compat.py b/addon/globalPlugins/soundmanager/psutil/_compat.py index 08aefe4..3db56c6 100644 --- a/addon/globalPlugins/soundmanager/psutil/_compat.py +++ b/addon/globalPlugins/soundmanager/psutil/_compat.py @@ -2,32 +2,60 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -"""Module which provides compatibility with older Python versions.""" +"""Module which provides compatibility with older Python versions. +This is more future-compatible rather than the opposite (prefer latest +Python 3 way of doing things). +""" import collections +import contextlib +import errno import functools import os import sys +import types -__all__ = ["PY3", "long", "xrange", "unicode", "basestring", "u", "b", - "lru_cache", "which"] -PY3 = sys.version_info[0] == 3 +# fmt: off +__all__ = [ + # constants + "PY3", + # builtins + "long", "range", "super", "unicode", "basestring", + # literals + "u", "b", + # collections module + "lru_cache", + # shutil module + "which", "get_terminal_size", + # contextlib module + "redirect_stderr", + # python 3 exceptions + "FileNotFoundError", "PermissionError", "ProcessLookupError", + "InterruptedError", "ChildProcessError", "FileExistsError", +] +# fmt: on + + +PY3 = sys.version_info[0] >= 3 +_SENTINEL = object() if PY3: long = int xrange = range unicode = str basestring = str + range = range def u(s): return s def b(s): return s.encode("latin-1") + else: long = long - xrange = xrange + range = xrange unicode = unicode basestring = basestring @@ -38,6 +66,153 @@ def b(s): return s +# --- builtins + + +# Python 3 super(). +# Taken from "future" package. +# Credit: Ryan Kelly +if PY3: + super = super +else: + _builtin_super = super + + def super(type_=_SENTINEL, type_or_obj=_SENTINEL, framedepth=1): + """Like Python 3 builtin super(). If called without any arguments + it attempts to infer them at runtime. + """ + if type_ is _SENTINEL: + f = sys._getframe(framedepth) + try: + # Get the function's first positional argument. + type_or_obj = f.f_locals[f.f_code.co_varnames[0]] + except (IndexError, KeyError): + msg = 'super() used in a function with no args' + raise RuntimeError(msg) + try: + # Get the MRO so we can crawl it. + mro = type_or_obj.__mro__ + except (AttributeError, RuntimeError): + try: + mro = type_or_obj.__class__.__mro__ + except AttributeError: + msg = 'super() used in a non-newstyle class' + raise RuntimeError(msg) + for type_ in mro: + # Find the class that owns the currently-executing method. + for meth in type_.__dict__.values(): + # Drill down through any wrappers to the underlying func. + # This handles e.g. classmethod() and staticmethod(). + try: + while not isinstance(meth, types.FunctionType): + if isinstance(meth, property): + # Calling __get__ on the property will invoke + # user code which might throw exceptions or + # have side effects + meth = meth.fget + else: + try: + meth = meth.__func__ + except AttributeError: + meth = meth.__get__(type_or_obj, type_) + except (AttributeError, TypeError): + continue + if meth.func_code is f.f_code: + break # found + else: + # Not found. Move onto the next class in MRO. + continue + break # found + else: + msg = 'super() called outside a method' + raise RuntimeError(msg) + + # Dispatch to builtin super(). + if type_or_obj is not _SENTINEL: + return _builtin_super(type_, type_or_obj) + return _builtin_super(type_) + + +# --- exceptions + + +if PY3: + FileNotFoundError = FileNotFoundError # NOQA + PermissionError = PermissionError # NOQA + ProcessLookupError = ProcessLookupError # NOQA + InterruptedError = InterruptedError # NOQA + ChildProcessError = ChildProcessError # NOQA + FileExistsError = FileExistsError # NOQA +else: + # https://github.com/PythonCharmers/python-future/blob/exceptions/ + # src/future/types/exceptions/pep3151.py + import platform + + def _instance_checking_exception(base_exception=Exception): + def wrapped(instance_checker): + class TemporaryClass(base_exception): + def __init__(self, *args, **kwargs): + if len(args) == 1 and isinstance(args[0], TemporaryClass): + unwrap_me = args[0] + for attr in dir(unwrap_me): + if not attr.startswith('__'): + setattr(self, attr, getattr(unwrap_me, attr)) + else: + super(TemporaryClass, self).__init__( # noqa + *args, **kwargs + ) + + class __metaclass__(type): + def __instancecheck__(cls, inst): + return instance_checker(inst) + + def __subclasscheck__(cls, classinfo): + value = sys.exc_info()[1] + return isinstance(value, cls) + + TemporaryClass.__name__ = instance_checker.__name__ + TemporaryClass.__doc__ = instance_checker.__doc__ + return TemporaryClass + + return wrapped + + @_instance_checking_exception(EnvironmentError) + def FileNotFoundError(inst): + return getattr(inst, 'errno', _SENTINEL) == errno.ENOENT + + @_instance_checking_exception(EnvironmentError) + def ProcessLookupError(inst): + return getattr(inst, 'errno', _SENTINEL) == errno.ESRCH + + @_instance_checking_exception(EnvironmentError) + def PermissionError(inst): + return getattr(inst, 'errno', _SENTINEL) in (errno.EACCES, errno.EPERM) + + @_instance_checking_exception(EnvironmentError) + def InterruptedError(inst): + return getattr(inst, 'errno', _SENTINEL) == errno.EINTR + + @_instance_checking_exception(EnvironmentError) + def ChildProcessError(inst): + return getattr(inst, 'errno', _SENTINEL) == errno.ECHILD + + @_instance_checking_exception(EnvironmentError) + def FileExistsError(inst): + return getattr(inst, 'errno', _SENTINEL) == errno.EEXIST + + if platform.python_implementation() != "CPython": + try: + raise OSError(errno.EEXIST, "perm") + except FileExistsError: + pass + except OSError: + msg = ( + "broken or incompatible Python implementation, see: " + "https://github.com/giampaolo/psutil/issues/1659" + ) + raise RuntimeError(msg) + + # --- stdlib additions @@ -53,10 +228,11 @@ def b(s): from dummy_threading import RLock _CacheInfo = collections.namedtuple( - "CacheInfo", ["hits", "misses", "maxsize", "currsize"]) + "CacheInfo", ["hits", "misses", "maxsize", "currsize"] + ) class _HashedSeq(list): - __slots__ = 'hashvalue' + __slots__ = ('hashvalue',) def __init__(self, tup, hash=hash): self[:] = tup @@ -65,10 +241,17 @@ def __init__(self, tup, hash=hash): def __hash__(self): return self.hashvalue - def _make_key(args, kwds, typed, - kwd_mark=(object(), ), - fasttypes=set((int, str, frozenset, type(None))), - sorted=sorted, tuple=tuple, type=type, len=len): + def _make_key( + args, + kwds, + typed, + kwd_mark=(_SENTINEL,), + fasttypes=set((int, str, frozenset, type(None))), # noqa + sorted=sorted, + tuple=tuple, + type=type, + len=len, + ): key = args if kwds: sorted_items = sorted(kwds.items()) @@ -85,10 +268,11 @@ def _make_key(args, kwds, typed, def lru_cache(maxsize=100, typed=False): """Least-recently-used cache decorator, see: - http://docs.python.org/3/library/functools.html#functools.lru_cache + http://docs.python.org/3/library/functools.html#functools.lru_cache. """ + def decorating_function(user_function): - cache = dict() + cache = {} stats = [0, 0] HITS, MISSES = 0, 1 make_key = _make_key @@ -100,11 +284,14 @@ def decorating_function(user_function): nonlocal_root = [root] PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 if maxsize == 0: + def wrapper(*args, **kwds): result = user_function(*args, **kwds) stats[MISSES] += 1 return result + elif maxsize is None: + def wrapper(*args, **kwds): key = make_key(args, kwds, typed) result = cache_get(key, root) @@ -115,7 +302,9 @@ def wrapper(*args, **kwds): cache[key] = result stats[MISSES] += 1 return result + else: + def wrapper(*args, **kwds): if kwds or typed: key = make_key(args, kwds, typed) @@ -125,7 +314,7 @@ def wrapper(*args, **kwds): try: link = cache_get(key) if link is not None: - root, = nonlocal_root + (root,) = nonlocal_root link_prev, link_next, key, result = link link_prev[NEXT] = link_next link_next[PREV] = link_prev @@ -140,7 +329,7 @@ def wrapper(*args, **kwds): result = user_function(*args, **kwds) lock.acquire() try: - root, = nonlocal_root + (root,) = nonlocal_root if key in cache: pass elif _len(cache) >= maxsize: @@ -162,16 +351,17 @@ def wrapper(*args, **kwds): return result def cache_info(): - """Report cache statistics""" + """Report cache statistics.""" lock.acquire() try: - return _CacheInfo(stats[HITS], stats[MISSES], maxsize, - len(cache)) + return _CacheInfo( + stats[HITS], stats[MISSES], maxsize, len(cache) + ) finally: lock.release() def cache_clear(): - """Clear the cache and cache statistics""" + """Clear the cache and cache statistics.""" lock.acquire() try: cache.clear() @@ -193,6 +383,7 @@ def cache_clear(): try: from shutil import which except ImportError: + def which(cmd, mode=os.F_OK | os.X_OK, path=None): """Given a command, mode, and a PATH string, return the path which conforms to the given mode on the PATH, or None if there is no such @@ -202,9 +393,13 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None): of os.environ.get("PATH"), or can be overridden with a custom search path. """ + def _access_check(fn, mode): - return (os.path.exists(fn) and os.access(fn, mode) and - not os.path.isdir(fn)) + return ( + os.path.exists(fn) + and os.access(fn, mode) + and not os.path.isdir(fn) + ) if os.path.dirname(cmd): if _access_check(cmd, mode): @@ -239,3 +434,50 @@ def _access_check(fn, mode): if _access_check(name, mode): return name return None + + +# python 3.3 +try: + from shutil import get_terminal_size +except ImportError: + + def get_terminal_size(fallback=(80, 24)): + try: + import fcntl + import struct + import termios + except ImportError: + return fallback + else: + try: + # This should work on Linux. + res = struct.unpack( + 'hh', fcntl.ioctl(1, termios.TIOCGWINSZ, '1234') + ) + return (res[1], res[0]) + except Exception: # noqa: BLE001 + return fallback + + +# python 3.3 +try: + from subprocess import TimeoutExpired as SubprocessTimeoutExpired +except ImportError: + + class SubprocessTimeoutExpired(Exception): + pass + + +# python 3.5 +try: + from contextlib import redirect_stderr +except ImportError: + + @contextlib.contextmanager + def redirect_stderr(new_target): + original = sys.stderr + try: + sys.stderr = new_target + yield new_target + finally: + sys.stderr = original diff --git a/addon/globalPlugins/soundmanager/psutil/_compat.pyc b/addon/globalPlugins/soundmanager/psutil/_compat.pyc deleted file mode 100644 index fb964fd..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/_compat.pyc and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/_exceptions.py b/addon/globalPlugins/soundmanager/psutil/_exceptions.py deleted file mode 100755 index 6dbbd28..0000000 --- a/addon/globalPlugins/soundmanager/psutil/_exceptions.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - - -class Error(Exception): - """Base exception class. All other psutil exceptions inherit - from this one. - """ - - def __init__(self, msg=""): - Exception.__init__(self, msg) - self.msg = msg - - def __repr__(self): - ret = "psutil.%s %s" % (self.__class__.__name__, self.msg) - return ret.strip() - - __str__ = __repr__ - - -class NoSuchProcess(Error): - """Exception raised when a process with a certain PID doesn't - or no longer exists. - """ - - def __init__(self, pid, name=None, msg=None): - Error.__init__(self, msg) - self.pid = pid - self.name = name - self.msg = msg - if msg is None: - if name: - details = "(pid=%s, name=%s)" % (self.pid, repr(self.name)) - else: - details = "(pid=%s)" % self.pid - self.msg = "process no longer exists " + details - - -class ZombieProcess(NoSuchProcess): - """Exception raised when querying a zombie process. This is - raised on macOS, BSD and Solaris only, and not always: depending - on the query the OS may be able to succeed anyway. - On Linux all zombie processes are querable (hence this is never - raised). Windows doesn't have zombie processes. - """ - - def __init__(self, pid, name=None, ppid=None, msg=None): - NoSuchProcess.__init__(self, msg) - self.pid = pid - self.ppid = ppid - self.name = name - self.msg = msg - if msg is None: - args = ["pid=%s" % pid] - if name: - args.append("name=%s" % repr(self.name)) - if ppid: - args.append("ppid=%s" % self.ppid) - details = "(%s)" % ", ".join(args) - self.msg = "process still exists but it's a zombie " + details - - -class AccessDenied(Error): - """Exception raised when permission to perform an action is denied.""" - - def __init__(self, pid=None, name=None, msg=None): - Error.__init__(self, msg) - self.pid = pid - self.name = name - self.msg = msg - if msg is None: - if (pid is not None) and (name is not None): - self.msg = "(pid=%s, name=%s)" % (pid, repr(name)) - elif (pid is not None): - self.msg = "(pid=%s)" % self.pid - else: - self.msg = "" - - -class TimeoutExpired(Error): - """Raised on Process.wait(timeout) if timeout expires and process - is still alive. - """ - - def __init__(self, seconds, pid=None, name=None): - Error.__init__(self, "timeout after %s seconds" % seconds) - self.seconds = seconds - self.pid = pid - self.name = name - if (pid is not None) and (name is not None): - self.msg += " (pid=%s, name=%s)" % (pid, repr(name)) - elif (pid is not None): - self.msg += " (pid=%s)" % self.pid diff --git a/addon/globalPlugins/soundmanager/psutil/_psaix.py b/addon/globalPlugins/soundmanager/psutil/_psaix.py index 7ba212d..7310ab6 100644 --- a/addon/globalPlugins/soundmanager/psutil/_psaix.py +++ b/addon/globalPlugins/soundmanager/psutil/_psaix.py @@ -6,31 +6,32 @@ """AIX platform implementation.""" -import errno +import functools import glob import os import re import subprocess import sys from collections import namedtuple -from socket import AF_INET from . import _common from . import _psposix from . import _psutil_aix as cext from . import _psutil_posix as cext_posix -from ._common import AF_INET6 -from ._common import memoize_when_activated from ._common import NIC_DUPLEX_FULL from ._common import NIC_DUPLEX_HALF from ._common import NIC_DUPLEX_UNKNOWN -from ._common import sockfam_to_enum -from ._common import socktype_to_enum +from ._common import AccessDenied +from ._common import NoSuchProcess +from ._common import ZombieProcess +from ._common import conn_to_ntuple +from ._common import get_procfs_path +from ._common import memoize_when_activated from ._common import usage_percent from ._compat import PY3 -from ._exceptions import AccessDenied -from ._exceptions import NoSuchProcess -from ._exceptions import ZombieProcess +from ._compat import FileNotFoundError +from ._compat import PermissionError +from ._compat import ProcessLookupError __extra__all__ = ["PROCFS_PATH"] @@ -42,15 +43,17 @@ HAS_THREADS = hasattr(cext, "proc_threads") +HAS_NET_IO_COUNTERS = hasattr(cext, "net_io_counters") +HAS_PROC_IO_COUNTERS = hasattr(cext, "proc_io_counters") -PAGE_SIZE = os.sysconf('SC_PAGE_SIZE') +PAGE_SIZE = cext_posix.getpagesize() AF_LINK = cext_posix.AF_LINK PROC_STATUSES = { cext.SIDL: _common.STATUS_IDLE, cext.SZOMB: _common.STATUS_ZOMBIE, cext.SACTIVE: _common.STATUS_RUNNING, - cext.SSWAP: _common.STATUS_RUNNING, # TODO what status is this? + cext.SSWAP: _common.STATUS_RUNNING, # TODO what status is this? cext.SSTOP: _common.STATUS_STOPPED, } @@ -77,7 +80,8 @@ nice=4, num_threads=5, status=6, - ttynr=7) + ttynr=7, +) # ===================================================================== @@ -93,21 +97,6 @@ scputimes = namedtuple('scputimes', ['user', 'system', 'idle', 'iowait']) # psutil.virtual_memory() svmem = namedtuple('svmem', ['total', 'available', 'percent', 'used', 'free']) -# psutil.Process.memory_maps(grouped=True) -pmmap_grouped = namedtuple('pmmap_grouped', ['path', 'rss', 'anon', 'locked']) -# psutil.Process.memory_maps(grouped=False) -pmmap_ext = namedtuple( - 'pmmap_ext', 'addr perms ' + ' '.join(pmmap_grouped._fields)) - - -# ===================================================================== -# --- utils -# ===================================================================== - - -def get_procfs_path(): - """Return updated psutil.PROCFS_PATH constant.""" - return sys.modules['psutil'].PROCFS_PATH # ===================================================================== @@ -135,13 +124,13 @@ def swap_memory(): def cpu_times(): - """Return system-wide CPU times as a named tuple""" + """Return system-wide CPU times as a named tuple.""" ret = cext.per_cpu_times() return scputimes(*[sum(x) for x in zip(*ret)]) def per_cpu_times(): - """Return system per-CPU times as a list of named tuples""" + """Return system per-CPU times as a list of named tuples.""" ret = cext.per_cpu_times() return [scputimes(*x) for x in ret] @@ -155,14 +144,14 @@ def cpu_count_logical(): return None -def cpu_count_physical(): - cmd = "lsdev -Cc processor" - p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) +def cpu_count_cores(): + cmd = ["lsdev", "-Cc", "processor"] + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = p.communicate() if PY3: - stdout, stderr = [x.decode(sys.stdout.encoding) - for x in (stdout, stderr)] + stdout, stderr = ( + x.decode(sys.stdout.encoding) for x in (stdout, stderr) + ) if p.returncode != 0: raise RuntimeError("%r command error\n%s" % (cmd, stderr)) processors = stdout.strip().splitlines() @@ -173,7 +162,8 @@ def cpu_stats(): """Return various CPU stats as a named tuple.""" ctx_switches, interrupts, soft_interrupts, syscalls = cext.cpu_stats() return _common.scpustats( - ctx_switches, interrupts, soft_interrupts, syscalls) + ctx_switches, interrupts, soft_interrupts, syscalls + ) # ===================================================================== @@ -201,7 +191,10 @@ def disk_partitions(all=False): # filter by filesystem having a total size > 0. if not disk_usage(mountpoint).total: continue - ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) + maxfile = maxpath = None # set later + ntuple = _common.sdiskpart( + device, mountpoint, fstype, opts, maxfile, maxpath + ) retlist.append(ntuple) return retlist @@ -212,7 +205,9 @@ def disk_partitions(all=False): net_if_addrs = cext_posix.net_if_addrs -net_io_counters = cext.net_io_counters + +if HAS_NET_IO_COUNTERS: + net_io_counters = cext.net_io_counters def net_connections(kind, _pid=-1): @@ -221,62 +216,69 @@ def net_connections(kind, _pid=-1): """ cmap = _common.conn_tmap if kind not in cmap: - raise ValueError("invalid %r kind argument; choose between %s" - % (kind, ', '.join([repr(x) for x in cmap]))) + raise ValueError( + "invalid %r kind argument; choose between %s" + % (kind, ', '.join([repr(x) for x in cmap])) + ) families, types = _common.conn_tmap[kind] rawlist = cext.net_connections(_pid) - ret = set() + ret = [] for item in rawlist: fd, fam, type_, laddr, raddr, status, pid = item if fam not in families: continue if type_ not in types: continue - status = TCP_STATUSES[status] - if fam in (AF_INET, AF_INET6): - if laddr: - laddr = _common.addr(*laddr) - if raddr: - raddr = _common.addr(*raddr) - fam = sockfam_to_enum(fam) - type_ = socktype_to_enum(type_) - if _pid == -1: - nt = _common.sconn(fd, fam, type_, laddr, raddr, status, pid) - else: - nt = _common.pconn(fd, fam, type_, laddr, raddr, status) - ret.add(nt) - return list(ret) + nt = conn_to_ntuple( + fd, + fam, + type_, + laddr, + raddr, + status, + TCP_STATUSES, + pid=pid if _pid == -1 else None, + ) + ret.append(nt) + return ret def net_if_stats(): """Get NIC stats (isup, duplex, speed, mtu).""" - duplex_map = {"Full": NIC_DUPLEX_FULL, - "Half": NIC_DUPLEX_HALF} + duplex_map = {"Full": NIC_DUPLEX_FULL, "Half": NIC_DUPLEX_HALF} names = set([x[0] for x in net_if_addrs()]) ret = {} for name in names: - isup, mtu = cext.net_if_stats(name) + mtu = cext_posix.net_if_mtu(name) + flags = cext_posix.net_if_flags(name) # try to get speed and duplex # TODO: rewrite this in C (entstat forks, so use truss -f to follow. # looks like it is using an undocumented ioctl?) duplex = "" speed = 0 - p = subprocess.Popen(["/usr/bin/entstat", "-d", name], - stdout=subprocess.PIPE, stderr=subprocess.PIPE) + p = subprocess.Popen( + ["/usr/bin/entstat", "-d", name], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) stdout, stderr = p.communicate() if PY3: - stdout, stderr = [x.decode(sys.stdout.encoding) - for x in (stdout, stderr)] + stdout, stderr = ( + x.decode(sys.stdout.encoding) for x in (stdout, stderr) + ) if p.returncode == 0: re_result = re.search( - r"Running: (\d+) Mbps.*?(\w+) Duplex", stdout) + r"Running: (\d+) Mbps.*?(\w+) Duplex", stdout + ) if re_result is not None: speed = int(re_result.group(1)) duplex = re_result.group(2) + output_flags = ','.join(flags) + isup = 'running' in flags duplex = duplex_map.get(duplex, NIC_DUPLEX_UNKNOWN) - ret[name] = _common.snicstats(isup, duplex, speed, mtu) + ret[name] = _common.snicstats(isup, duplex, speed, mtu, output_flags) return ret @@ -329,32 +331,28 @@ def wrap_exceptions(fun): EACCES and EPERM in NoSuchProcess or AccessDenied exceptions. """ + @functools.wraps(fun) def wrapper(self, *args, **kwargs): try: return fun(self, *args, **kwargs) - except EnvironmentError as err: - # support for private module import - if (NoSuchProcess is None or AccessDenied is None or - ZombieProcess is None): - raise + except (FileNotFoundError, ProcessLookupError): # ENOENT (no such file or directory) gets raised on open(). # ESRCH (no such process) can get raised on read() if # process is gone in meantime. - if err.errno in (errno.ENOENT, errno.ESRCH): - if not pid_exists(self.pid): - raise NoSuchProcess(self.pid, self._name) - else: - raise ZombieProcess(self.pid, self._name, self._ppid) - if err.errno in (errno.EPERM, errno.EACCES): - raise AccessDenied(self.pid, self._name) - raise + if not pid_exists(self.pid): + raise NoSuchProcess(self.pid, self._name) + else: + raise ZombieProcess(self.pid, self._name, self._ppid) + except PermissionError: + raise AccessDenied(self.pid, self._name) + return wrapper -class Process(object): +class Process: """Wrapper class around underlying C implementation.""" - __slots__ = ["pid", "_name", "_ppid", "_procfs_path"] + __slots__ = ["pid", "_name", "_ppid", "_procfs_path", "_cache"] def __init__(self, pid): self.pid = pid @@ -363,23 +361,19 @@ def __init__(self, pid): self._procfs_path = get_procfs_path() def oneshot_enter(self): - self._proc_name_and_args.cache_activate() - self._proc_basic_info.cache_activate() - self._proc_cred.cache_activate() + self._proc_basic_info.cache_activate(self) + self._proc_cred.cache_activate(self) def oneshot_exit(self): - self._proc_name_and_args.cache_deactivate() - self._proc_basic_info.cache_deactivate() - self._proc_cred.cache_deactivate() - - @memoize_when_activated - def _proc_name_and_args(self): - return cext.proc_name_and_args(self.pid, self._procfs_path) + self._proc_basic_info.cache_deactivate(self) + self._proc_cred.cache_deactivate(self) + @wrap_exceptions @memoize_when_activated def _proc_basic_info(self): return cext.proc_basic_info(self.pid, self._procfs_path) + @wrap_exceptions @memoize_when_activated def _proc_cred(self): return cext.proc_cred(self.pid, self._procfs_path) @@ -388,36 +382,46 @@ def _proc_cred(self): def name(self): if self.pid == 0: return "swapper" - # note: this is limited to 15 characters - return self._proc_name_and_args()[0].rstrip("\x00") + # note: max 16 characters + return cext.proc_name(self.pid, self._procfs_path).rstrip("\x00") @wrap_exceptions def exe(self): # there is no way to get executable path in AIX other than to guess, # and guessing is more complex than what's in the wrapping class - exe = self.cmdline()[0] + cmdline = self.cmdline() + if not cmdline: + return '' + exe = cmdline[0] if os.path.sep in exe: # relative or absolute path if not os.path.isabs(exe): # if cwd has changed, we're out of luck - this may be wrong! exe = os.path.abspath(os.path.join(self.cwd(), exe)) - if (os.path.isabs(exe) and - os.path.isfile(exe) and - os.access(exe, os.X_OK)): + if ( + os.path.isabs(exe) + and os.path.isfile(exe) + and os.access(exe, os.X_OK) + ): return exe # not found, move to search in PATH using basename only exe = os.path.basename(exe) # search for exe name PATH for path in os.environ["PATH"].split(":"): possible_exe = os.path.abspath(os.path.join(path, exe)) - if (os.path.isfile(possible_exe) and - os.access(possible_exe, os.X_OK)): + if os.path.isfile(possible_exe) and os.access( + possible_exe, os.X_OK + ): return possible_exe return '' @wrap_exceptions def cmdline(self): - return self._proc_name_and_args()[1].split(' ') + return cext.proc_args(self.pid) + + @wrap_exceptions + def environ(self): + return cext.proc_environ(self.pid) @wrap_exceptions def create_time(self): @@ -428,6 +432,7 @@ def num_threads(self): return self._proc_basic_info()[proc_info_map['num_threads']] if HAS_THREADS: + @wrap_exceptions def threads(self): rawlist = cext.proc_threads(self.pid) @@ -483,14 +488,14 @@ def gids(self): @wrap_exceptions def cpu_times(self): - cpu_times = cext.proc_cpu_times(self.pid, self._procfs_path) - return _common.pcputimes(*cpu_times) + t = cext.proc_cpu_times(self.pid, self._procfs_path) + return _common.pcputimes(*t) @wrap_exceptions def terminal(self): ttydev = self._proc_basic_info()[proc_info_map['ttynr']] # convert from 64-bit dev_t to 32-bit dev_t and then map the device - ttydev = (((ttydev & 0x0000FFFF00000000) >> 16) | (ttydev & 0xFFFF)) + ttydev = ((ttydev & 0x0000FFFF00000000) >> 16) | (ttydev & 0xFFFF) # try to match rdev of /dev/pts/* files ttydev for dev in glob.glob("/dev/**/*"): if os.stat(dev).st_rdev == ttydev: @@ -503,11 +508,9 @@ def cwd(self): try: result = os.readlink("%s/%s/cwd" % (procfs_path, self.pid)) return result.rstrip('/') - except OSError as err: - if err.errno == errno.ENOENT: - os.stat("%s/%s" % (procfs_path, self.pid)) # raise NSP or AD - return None - raise + except FileNotFoundError: + os.stat("%s/%s" % (procfs_path, self.pid)) # raise NSP or AD + return "" @wrap_exceptions def memory_info(self): @@ -527,12 +530,16 @@ def status(self): def open_files(self): # TODO rewrite without using procfiles (stat /proc/pid/fd/* and then # find matching name of the inode) - p = subprocess.Popen(["/usr/bin/procfiles", "-n", str(self.pid)], - stdout=subprocess.PIPE, stderr=subprocess.PIPE) + p = subprocess.Popen( + ["/usr/bin/procfiles", "-n", str(self.pid)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) stdout, stderr = p.communicate() if PY3: - stdout, stderr = [x.decode(sys.stdout.encoding) - for x in (stdout, stderr)] + stdout, stderr = ( + x.decode(sys.stdout.encoding) for x in (stdout, stderr) + ) if "no such process" in stderr.lower(): raise NoSuchProcess(self.pid, self._name) procfiles = re.findall(r"(\d+): S_IFREG.*\s*.*name:(.*)\n", stdout) @@ -548,27 +555,28 @@ def open_files(self): @wrap_exceptions def num_fds(self): - if self.pid == 0: # no /proc/0/fd + if self.pid == 0: # no /proc/0/fd return 0 return len(os.listdir("%s/%s/fd" % (self._procfs_path, self.pid))) @wrap_exceptions def num_ctx_switches(self): - return _common.pctxsw( - *cext.proc_num_ctx_switches(self.pid)) + return _common.pctxsw(*cext.proc_num_ctx_switches(self.pid)) @wrap_exceptions def wait(self, timeout=None): return _psposix.wait_pid(self.pid, timeout, self._name) - @wrap_exceptions - def io_counters(self): - try: - rc, wc, rb, wb = cext.proc_io_counters(self.pid) - except OSError: - # if process is terminated, proc_io_counters returns OSError - # instead of NSP - if not pid_exists(self.pid): - raise NoSuchProcess(self.pid, self._name) - raise - return _common.pio(rc, wc, rb, wb) + if HAS_PROC_IO_COUNTERS: + + @wrap_exceptions + def io_counters(self): + try: + rc, wc, rb, wb = cext.proc_io_counters(self.pid) + except OSError: + # if process is terminated, proc_io_counters returns OSError + # instead of NSP + if not pid_exists(self.pid): + raise NoSuchProcess(self.pid, self._name) + raise + return _common.pio(rc, wc, rb, wb) diff --git a/addon/globalPlugins/soundmanager/psutil/_psaix.pyc b/addon/globalPlugins/soundmanager/psutil/_psaix.pyc deleted file mode 100644 index 98d1f65..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/_psaix.pyc and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/_psbsd.py b/addon/globalPlugins/soundmanager/psutil/_psbsd.py index 7f4bcb6..da68f5e 100644 --- a/addon/globalPlugins/soundmanager/psutil/_psbsd.py +++ b/addon/globalPlugins/soundmanager/psutil/_psbsd.py @@ -8,28 +8,31 @@ import errno import functools import os -import xml.etree.ElementTree as ET +from collections import defaultdict from collections import namedtuple -from socket import AF_INET +from xml.etree import ElementTree from . import _common from . import _psposix from . import _psutil_bsd as cext from . import _psutil_posix as cext_posix -from ._common import AF_INET6 -from ._common import conn_tmap from ._common import FREEBSD -from ._common import memoize -from ._common import memoize_when_activated from ._common import NETBSD from ._common import OPENBSD -from ._common import sockfam_to_enum -from ._common import socktype_to_enum +from ._common import AccessDenied +from ._common import NoSuchProcess +from ._common import ZombieProcess +from ._common import conn_tmap +from ._common import conn_to_ntuple +from ._common import debug +from ._common import memoize +from ._common import memoize_when_activated from ._common import usage_percent +from ._compat import FileNotFoundError +from ._compat import PermissionError +from ._compat import ProcessLookupError from ._compat import which -from ._exceptions import AccessDenied -from ._exceptions import NoSuchProcess -from ._exceptions import ZombieProcess + __extra__all__ = [] @@ -49,7 +52,7 @@ cext.SWAIT: _common.STATUS_WAITING, cext.SLOCK: _common.STATUS_LOCKED, } -elif OPENBSD or NETBSD: +elif OPENBSD: PROC_STATUSES = { cext.SIDL: _common.STATUS_IDLE, cext.SSLEEP: _common.STATUS_SLEEPING, @@ -74,12 +77,11 @@ elif NETBSD: PROC_STATUSES = { cext.SIDL: _common.STATUS_IDLE, - cext.SACTIVE: _common.STATUS_RUNNING, - cext.SDYING: _common.STATUS_ZOMBIE, + cext.SSLEEP: _common.STATUS_SLEEPING, cext.SSTOP: _common.STATUS_STOPPED, cext.SZOMB: _common.STATUS_ZOMBIE, - cext.SDEAD: _common.STATUS_DEAD, - cext.SSUSPENDED: _common.STATUS_SUSPENDED, # unique to NetBSD + cext.SRUN: _common.STATUS_WAKING, + cext.SONPROC: _common.STATUS_RUNNING, } TCP_STATUSES = { @@ -97,12 +99,14 @@ cext.PSUTIL_CONN_NONE: _common.CONN_NONE, } -if NETBSD: - PAGESIZE = os.sysconf("SC_PAGESIZE") -else: - PAGESIZE = os.sysconf("SC_PAGE_SIZE") +PAGESIZE = cext_posix.getpagesize() AF_LINK = cext_posix.AF_LINK +HAS_PER_CPU_TIMES = hasattr(cext, "per_cpu_times") +HAS_PROC_NUM_THREADS = hasattr(cext, "proc_num_threads") +HAS_PROC_OPEN_FILES = hasattr(cext, 'proc_open_files') +HAS_PROC_NUM_FDS = hasattr(cext, 'proc_num_fds') + kinfo_proc_map = dict( ppid=0, status=1, @@ -137,6 +141,7 @@ # ===================================================================== +# fmt: off # psutil.virtual_memory() svmem = namedtuple( 'svmem', ['total', 'available', 'percent', 'used', 'free', @@ -166,6 +171,7 @@ else: sdiskio = namedtuple('sdiskio', ['read_count', 'write_count', 'read_bytes', 'write_bytes']) +# fmt: on # ===================================================================== @@ -174,10 +180,9 @@ def virtual_memory(): - """System virtual memory as a namedtuple.""" mem = cext.virtual_mem() - total, free, active, inactive, wired, cached, buffers, shared = mem if NETBSD: + total, free, active, inactive, wired, cached = mem # On NetBSD buffers and shared mem is determined via /proc. # The C ext set them to 0. with open('/proc/meminfo', 'rb') as f: @@ -186,11 +191,39 @@ def virtual_memory(): buffers = int(line.split()[1]) * 1024 elif line.startswith(b'MemShared:'): shared = int(line.split()[1]) * 1024 - avail = inactive + cached + free - used = active + wired + cached + # Before avail was calculated as (inactive + cached + free), + # same as zabbix, but it turned out it could exceed total (see + # #2233), so zabbix seems to be wrong. Htop calculates it + # differently, and the used value seem more realistic, so let's + # match htop. + # https://github.com/htop-dev/htop/blob/e7f447b/netbsd/NetBSDProcessList.c#L162 # noqa + # https://github.com/zabbix/zabbix/blob/af5e0f8/src/libs/zbxsysinfo/netbsd/memory.c#L135 # noqa + used = active + wired + avail = total - used + else: + total, free, active, inactive, wired, cached, buffers, shared = mem + # matches freebsd-memory CLI: + # * https://people.freebsd.org/~rse/dist/freebsd-memory + # * https://www.cyberciti.biz/files/scripts/freebsd-memory.pl.txt + # matches zabbix: + # * https://github.com/zabbix/zabbix/blob/af5e0f8/src/libs/zbxsysinfo/freebsd/memory.c#L143 # noqa + avail = inactive + cached + free + used = active + wired + cached + percent = usage_percent((total - avail), total, round_=1) - return svmem(total, avail, percent, used, free, - active, inactive, buffers, cached, shared, wired) + return svmem( + total, + avail, + percent, + used, + free, + active, + inactive, + buffers, + cached, + shared, + wired, + ) def swap_memory(): @@ -206,20 +239,22 @@ def swap_memory(): def cpu_times(): - """Return system per-CPU times as a namedtuple""" + """Return system per-CPU times as a namedtuple.""" user, nice, system, idle, irq = cext.cpu_times() return scputimes(user, nice, system, idle, irq) -if hasattr(cext, "per_cpu_times"): +if HAS_PER_CPU_TIMES: + def per_cpu_times(): - """Return system CPU times as a namedtuple""" + """Return system CPU times as a namedtuple.""" ret = [] for cpu_t in cext.per_cpu_times(): user, nice, system, idle, irq = cpu_t item = scputimes(user, nice, system, idle, irq) ret.append(item) return ret + else: # XXX # Ok, this is very dirty. @@ -229,11 +264,12 @@ def per_cpu_times(): # crash at psutil import time. # Next calls will fail with NotImplementedError def per_cpu_times(): - """Return system CPU times as a namedtuple""" + """Return system CPU times as a namedtuple.""" if cpu_count_logical() == 1: return [cpu_times()] if per_cpu_times.__called__: - raise NotImplementedError("supported only starting from FreeBSD 8") + msg = "supported only starting from FreeBSD 8" + raise NotImplementedError(msg) per_cpu_times.__called__ = True return [cpu_times()] @@ -246,33 +282,35 @@ def cpu_count_logical(): if OPENBSD or NETBSD: - def cpu_count_physical(): + + def cpu_count_cores(): # OpenBSD and NetBSD do not implement this. return 1 if cpu_count_logical() == 1 else None + else: - def cpu_count_physical(): - """Return the number of physical CPUs in the system.""" + + def cpu_count_cores(): + """Return the number of CPU cores in the system.""" # From the C module we'll get an XML string similar to this: # http://manpages.ubuntu.com/manpages/precise/man4/smp.4freebsd.html # We may get None in case "sysctl kern.sched.topology_spec" # is not supported on this BSD version, in which case we'll mimic # os.cpu_count() and return None. ret = None - s = cext.cpu_count_phys() + s = cext.cpu_topology() if s is not None: # get rid of padding chars appended at the end of the string index = s.rfind("") if index != -1: - s = s[:index + 9] - root = ET.fromstring(s) + s = s[: index + 9] + root = ElementTree.fromstring(s) try: ret = len(root.findall('group/children/group/cpu')) or None finally: # needed otherwise it will memleak root.clear() if not ret: - # If logical CPUs are 1 it's obvious we'll have only 1 - # physical CPU. + # If logical CPUs == 1 it's obvious we' have only 1 core. if cpu_count_logical() == 1: return 1 return ret @@ -294,8 +332,9 @@ def cpu_stats(): # # Note: the C ext is returning some metrics we are not exposing: # traps, faults and forks. - ctxsw, intrs, soft_intrs, syscalls, traps, faults, forks = \ + ctxsw, intrs, soft_intrs, syscalls, traps, faults, forks = ( cext.cpu_stats() + ) with open('/proc/stat', 'rb') as f: for line in f: if line.startswith(b'intr'): @@ -303,11 +342,45 @@ def cpu_stats(): elif OPENBSD: # Note: the C ext is returning some metrics we are not exposing: # traps, faults and forks. - ctxsw, intrs, soft_intrs, syscalls, traps, faults, forks = \ + ctxsw, intrs, soft_intrs, syscalls, traps, faults, forks = ( cext.cpu_stats() + ) return _common.scpustats(ctxsw, intrs, soft_intrs, syscalls) +if FREEBSD: + + def cpu_freq(): + """Return frequency metrics for CPUs. As of Dec 2018 only + CPU 0 appears to be supported by FreeBSD and all other cores + match the frequency of CPU 0. + """ + ret = [] + num_cpus = cpu_count_logical() + for cpu in range(num_cpus): + try: + current, available_freq = cext.cpu_freq(cpu) + except NotImplementedError: + continue + if available_freq: + try: + min_freq = int(available_freq.split(" ")[-1].split("/")[0]) + except (IndexError, ValueError): + min_freq = None + try: + max_freq = int(available_freq.split(" ")[0].split("/")[0]) + except (IndexError, ValueError): + max_freq = None + ret.append(_common.scpufreq(current, min_freq, max_freq)) + return ret + +elif OPENBSD: + + def cpu_freq(): + curr = float(cext.cpu_freq()) + return [_common.scpufreq(curr, 0.0, 0.0)] + + # ===================================================================== # --- disks # ===================================================================== @@ -316,13 +389,16 @@ def cpu_stats(): def disk_partitions(all=False): """Return mounted disk partitions as a list of namedtuples. 'all' argument is ignored, see: - https://github.com/giampaolo/psutil/issues/906 + https://github.com/giampaolo/psutil/issues/906. """ retlist = [] partitions = cext.disk_partitions() for partition in partitions: device, mountpoint, fstype, opts = partition - ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) + maxfile = maxpath = None # set later + ntuple = _common.sdiskpart( + device, mountpoint, fstype, opts, maxfile, maxpath + ) retlist.append(ntuple) return retlist @@ -347,7 +423,7 @@ def net_if_stats(): for name in names: try: mtu = cext_posix.net_if_mtu(name) - isup = cext_posix.net_if_flags(name) + flags = cext_posix.net_if_flags(name) duplex, speed = cext_posix.net_if_duplex_speed(name) except OSError as err: # https://github.com/giampaolo/psutil/issues/1279 @@ -356,56 +432,37 @@ def net_if_stats(): else: if hasattr(_common, 'NicDuplex'): duplex = _common.NicDuplex(duplex) - ret[name] = _common.snicstats(isup, duplex, speed, mtu) + output_flags = ','.join(flags) + isup = 'running' in flags + ret[name] = _common.snicstats( + isup, duplex, speed, mtu, output_flags + ) return ret def net_connections(kind): """System-wide network connections.""" - if OPENBSD: - ret = [] - for pid in pids(): - try: - cons = Process(pid).connections(kind) - except (NoSuchProcess, ZombieProcess): - continue - else: - for conn in cons: - conn = list(conn) - conn.append(pid) - ret.append(_common.sconn(*conn)) - return ret - if kind not in _common.conn_tmap: - raise ValueError("invalid %r kind argument; choose between %s" - % (kind, ', '.join([repr(x) for x in conn_tmap]))) + raise ValueError( + "invalid %r kind argument; choose between %s" + % (kind, ', '.join([repr(x) for x in conn_tmap])) + ) families, types = conn_tmap[kind] ret = set() - if NETBSD: - rawlist = cext.net_connections(-1) - else: - rawlist = cext.net_connections() + + if OPENBSD: + rawlist = cext.net_connections(-1, families, types) + elif NETBSD: + rawlist = cext.net_connections(-1, kind) + else: # FreeBSD + rawlist = cext.net_connections(families, types) + for item in rawlist: fd, fam, type, laddr, raddr, status, pid = item - # TODO: apply filter at C level - if fam in families and type in types: - try: - status = TCP_STATUSES[status] - except KeyError: - # XXX: Not sure why this happens. I saw this occurring - # with IPv6 sockets opened by 'vim'. Those sockets - # have a very short lifetime so maybe the kernel - # can't initialize their status? - status = TCP_STATUSES[cext.PSUTIL_CONN_NONE] - if fam in (AF_INET, AF_INET6): - if laddr: - laddr = _common.addr(*laddr) - if raddr: - raddr = _common.addr(*raddr) - fam = sockfam_to_enum(fam) - type = socktype_to_enum(type) - nt = _common.sconn(fd, fam, type, laddr, raddr, status, pid) - ret.add(nt) + nt = conn_to_ntuple( + fd, fam, type, laddr, raddr, status, TCP_STATUSES, pid + ) + ret.add(nt) return list(ret) @@ -432,6 +489,24 @@ def sensors_battery(): secsleft = minsleft * 60 return _common.sbattery(percent, secsleft, power_plugged) + def sensors_temperatures(): + """Return CPU cores temperatures if available, else an empty dict.""" + ret = defaultdict(list) + num_cpus = cpu_count_logical() + for cpu in range(num_cpus): + try: + current, high = cext.sensors_cpu_temperature(cpu) + if high <= 0: + high = None + name = "Core %s" % cpu + ret["coretemp"].append( + _common.shwtemp(name, current, high, high) + ) + except NotImplementedError: + pass + + return ret + # ===================================================================== # --- other system functions @@ -487,6 +562,7 @@ def pids(): if OPENBSD or NETBSD: + def pid_exists(pid): """Return True if pid exists.""" exists = _psposix.pid_exists(pid) @@ -496,32 +572,43 @@ def pid_exists(pid): return pid in pids() else: return True + else: pid_exists = _psposix.pid_exists +def is_zombie(pid): + try: + st = cext.proc_oneshot_info(pid)[kinfo_proc_map['status']] + return PROC_STATUSES.get(st) == _common.STATUS_ZOMBIE + except OSError: + return False + + def wrap_exceptions(fun): """Decorator which translates bare OSError exceptions into NoSuchProcess and AccessDenied. """ + @functools.wraps(fun) def wrapper(self, *args, **kwargs): try: return fun(self, *args, **kwargs) - except OSError as err: + except ProcessLookupError: + if is_zombie(self.pid): + raise ZombieProcess(self.pid, self._name, self._ppid) + else: + raise NoSuchProcess(self.pid, self._name) + except PermissionError: + raise AccessDenied(self.pid, self._name) + except OSError: if self.pid == 0: if 0 in pids(): raise AccessDenied(self.pid, self._name) else: raise - if err.errno == errno.ESRCH: - if not pid_exists(self.pid): - raise NoSuchProcess(self.pid, self._name) - else: - raise ZombieProcess(self.pid, self._name, self._ppid) - if err.errno in (errno.EPERM, errno.EACCES): - raise AccessDenied(self.pid, self._name) raise + return wrapper @@ -530,30 +617,35 @@ def wrap_exceptions_procfs(inst): """Same as above, for routines relying on reading /proc fs.""" try: yield - except EnvironmentError as err: + except (ProcessLookupError, FileNotFoundError): # ENOENT (no such file or directory) gets raised on open(). # ESRCH (no such process) can get raised on read() if # process is gone in meantime. - if err.errno in (errno.ENOENT, errno.ESRCH): - if not pid_exists(inst.pid): - raise NoSuchProcess(inst.pid, inst._name) - else: - raise ZombieProcess(inst.pid, inst._name, inst._ppid) - if err.errno in (errno.EPERM, errno.EACCES): - raise AccessDenied(inst.pid, inst._name) - raise + if is_zombie(inst.pid): + raise ZombieProcess(inst.pid, inst._name, inst._ppid) + else: + raise NoSuchProcess(inst.pid, inst._name) + except PermissionError: + raise AccessDenied(inst.pid, inst._name) -class Process(object): +class Process: """Wrapper class around underlying C implementation.""" - __slots__ = ["pid", "_name", "_ppid"] + __slots__ = ["pid", "_name", "_ppid", "_cache"] def __init__(self, pid): self.pid = pid self._name = None self._ppid = None + def _assert_alive(self): + """Raise NSP if the process disappeared on us.""" + # For those C function who do not raise NSP, possibly returning + # incorrect or incomplete result. + cext.proc_name(self.pid) + + @wrap_exceptions @memoize_when_activated def oneshot(self): """Retrieves multiple process info in one shot as a raw tuple.""" @@ -562,10 +654,10 @@ def oneshot(self): return ret def oneshot_enter(self): - self.oneshot.cache_activate() + self.oneshot.cache_activate(self) def oneshot_exit(self): - self.oneshot.cache_deactivate() + self.oneshot.cache_deactivate(self) @wrap_exceptions def name(self): @@ -575,6 +667,8 @@ def name(self): @wrap_exceptions def exe(self): if FREEBSD: + if self.pid == 0: + return '' # else NSP return cext.proc_exe(self.pid) elif NETBSD: if self.pid == 0: @@ -590,7 +684,7 @@ def exe(self): # cmdline arg (may return None). cmdline = self.cmdline() if cmdline: - return which(cmdline[0]) + return which(cmdline[0]) or "" else: return "" @@ -599,23 +693,32 @@ def cmdline(self): if OPENBSD and self.pid == 0: return [] # ...else it crashes elif NETBSD: - # XXX - most of the times the underlying sysctl() call on Net - # and Open BSD returns a truncated string. - # Also /proc/pid/cmdline behaves the same so it looks - # like this is a kernel bug. + # XXX - most of the times the underlying sysctl() call on + # NetBSD and OpenBSD returns a truncated string. Also + # /proc/pid/cmdline behaves the same so it looks like this + # is a kernel bug. try: return cext.proc_cmdline(self.pid) except OSError as err: if err.errno == errno.EINVAL: - if not pid_exists(self.pid): - raise NoSuchProcess(self.pid, self._name) - else: + if is_zombie(self.pid): raise ZombieProcess(self.pid, self._name, self._ppid) + elif not pid_exists(self.pid): + raise NoSuchProcess(self.pid, self._name, self._ppid) + else: + # XXX: this happens with unicode tests. It means the C + # routine is unable to decode invalid unicode chars. + debug("ignoring %r and returning an empty list" % err) + return [] else: raise else: return cext.proc_cmdline(self.pid) + @wrap_exceptions + def environ(self): + return cext.proc_environ(self.pid) + @wrap_exceptions def terminal(self): tty_nr = self.oneshot()[kinfo_proc_map['ttynr']] @@ -636,7 +739,8 @@ def uids(self): return _common.puids( rawtuple[kinfo_proc_map['real_uid']], rawtuple[kinfo_proc_map['effective_uid']], - rawtuple[kinfo_proc_map['saved_uid']]) + rawtuple[kinfo_proc_map['saved_uid']], + ) @wrap_exceptions def gids(self): @@ -644,7 +748,8 @@ def gids(self): return _common.pgids( rawtuple[kinfo_proc_map['real_gid']], rawtuple[kinfo_proc_map['effective_gid']], - rawtuple[kinfo_proc_map['saved_gid']]) + rawtuple[kinfo_proc_map['saved_gid']], + ) @wrap_exceptions def cpu_times(self): @@ -653,9 +758,11 @@ def cpu_times(self): rawtuple[kinfo_proc_map['user_time']], rawtuple[kinfo_proc_map['sys_time']], rawtuple[kinfo_proc_map['ch_user_time']], - rawtuple[kinfo_proc_map['ch_sys_time']]) + rawtuple[kinfo_proc_map['ch_sys_time']], + ) if FREEBSD: + @wrap_exceptions def cpu_num(self): return self.oneshot()[kinfo_proc_map['cpunum']] @@ -668,7 +775,8 @@ def memory_info(self): rawtuple[kinfo_proc_map['vms']], rawtuple[kinfo_proc_map['memtext']], rawtuple[kinfo_proc_map['memdata']], - rawtuple[kinfo_proc_map['memstack']]) + rawtuple[kinfo_proc_map['memstack']], + ) memory_full_info = memory_info @@ -678,7 +786,7 @@ def create_time(self): @wrap_exceptions def num_threads(self): - if hasattr(cext, "proc_num_threads"): + if HAS_PROC_NUM_THREADS: # FreeBSD return cext.proc_num_threads(self.pid) else: @@ -689,7 +797,8 @@ def num_ctx_switches(self): rawtuple = self.oneshot() return _common.pctxsw( rawtuple[kinfo_proc_map['ctx_switches_vol']], - rawtuple[kinfo_proc_map['ctx_switches_unvol']]) + rawtuple[kinfo_proc_map['ctx_switches_unvol']], + ) @wrap_exceptions def threads(self): @@ -700,65 +809,37 @@ def threads(self): ntuple = _common.pthread(thread_id, utime, stime) retlist.append(ntuple) if OPENBSD: - # On OpenBSD the underlying C function does not raise NSP - # in case the process is gone (and the returned list may - # incomplete). - self.name() # raise NSP if the process disappeared on us + self._assert_alive() return retlist @wrap_exceptions def connections(self, kind='inet'): if kind not in conn_tmap: - raise ValueError("invalid %r kind argument; choose between %s" - % (kind, ', '.join([repr(x) for x in conn_tmap]))) + raise ValueError( + "invalid %r kind argument; choose between %s" + % (kind, ', '.join([repr(x) for x in conn_tmap])) + ) + families, types = conn_tmap[kind] + ret = [] if NETBSD: - families, types = conn_tmap[kind] - ret = set() - rawlist = cext.net_connections(self.pid) - for item in rawlist: - fd, fam, type, laddr, raddr, status, pid = item - assert pid == self.pid - if fam in families and type in types: - try: - status = TCP_STATUSES[status] - except KeyError: - status = TCP_STATUSES[cext.PSUTIL_CONN_NONE] - if fam in (AF_INET, AF_INET6): - if laddr: - laddr = _common.addr(*laddr) - if raddr: - raddr = _common.addr(*raddr) - fam = sockfam_to_enum(fam) - type = socktype_to_enum(type) - nt = _common.pconn(fd, fam, type, laddr, raddr, status) - ret.add(nt) - # On NetBSD the underlying C function does not raise NSP - # in case the process is gone (and the returned list may - # incomplete). - self.name() # raise NSP if the process disappeared on us - return list(ret) + rawlist = cext.net_connections(self.pid, kind) + elif OPENBSD: + rawlist = cext.net_connections(self.pid, families, types) + else: + rawlist = cext.proc_connections(self.pid, families, types) - families, types = conn_tmap[kind] - rawlist = cext.proc_connections(self.pid, families, types) - ret = [] for item in rawlist: - fd, fam, type, laddr, raddr, status = item - if fam in (AF_INET, AF_INET6): - if laddr: - laddr = _common.addr(*laddr) - if raddr: - raddr = _common.addr(*raddr) - fam = sockfam_to_enum(fam) - type = socktype_to_enum(type) - status = TCP_STATUSES[status] - nt = _common.pconn(fd, fam, type, laddr, raddr, status) + fd, fam, type, laddr, raddr, status = item[:6] + if FREEBSD: + if (fam not in families) or (type not in types): + continue + nt = conn_to_ntuple( + fd, fam, type, laddr, raddr, status, TCP_STATUSES + ) ret.append(nt) - if OPENBSD: - # On OpenBSD the underlying C function does not raise NSP - # in case the process is gone (and the returned list may - # incomplete). - self.name() # raise NSP if the process disappeared on us + + self._assert_alive() return ret @wrap_exceptions @@ -786,7 +867,8 @@ def io_counters(self): rawtuple[kinfo_proc_map['read_io_count']], rawtuple[kinfo_proc_map['write_io_count']], -1, - -1) + -1, + ) @wrap_exceptions def cwd(self): @@ -794,50 +876,51 @@ def cwd(self): # sometimes we get an empty string, in which case we turn # it into None if OPENBSD and self.pid == 0: - return None # ...else it would raise EINVAL - elif NETBSD: - with wrap_exceptions_procfs(self): - return os.readlink("/proc/%s/cwd" % self.pid) - elif hasattr(cext, 'proc_open_files'): + return "" # ...else it would raise EINVAL + elif NETBSD or HAS_PROC_OPEN_FILES: # FreeBSD < 8 does not support functions based on # kinfo_getfile() and kinfo_getvmmap() - return cext.proc_cwd(self.pid) or None + return cext.proc_cwd(self.pid) else: raise NotImplementedError( - "supported only starting from FreeBSD 8" if - FREEBSD else "") + "supported only starting from FreeBSD 8" if FREEBSD else "" + ) nt_mmap_grouped = namedtuple( - 'mmap', 'path rss, private, ref_count, shadow_count') + 'mmap', 'path rss, private, ref_count, shadow_count' + ) nt_mmap_ext = namedtuple( - 'mmap', 'addr, perms path rss, private, ref_count, shadow_count') + 'mmap', 'addr, perms path rss, private, ref_count, shadow_count' + ) def _not_implemented(self): raise NotImplementedError # FreeBSD < 8 does not support functions based on kinfo_getfile() # and kinfo_getvmmap() - if hasattr(cext, 'proc_open_files'): + if HAS_PROC_OPEN_FILES: + @wrap_exceptions def open_files(self): """Return files opened by process as a list of namedtuples.""" rawlist = cext.proc_open_files(self.pid) return [_common.popenfile(path, fd) for path, fd in rawlist] + else: open_files = _not_implemented # FreeBSD < 8 does not support functions based on kinfo_getfile() # and kinfo_getvmmap() - if hasattr(cext, 'proc_num_fds'): + if HAS_PROC_NUM_FDS: + @wrap_exceptions def num_fds(self): """Return the number of file descriptors opened by this process.""" ret = cext.proc_num_fds(self.pid) if NETBSD: - # On NetBSD the underlying C function does not raise NSP - # in case the process is gone. - self.name() # raise NSP if the process disappeared on us + self._assert_alive() return ret + else: num_fds = _not_implemented @@ -857,8 +940,9 @@ def cpu_affinity_set(self, cpus): allcpus = tuple(range(len(per_cpu_times()))) for cpu in cpus: if cpu not in allcpus: - raise ValueError("invalid CPU #%i (choose between %s)" - % (cpu, allcpus)) + raise ValueError( + "invalid CPU #%i (choose between %s)" % (cpu, allcpus) + ) try: cext.proc_cpu_affinity_set(self.pid, cpus) except OSError as err: @@ -870,10 +954,24 @@ def cpu_affinity_set(self, cpus): for cpu in cpus: if cpu not in allcpus: raise ValueError( - "invalid CPU #%i (choose between %s)" % ( - cpu, allcpus)) + "invalid CPU #%i (choose between %s)" + % (cpu, allcpus) + ) raise @wrap_exceptions def memory_maps(self): return cext.proc_memory_maps(self.pid) + + @wrap_exceptions + def rlimit(self, resource, limits=None): + if limits is None: + return cext.proc_getrlimit(self.pid, resource) + else: + if len(limits) != 2: + raise ValueError( + "second argument must be a (soft, hard) tuple, got %s" + % repr(limits) + ) + soft, hard = limits + return cext.proc_setrlimit(self.pid, resource, soft, hard) diff --git a/addon/globalPlugins/soundmanager/psutil/_psbsd.pyc b/addon/globalPlugins/soundmanager/psutil/_psbsd.pyc deleted file mode 100644 index d420460..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/_psbsd.pyc and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/_pslinux.py b/addon/globalPlugins/soundmanager/psutil/_pslinux.py index df624de..798dd36 100644 --- a/addon/globalPlugins/soundmanager/psutil/_pslinux.py +++ b/addon/globalPlugins/soundmanager/psutil/_pslinux.py @@ -16,7 +16,6 @@ import socket import struct import sys -import traceback import warnings from collections import defaultdict from collections import namedtuple @@ -25,32 +24,41 @@ from . import _psposix from . import _psutil_linux as cext from . import _psutil_posix as cext_posix -from ._common import ENCODING -from ._common import ENCODING_ERRS -from ._common import isfile_strict -from ._common import memoize -from ._common import memoize_when_activated from ._common import NIC_DUPLEX_FULL from ._common import NIC_DUPLEX_HALF from ._common import NIC_DUPLEX_UNKNOWN +from ._common import AccessDenied +from ._common import NoSuchProcess +from ._common import ZombieProcess +from ._common import bcat +from ._common import cat +from ._common import debug +from ._common import decode +from ._common import get_procfs_path +from ._common import isfile_strict +from ._common import memoize +from ._common import memoize_when_activated +from ._common import open_binary +from ._common import open_text from ._common import parse_environ_block from ._common import path_exists_strict from ._common import supports_ipv6 from ._common import usage_percent +from ._compat import PY3 +from ._compat import FileNotFoundError +from ._compat import PermissionError +from ._compat import ProcessLookupError from ._compat import b from ._compat import basestring -from ._compat import long -from ._compat import PY3 -from ._exceptions import AccessDenied -from ._exceptions import NoSuchProcess -from ._exceptions import ZombieProcess -if sys.version_info >= (3, 4): + +if PY3: import enum else: enum = None +# fmt: off __extra__all__ = [ # 'PROCFS_PATH', @@ -60,7 +68,9 @@ # connection status constants "CONN_ESTABLISHED", "CONN_SYN_SENT", "CONN_SYN_RECV", "CONN_FIN_WAIT1", "CONN_FIN_WAIT2", "CONN_TIME_WAIT", "CONN_CLOSE", "CONN_CLOSE_WAIT", - "CONN_LAST_ACK", "CONN_LISTEN", "CONN_CLOSING", ] + "CONN_LAST_ACK", "CONN_LISTEN", "CONN_CLOSING", +] +# fmt: on # ===================================================================== @@ -69,24 +79,15 @@ POWER_SUPPLY_PATH = "/sys/class/power_supply" -HAS_SMAPS = os.path.exists('/proc/%s/smaps' % os.getpid()) -HAS_PRLIMIT = hasattr(cext, "linux_prlimit") -_DEFAULT = object() - -# RLIMIT_* constants, not guaranteed to be present on all kernels -if HAS_PRLIMIT: - for name in dir(cext): - if name.startswith('RLIM'): - __extra__all__.append(name) +HAS_PROC_SMAPS = os.path.exists('/proc/%s/smaps' % os.getpid()) +HAS_PROC_SMAPS_ROLLUP = os.path.exists('/proc/%s/smaps_rollup' % os.getpid()) +HAS_PROC_IO_PRIORITY = hasattr(cext, "proc_ioprio_get") +HAS_CPU_AFFINITY = hasattr(cext, "proc_cpu_affinity_get") # Number of clock ticks per second CLOCK_TICKS = os.sysconf("SC_CLK_TCK") -PAGESIZE = os.sysconf("SC_PAGE_SIZE") +PAGESIZE = cext_posix.getpagesize() BOOT_TIME = None # set later -# Used when reading "big" files, namely /proc/{pid}/smaps and /proc/net/*. -# On Python 2, using a buffer with open() for such files may result in a -# speedup, see: https://github.com/giampaolo/psutil/issues/708 -BIGFILE_BUFFERING = -1 if PY3 else 8192 LITTLE_ENDIAN = sys.byteorder == 'little' # "man iostat" states that sectors are equivalent with blocks and have @@ -105,8 +106,9 @@ if enum is None: AF_LINK = socket.AF_PACKET else: - AddressFamily = enum.IntEnum('AddressFamily', - {'AF_LINK': int(socket.AF_PACKET)}) + AddressFamily = enum.IntEnum( + 'AddressFamily', {'AF_LINK': int(socket.AF_PACKET)} + ) AF_LINK = AddressFamily.AF_LINK # ioprio_* constants http://linux.die.net/man/2/ioprio_get @@ -116,6 +118,7 @@ IOPRIO_CLASS_BE = 2 IOPRIO_CLASS_IDLE = 3 else: + class IOPriority(enum.IntEnum): IOPRIO_CLASS_NONE = 0 IOPRIO_CLASS_RT = 1 @@ -155,7 +158,7 @@ class IOPriority(enum.IntEnum): "08": _common.CONN_CLOSE_WAIT, "09": _common.CONN_LAST_ACK, "0A": _common.CONN_LISTEN, - "0B": _common.CONN_CLOSING + "0B": _common.CONN_CLOSING, } @@ -164,6 +167,7 @@ class IOPriority(enum.IntEnum): # ===================================================================== +# fmt: off # psutil.virtual_memory() svmem = namedtuple( 'svmem', ['total', 'available', 'percent', 'used', 'free', @@ -194,6 +198,11 @@ class IOPriority(enum.IntEnum): pio = namedtuple('pio', ['read_count', 'write_count', 'read_bytes', 'write_bytes', 'read_chars', 'write_chars']) +# psutil.Process.cpu_times() +pcputimes = namedtuple('pcputimes', + ['user', 'system', 'children_user', 'children_system', + 'iowait']) +# fmt: on # ===================================================================== @@ -201,37 +210,6 @@ class IOPriority(enum.IntEnum): # ===================================================================== -def open_binary(fname, **kwargs): - return open(fname, "rb", **kwargs) - - -def open_text(fname, **kwargs): - """On Python 3 opens a file in text mode by using fs encoding and - a proper en/decoding errors handler. - On Python 2 this is just an alias for open(name, 'rt'). - """ - if PY3: - # See: - # https://github.com/giampaolo/psutil/issues/675 - # https://github.com/giampaolo/psutil/pull/733 - kwargs.setdefault('encoding', ENCODING) - kwargs.setdefault('errors', ENCODING_ERRS) - return open(fname, "rt", **kwargs) - - -if PY3: - def decode(s): - return s.decode(encoding=ENCODING, errors=ENCODING_ERRS) -else: - def decode(s): - return s - - -def get_procfs_path(): - """Return updated psutil.PROCFS_PATH constant.""" - return sys.modules['psutil'].PROCFS_PATH - - def readlink(path): """Wrapper around os.readlink().""" assert isinstance(path, basestring), path @@ -271,7 +249,7 @@ def is_storage_device(name): "nvme0n1p1"). If name is a virtual device (e.g. "loop1", "ram") return True. """ - # Readapted from iostat source code, see: + # Re-adapted from iostat source code, see: # https://github.com/sysstat/sysstat/blob/ # 97912938cd476645b267280069e83b1c8dc0e1c7/common.c#L208 # Some devices may have a slash in their name (e.g. cciss/c0d0...). @@ -309,30 +287,66 @@ def set_scputimes_ntuple(procfs_path): scputimes = namedtuple('scputimes', fields) -def cat(fname, fallback=_DEFAULT, binary=True): - """Return file content. - fallback: the value returned in case the file does not exist or - cannot be read - binary: whether to open the file in binary or text mode. - """ - try: - with open_binary(fname) if binary else open_text(fname) as f: - return f.read().strip() - except (IOError, OSError): - if fallback is not _DEFAULT: - return fallback - else: - raise - - try: set_scputimes_ntuple("/proc") -except Exception: +except Exception as err: # noqa: BLE001 # Don't want to crash at import time. - traceback.print_exc() + debug("ignoring exception on import: %r" % err) scputimes = namedtuple('scputimes', 'user system idle')(0.0, 0.0, 0.0) +# ===================================================================== +# --- prlimit +# ===================================================================== + +# Backport of resource.prlimit() for Python 2. Originally this was done +# in C, but CentOS-6 which we use to create manylinux wheels is too old +# and does not support prlimit() syscall. As such the resulting wheel +# would not include prlimit(), even when installed on newer systems. +# This is the only part of psutil using ctypes. + +prlimit = None +try: + from resource import prlimit # python >= 3.4 +except ImportError: + import ctypes + + libc = ctypes.CDLL(None, use_errno=True) + + if hasattr(libc, "prlimit"): + + def prlimit(pid, resource_, limits=None): + class StructRlimit(ctypes.Structure): + _fields_ = [ + ('rlim_cur', ctypes.c_longlong), + ('rlim_max', ctypes.c_longlong), + ] + + current = StructRlimit() + if limits is None: + # get + ret = libc.prlimit(pid, resource_, None, ctypes.byref(current)) + else: + # set + new = StructRlimit() + new.rlim_cur = limits[0] + new.rlim_max = limits[1] + ret = libc.prlimit( + pid, resource_, ctypes.byref(new), ctypes.byref(current) + ) + + if ret != 0: + errno_ = ctypes.get_errno() + raise OSError(errno_, os.strerror(errno_)) + return (current.rlim_cur, current.rlim_max) + + +if prlimit is not None: + __extra__all__.extend( + [x for x in dir(cext) if x.startswith('RLIM') and x.isupper()] + ) + + # ===================================================================== # --- system memory # ===================================================================== @@ -340,34 +354,46 @@ def cat(fname, fallback=_DEFAULT, binary=True): def calculate_avail_vmem(mems): """Fallback for kernels < 3.14 where /proc/meminfo does not provide - "MemAvailable:" column, see: - https://blog.famzah.net/2014/09/24/ + "MemAvailable", see: + https://blog.famzah.net/2014/09/24/. + This code reimplements the algorithm outlined here: https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/ commit/?id=34e431b0ae398fc54ea69ff85ec700722c9da773 - XXX: on recent kernels this calculation differs by ~1.5% than - "MemAvailable:" as it's calculated slightly differently, see: - https://gitlab.com/procps-ng/procps/issues/42 - https://github.com/famzah/linux-memavailable-procfs/issues/2 + We use this function also when "MemAvailable" returns 0 (possibly a + kernel bug, see: https://github.com/giampaolo/psutil/issues/1915). + In that case this routine matches "free" CLI tool result ("available" + column). + + XXX: on recent kernels this calculation may differ by ~1.5% compared + to "MemAvailable:", as it's calculated slightly differently. It is still way more realistic than doing (free + cached) though. + See: + * https://gitlab.com/procps-ng/procps/issues/42 + * https://github.com/famzah/linux-memavailable-procfs/issues/2 """ - # Fallback for very old distros. According to + # Note about "fallback" value. According to: # https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/ # commit/?id=34e431b0ae398fc54ea69ff85ec700722c9da773 - # ...long ago "avail" was calculated as (free + cached). - # We might fallback in such cases: - # "Active(file)" not available: 2.6.28 / Dec 2008 - # "Inactive(file)" not available: 2.6.28 / Dec 2008 - # "SReclaimable:" not available: 2.6.19 / Nov 2006 - # /proc/zoneinfo not available: 2.6.13 / Aug 2005 + # ...long ago "available" memory was calculated as (free + cached), + # We use fallback when one of these is missing from /proc/meminfo: + # "Active(file)": introduced in 2.6.28 / Dec 2008 + # "Inactive(file)": introduced in 2.6.28 / Dec 2008 + # "SReclaimable": introduced in 2.6.19 / Nov 2006 + # /proc/zoneinfo: introduced in 2.6.13 / Aug 2005 free = mems[b'MemFree:'] fallback = free + mems.get(b"Cached:", 0) try: lru_active_file = mems[b'Active(file):'] lru_inactive_file = mems[b'Inactive(file):'] slab_reclaimable = mems[b'SReclaimable:'] - except KeyError: + except KeyError as err: + debug( + "%s is missing from /proc/meminfo; using an approximation for " + "calculating available memory" + % err.args[0] + ) return fallback try: f = open_binary('%s/zoneinfo' % get_procfs_path()) @@ -381,7 +407,6 @@ def calculate_avail_vmem(mems): if line.startswith(b'low'): watermark_low += int(line.split()[1]) watermark_low *= PAGESIZE - watermark_low = watermark_low avail = free - watermark_low pagecache = lru_active_file + lru_inactive_file @@ -393,19 +418,11 @@ def calculate_avail_vmem(mems): def virtual_memory(): """Report virtual memory stats. - This implementation matches "free" and "vmstat -s" cmdline - utility values and procps-ng-3.3.12 source was used as a reference - (2016-09-18): + This implementation mimicks procps-ng-3.3.12, aka "free" CLI tool: https://gitlab.com/procps-ng/procps/blob/ - 24fd2605c51fccc375ab0287cec33aa767f06718/proc/sysinfo.c - For reference, procps-ng-3.3.10 is the version available on Ubuntu - 16.04. - - Note about "available" memory: up until psutil 4.3 it was - calculated as "avail = (free + buffers + cached)". Now - "MemAvailable:" column (kernel 3.14) from /proc/meminfo is used as - it's more accurate. - That matches "available" column in newer versions of "free". + 24fd2605c51fccc375ab0287cec33aa767f06718/proc/sysinfo.c#L778-791 + The returned values are supposed to match both "free" and "vmstat -s" + CLI tools. """ missing_fields = [] mems = {} @@ -457,10 +474,11 @@ def virtual_memory(): inactive = mems[b"Inactive:"] except KeyError: try: - inactive = \ - mems[b"Inact_dirty:"] + \ - mems[b"Inact_clean:"] + \ - mems[b"Inact_laundry:"] + inactive = ( + mems[b"Inact_dirty:"] + + mems[b"Inact_clean:"] + + mems[b"Inact_laundry:"] + ) except KeyError: inactive = 0 missing_fields.append('inactive') @@ -487,17 +505,23 @@ def virtual_memory(): avail = mems[b'MemAvailable:'] except KeyError: avail = calculate_avail_vmem(mems) + else: + if avail == 0: + # Yes, it can happen (probably a kernel bug): + # https://github.com/giampaolo/psutil/issues/1915 + # In this case "free" CLI tool makes an estimate. We do the same, + # and it matches "free" CLI tool. + avail = calculate_avail_vmem(mems) if avail < 0: avail = 0 missing_fields.append('available') - - # If avail is greater than total or our calculation overflows, - # that's symptomatic of running within a LCX container where such - # values will be dramatically distorted over those of the host. - # https://gitlab.com/procps-ng/procps/blob/ - # 24fd2605c51fccc375ab0287cec33aa767f06718/proc/sysinfo.c#L764 - if avail > total: + elif avail > total: + # If avail is greater than total or our calculation overflows, + # that's symptomatic of running within a LCX container where such + # values will be dramatically distorted over those of the host. + # https://gitlab.com/procps-ng/procps/blob/ + # 24fd2605c51fccc375ab0287cec33aa767f06718/proc/sysinfo.c#L764 avail = free percent = usage_percent((total - avail), total, round_=1) @@ -506,11 +530,23 @@ def virtual_memory(): if missing_fields: msg = "%s memory stats couldn't be determined and %s set to 0" % ( ", ".join(missing_fields), - "was" if len(missing_fields) == 1 else "were") - warnings.warn(msg, RuntimeWarning) - - return svmem(total, avail, percent, used, free, - active, inactive, buffers, cached, shared, slab) + "was" if len(missing_fields) == 1 else "were", + ) + warnings.warn(msg, RuntimeWarning, stacklevel=2) + + return svmem( + total, + avail, + percent, + used, + free, + active, + inactive, + buffers, + cached, + shared, + slab, + ) def swap_memory(): @@ -539,9 +575,11 @@ def swap_memory(): f = open_binary("%s/vmstat" % get_procfs_path()) except IOError as err: # see https://github.com/giampaolo/psutil/issues/722 - msg = "'sin' and 'sout' swap memory stats couldn't " \ - "be determined and were set to 0 (%s)" % str(err) - warnings.warn(msg, RuntimeWarning) + msg = ( + "'sin' and 'sout' swap memory stats couldn't " + + "be determined and were set to 0 (%s)" % str(err) + ) + warnings.warn(msg, RuntimeWarning, stacklevel=2) sin = sout = 0 else: with f: @@ -559,9 +597,9 @@ def swap_memory(): # we might get here when dealing with exotic Linux # flavors, see: # https://github.com/giampaolo/psutil/issues/313 - msg = "'sin' and 'sout' swap memory stats couldn't " \ - "be determined and were set to 0" - warnings.warn(msg, RuntimeWarning) + msg = "'sin' and 'sout' swap memory stats couldn't " + msg += "be determined and were set to 0" + warnings.warn(msg, RuntimeWarning, stacklevel=2) sin = sout = 0 return _common.sswap(total, used, free, percent, sin, sout) @@ -582,7 +620,7 @@ def cpu_times(): set_scputimes_ntuple(procfs_path) with open_binary('%s/stat' % procfs_path) as f: values = f.readline().split() - fields = values[1:len(scputimes._fields) + 1] + fields = values[1 : len(scputimes._fields) + 1] fields = [float(x) / CLOCK_TICKS for x in fields] return scputimes(*fields) @@ -600,7 +638,7 @@ def per_cpu_times(): for line in f: if line.startswith(b'cpu'): values = line.split() - fields = values[1:len(scputimes._fields) + 1] + fields = values[1 : len(scputimes._fields) + 1] fields = [float(x) / CLOCK_TICKS for x in fields] entry = scputimes(*fields) cpus.append(entry) @@ -636,8 +674,25 @@ def cpu_count_logical(): return num -def cpu_count_physical(): - """Return the number of physical cores in the system.""" +def cpu_count_cores(): + """Return the number of CPU cores in the system.""" + # Method #1 + ls = set() + # These 2 files are the same but */core_cpus_list is newer while + # */thread_siblings_list is deprecated and may disappear in the future. + # https://www.kernel.org/doc/Documentation/admin-guide/cputopology.rst + # https://github.com/giampaolo/psutil/pull/1727#issuecomment-707624964 + # https://lkml.org/lkml/2019/2/26/41 + p1 = "/sys/devices/system/cpu/cpu[0-9]*/topology/core_cpus_list" + p2 = "/sys/devices/system/cpu/cpu[0-9]*/topology/thread_siblings_list" + for path in glob.glob(p1) or glob.glob(p2): + with open_binary(path) as f: + ls.add(f.read().strip()) + result = len(ls) + if result != 0: + return result + + # Method #2 mapping = {} current_info = {} with open_binary('%s/cpuinfo' % get_procfs_path()) as f: @@ -645,20 +700,21 @@ def cpu_count_physical(): line = line.strip().lower() if not line: # new section - if (b'physical id' in current_info and - b'cpu cores' in current_info): - mapping[current_info[b'physical id']] = \ - current_info[b'cpu cores'] + try: + mapping[current_info[b'physical id']] = current_info[ + b'cpu cores' + ] + except KeyError: + pass current_info = {} else: # ongoing section - if (line.startswith(b'physical id') or - line.startswith(b'cpu cores')): + if line.startswith((b'physical id', b'cpu cores')): key, value = line.split(b'\t:', 1) current_info[key] = int(value) - # mimic os.cpu_count() - return sum(mapping.values()) or None + result = sum(mapping.values()) + return result or None # mimic os.cpu_count() def cpu_stats(): @@ -674,52 +730,72 @@ def cpu_stats(): interrupts = int(line.split()[1]) elif line.startswith(b'softirq'): soft_interrupts = int(line.split()[1]) - if ctx_switches is not None and soft_interrupts is not None \ - and interrupts is not None: + if ( + ctx_switches is not None + and soft_interrupts is not None + and interrupts is not None + ): break syscalls = 0 return _common.scpustats( - ctx_switches, interrupts, soft_interrupts, syscalls) + ctx_switches, interrupts, soft_interrupts, syscalls + ) + + +def _cpu_get_cpuinfo_freq(): + """Return current CPU frequency from cpuinfo if available.""" + ret = [] + with open_binary('%s/cpuinfo' % get_procfs_path()) as f: + for line in f: + if line.lower().startswith(b'cpu mhz'): + ret.append(float(line.split(b':', 1)[1])) + return ret -if os.path.exists("/sys/devices/system/cpu/cpufreq") or \ - os.path.exists("/sys/devices/system/cpu/cpu0/cpufreq"): +if os.path.exists("/sys/devices/system/cpu/cpufreq/policy0") or os.path.exists( + "/sys/devices/system/cpu/cpu0/cpufreq" +): + def cpu_freq(): """Return frequency metrics for all CPUs. Contrarily to other OSes, Linux updates these values in real-time. """ - # scaling_* files seem preferable to cpuinfo_*, see: - # http://unix.stackexchange.com/a/87537/168884 + cpuinfo_freqs = _cpu_get_cpuinfo_freq() + paths = glob.glob( + "/sys/devices/system/cpu/cpufreq/policy[0-9]*" + ) or glob.glob("/sys/devices/system/cpu/cpu[0-9]*/cpufreq") + paths.sort(key=lambda x: int(re.search(r"[0-9]+", x).group())) ret = [] - ls = glob.glob("/sys/devices/system/cpu/cpufreq/policy*") - if ls: - # Sort the list so that '10' comes after '2'. This should - # ensure the CPU order is consistent with other CPU functions - # having a 'percpu' argument and returning results for multiple - # CPUs (cpu_times(), cpu_percent(), cpu_times_percent()). - ls.sort(key=lambda x: int(os.path.basename(x)[6:])) - else: - # https://github.com/giampaolo/psutil/issues/981 - ls = glob.glob("/sys/devices/system/cpu/cpu[0-9]*/cpufreq") - ls.sort(key=lambda x: int(re.search('[0-9]+', x).group(0))) - pjoin = os.path.join - for path in ls: - curr = cat(pjoin(path, "scaling_cur_freq"), fallback=None) + for i, path in enumerate(paths): + if len(paths) == len(cpuinfo_freqs): + # take cached value from cpuinfo if available, see: + # https://github.com/giampaolo/psutil/issues/1851 + curr = cpuinfo_freqs[i] * 1000 + else: + curr = bcat(pjoin(path, "scaling_cur_freq"), fallback=None) if curr is None: # Likely an old RedHat, see: # https://github.com/giampaolo/psutil/issues/1071 - curr = cat(pjoin(path, "cpuinfo_cur_freq"), fallback=None) + curr = bcat(pjoin(path, "cpuinfo_cur_freq"), fallback=None) if curr is None: - raise NotImplementedError( - "can't find current frequency file") + msg = "can't find current frequency file" + raise NotImplementedError(msg) curr = int(curr) / 1000 - max_ = int(cat(pjoin(path, "scaling_max_freq"))) / 1000 - min_ = int(cat(pjoin(path, "scaling_min_freq"))) / 1000 + max_ = int(bcat(pjoin(path, "scaling_max_freq"))) / 1000 + min_ = int(bcat(pjoin(path, "scaling_min_freq"))) / 1000 ret.append(_common.scpufreq(curr, min_, max_)) return ret +else: + + def cpu_freq(): + """Alternate implementation using /proc/cpuinfo. + min and max frequencies are not available and are set to None. + """ + return [_common.scpufreq(x, 0.0, 0.0) for x in _cpu_get_cpuinfo_freq()] + # ===================================================================== # --- network @@ -746,6 +822,8 @@ class Connections: """ def __init__(self): + # The string represents the basename of the corresponding + # /proc/net/{proto_name} file. tcp4 = ("tcp", socket.AF_INET, socket.SOCK_STREAM) tcp6 = ("tcp6", socket.AF_INET6, socket.SOCK_STREAM) udp4 = ("udp", socket.AF_INET, socket.SOCK_DGRAM) @@ -771,17 +849,20 @@ def get_proc_inodes(self, pid): for fd in os.listdir("%s/%s/fd" % (self._procfs_path, pid)): try: inode = readlink("%s/%s/fd/%s" % (self._procfs_path, pid, fd)) - except OSError as err: + except (FileNotFoundError, ProcessLookupError): # ENOENT == file which is gone in the meantime; # os.stat('/proc/%s' % self.pid) will be done later # to force NSP (if it's the case) - if err.errno in (errno.ENOENT, errno.ESRCH): - continue - elif err.errno == errno.EINVAL: + continue + except OSError as err: + if err.errno == errno.EINVAL: # not a link continue - else: - raise + if err.errno == errno.ENAMETOOLONG: + # file name too long + debug(err) + continue + raise else: if inode.startswith('socket:['): # the process is using a socket @@ -794,7 +875,7 @@ def get_all_inodes(self): for pid in pids(): try: inodes.update(self.get_proc_inodes(pid)) - except OSError as err: + except (FileNotFoundError, ProcessLookupError, PermissionError): # os.listdir() is gonna raise a lot of access denied # exceptions in case of unprivileged user; that's fine # as we'll just end up returning a connection with PID @@ -802,9 +883,7 @@ def get_all_inodes(self): # Both netstat -an and lsof does the same so it's # unlikely we can do any better. # ENOENT just means a PID disappeared on us. - if err.errno not in ( - errno.ENOENT, errno.ESRCH, errno.EPERM, errno.EACCES): - raise + continue return inodes @staticmethod @@ -839,21 +918,19 @@ def decode_address(addr, family): else: ip = socket.inet_ntop(family, base64.b16decode(ip)) else: # IPv6 - # old version - let's keep it, just in case... - # ip = ip.decode('hex') - # return socket.inet_ntop(socket.AF_INET6, - # ''.join(ip[i:i+4][::-1] for i in xrange(0, 16, 4))) ip = base64.b16decode(ip) try: # see: https://github.com/giampaolo/psutil/issues/201 if LITTLE_ENDIAN: ip = socket.inet_ntop( socket.AF_INET6, - struct.pack('>4I', *struct.unpack('<4I', ip))) + struct.pack('>4I', *struct.unpack('<4I', ip)), + ) else: ip = socket.inet_ntop( socket.AF_INET6, - struct.pack('<4I', *struct.unpack('<4I', ip))) + struct.pack('<4I', *struct.unpack('<4I', ip)), + ) except ValueError: # see: https://github.com/giampaolo/psutil/issues/623 if not supports_ipv6(): @@ -868,22 +945,24 @@ def process_inet(file, family, type_, inodes, filter_pid=None): if file.endswith('6') and not os.path.exists(file): # IPv6 not supported return - with open_text(file, buffering=BIGFILE_BUFFERING) as f: + with open_text(file) as f: f.readline() # skip the first line for lineno, line in enumerate(f, 1): try: - _, laddr, raddr, status, _, _, _, _, _, inode = \ + _, laddr, raddr, status, _, _, _, _, _, inode = ( line.split()[:10] + ) except ValueError: raise RuntimeError( - "error while parsing %s; malformed line %s %r" % ( - file, lineno, line)) + "error while parsing %s; malformed line %s %r" + % (file, lineno, line) + ) if inode in inodes: # # We assume inet sockets are unique, so we error # # out if there are multiple references to the # # same inode. We won't do this for UNIX sockets. # if len(inodes[inode]) > 1 and family != socket.AF_UNIX: - # raise ValueError("ambiguos inode with multiple " + # raise ValueError("ambiguous inode with multiple " # "PIDs references") pid, fd = inodes[inode][0] else: @@ -905,7 +984,7 @@ def process_inet(file, family, type_, inodes, filter_pid=None): @staticmethod def process_unix(file, family, inodes, filter_pid=None): """Parse /proc/net/unix files.""" - with open_text(file, buffering=BIGFILE_BUFFERING) as f: + with open_text(file) as f: f.readline() # skip the first line for line in f: tokens = line.split() @@ -916,9 +995,10 @@ def process_unix(file, family, inodes, filter_pid=None): # see: https://github.com/giampaolo/psutil/issues/766 continue raise RuntimeError( - "error while parsing %s; malformed line %r" % ( - file, line)) - if inode in inodes: + "error while parsing %s; malformed line %r" + % (file, line) + ) + if inode in inodes: # noqa # With UNIX sockets we can have a single inode # referencing many file descriptors. pairs = inodes[inode] @@ -928,11 +1008,8 @@ def process_unix(file, family, inodes, filter_pid=None): if filter_pid is not None and filter_pid != pid: continue else: - if len(tokens) == 8: - path = tokens[-1] - else: - path = "" - type_ = int(type_) + path = tokens[-1] if len(tokens) == 8 else '' + type_ = _common.socktype_to_enum(int(type_)) # XXX: determining the remote endpoint of a # UNIX socket on Linux is not possible, see: # https://serverfault.com/questions/252723/ @@ -942,8 +1019,10 @@ def process_unix(file, family, inodes, filter_pid=None): def retrieve(self, kind, pid=None): if kind not in self.tmap: - raise ValueError("invalid %r kind argument; choose between %s" - % (kind, ', '.join([repr(x) for x in self.tmap]))) + raise ValueError( + "invalid %r kind argument; choose between %s" + % (kind, ', '.join([repr(x) for x in self.tmap])) + ) self._procfs_path = get_procfs_path() if pid is not None: inodes = self.get_proc_inodes(pid) @@ -953,22 +1032,23 @@ def retrieve(self, kind, pid=None): else: inodes = self.get_all_inodes() ret = set() - for f, family, type_ in self.tmap[kind]: + for proto_name, family, type_ in self.tmap[kind]: + path = "%s/net/%s" % (self._procfs_path, proto_name) if family in (socket.AF_INET, socket.AF_INET6): ls = self.process_inet( - "%s/net/%s" % (self._procfs_path, f), - family, type_, inodes, filter_pid=pid) + path, family, type_, inodes, filter_pid=pid + ) else: - ls = self.process_unix( - "%s/net/%s" % (self._procfs_path, f), - family, inodes, filter_pid=pid) + ls = self.process_unix(path, family, inodes, filter_pid=pid) for fd, family, type_, laddr, raddr, status, bound_pid in ls: if pid: - conn = _common.pconn(fd, family, type_, laddr, raddr, - status) + conn = _common.pconn( + fd, family, type_, laddr, raddr, status + ) else: - conn = _common.sconn(fd, family, type_, laddr, raddr, - status, bound_pid) + conn = _common.sconn( + fd, family, type_, laddr, raddr, status, bound_pid + ) ret.add(conn) return list(ret) @@ -992,50 +1072,68 @@ def net_io_counters(): colon = line.rfind(':') assert colon > 0, repr(line) name = line[:colon].strip() - fields = line[colon + 1:].strip().split() + fields = line[colon + 1 :].strip().split() # in - (bytes_recv, - packets_recv, - errin, - dropin, - fifoin, # unused - framein, # unused - compressedin, # unused - multicastin, # unused - # out - bytes_sent, - packets_sent, - errout, - dropout, - fifoout, # unused - collisionsout, # unused - carrierout, # unused - compressedout) = map(int, fields) - - retdict[name] = (bytes_sent, bytes_recv, packets_sent, packets_recv, - errin, errout, dropin, dropout) + ( + bytes_recv, + packets_recv, + errin, + dropin, + fifoin, # unused + framein, # unused + compressedin, # unused + multicastin, # unused + # out + bytes_sent, + packets_sent, + errout, + dropout, + fifoout, # unused + collisionsout, # unused + carrierout, # unused + compressedout, + ) = map(int, fields) + + retdict[name] = ( + bytes_sent, + bytes_recv, + packets_sent, + packets_recv, + errin, + errout, + dropin, + dropout, + ) return retdict def net_if_stats(): """Get NIC stats (isup, duplex, speed, mtu).""" - duplex_map = {cext.DUPLEX_FULL: NIC_DUPLEX_FULL, - cext.DUPLEX_HALF: NIC_DUPLEX_HALF, - cext.DUPLEX_UNKNOWN: NIC_DUPLEX_UNKNOWN} + duplex_map = { + cext.DUPLEX_FULL: NIC_DUPLEX_FULL, + cext.DUPLEX_HALF: NIC_DUPLEX_HALF, + cext.DUPLEX_UNKNOWN: NIC_DUPLEX_UNKNOWN, + } names = net_io_counters().keys() ret = {} for name in names: try: mtu = cext_posix.net_if_mtu(name) - isup = cext_posix.net_if_flags(name) + flags = cext_posix.net_if_flags(name) duplex, speed = cext.net_if_duplex_speed(name) except OSError as err: # https://github.com/giampaolo/psutil/issues/1279 if err.errno != errno.ENODEV: raise + else: + debug(err) else: - ret[name] = _common.snicstats(isup, duplex_map[duplex], speed, mtu) + output_flags = ','.join(flags) + isup = 'running' in flags + ret[name] = _common.snicstats( + isup, duplex_map[duplex], speed, mtu, output_flags + ) return ret @@ -1051,6 +1149,7 @@ def disk_io_counters(perdisk=False): """Return disk I/O statistics for every disk installed on the system as a dict of raw tuples. """ + def read_procfs(): # OK, this is a bit confusing. The format of /proc/diskstats can # have 3 variations. @@ -1062,6 +1161,9 @@ def read_procfs(): # ...unless (Linux 2.6) the line refers to a partition instead # of a disk, in which case the line has less fields (7): # "3 1 hda1 8 8 8 8" + # 4.18+ has 4 fields added: + # "3 0 hda 8 8 8 8 8 8 8 8 8 8 8 0 0 0 0" + # 5.5 has 2 more fields. # See: # https://www.kernel.org/doc/Documentation/iostats.txt # https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats @@ -1070,13 +1172,14 @@ def read_procfs(): for line in lines: fields = line.split() flen = len(fields) + # fmt: off if flen == 15: # Linux 2.4 name = fields[3] reads = int(fields[2]) (reads_merged, rbytes, rtime, writes, writes_merged, wbytes, wtime, _, busy_time, _) = map(int, fields[4:14]) - elif flen == 14: + elif flen == 14 or flen >= 18: # Linux 2.6+, line referring to a disk name = fields[2] (reads, reads_merged, rbytes, rtime, writes, writes_merged, @@ -1090,6 +1193,7 @@ def read_procfs(): raise ValueError("not sure how to interpret line %r" % line) yield (name, reads, writes, rbytes, wbytes, rtime, wtime, reads_merged, writes_merged, busy_time) + # fmt: on def read_sysfs(): for block in os.listdir('/sys/block'): @@ -1099,10 +1203,12 @@ def read_sysfs(): with open_text(os.path.join(root, 'stat')) as f: fields = f.read().strip().split() name = os.path.basename(root) + # fmt: off (reads, reads_merged, rbytes, rtime, writes, writes_merged, - wbytes, wtime, _, busy_time, _) = map(int, fields) + wbytes, wtime, _, busy_time) = map(int, fields[:10]) yield (name, reads, writes, rbytes, wbytes, rtime, wtime, reads_merged, writes_merged, busy_time) + # fmt: on if os.path.exists('%s/diskstats' % get_procfs_path()): gen = read_procfs() @@ -1111,10 +1217,13 @@ def read_sysfs(): else: raise NotImplementedError( "%s/diskstats nor /sys/block filesystem are available on this " - "system" % get_procfs_path()) + "system" + % get_procfs_path() + ) retdict = {} for entry in gen: + # fmt: off (name, reads, writes, rbytes, wbytes, rtime, wtime, reads_merged, writes_merged, busy_time) = entry if not perdisk and not is_storage_device(name): @@ -1135,35 +1244,125 @@ def read_sysfs(): wbytes *= DISK_SECTOR_SIZE retdict[name] = (reads, writes, rbytes, wbytes, rtime, wtime, reads_merged, writes_merged, busy_time) + # fmt: on return retdict +class RootFsDeviceFinder: + """disk_partitions() may return partitions with device == "/dev/root" + or "rootfs". This container class uses different strategies to try to + obtain the real device path. Resources: + https://bootlin.com/blog/find-root-device/ + https://www.systutorials.com/how-to-find-the-disk-where-root-is-on-in-bash-on-linux/. + """ + + __slots__ = ['major', 'minor'] + + def __init__(self): + dev = os.stat("/").st_dev + self.major = os.major(dev) + self.minor = os.minor(dev) + + def ask_proc_partitions(self): + with open_text("%s/partitions" % get_procfs_path()) as f: + for line in f.readlines()[2:]: + fields = line.split() + if len(fields) < 4: # just for extra safety + continue + major = int(fields[0]) if fields[0].isdigit() else None + minor = int(fields[1]) if fields[1].isdigit() else None + name = fields[3] + if major == self.major and minor == self.minor: + if name: # just for extra safety + return "/dev/%s" % name + + def ask_sys_dev_block(self): + path = "/sys/dev/block/%s:%s/uevent" % (self.major, self.minor) + with open_text(path) as f: + for line in f: + if line.startswith("DEVNAME="): + name = line.strip().rpartition("DEVNAME=")[2] + if name: # just for extra safety + return "/dev/%s" % name + + def ask_sys_class_block(self): + needle = "%s:%s" % (self.major, self.minor) + files = glob.iglob("/sys/class/block/*/dev") + for file in files: + try: + f = open_text(file) + except FileNotFoundError: # race condition + continue + else: + with f: + data = f.read().strip() + if data == needle: + name = os.path.basename(os.path.dirname(file)) + return "/dev/%s" % name + + def find(self): + path = None + if path is None: + try: + path = self.ask_proc_partitions() + except (IOError, OSError) as err: + debug(err) + if path is None: + try: + path = self.ask_sys_dev_block() + except (IOError, OSError) as err: + debug(err) + if path is None: + try: + path = self.ask_sys_class_block() + except (IOError, OSError) as err: + debug(err) + # We use exists() because the "/dev/*" part of the path is hard + # coded, so we want to be sure. + if path is not None and os.path.exists(path): + return path + + def disk_partitions(all=False): """Return mounted disk partitions as a list of namedtuples.""" fstypes = set() - with open_text("%s/filesystems" % get_procfs_path()) as f: - for line in f: - line = line.strip() - if not line.startswith("nodev"): - fstypes.add(line.strip()) - else: - # ignore all lines starting with "nodev" except "nodev zfs" - fstype = line.split("\t")[1] - if fstype == "zfs": - fstypes.add("zfs") + procfs_path = get_procfs_path() + if not all: + with open_text("%s/filesystems" % procfs_path) as f: + for line in f: + line = line.strip() + if not line.startswith("nodev"): + fstypes.add(line.strip()) + else: + # ignore all lines starting with "nodev" except "nodev zfs" + fstype = line.split("\t")[1] + if fstype == "zfs": + fstypes.add("zfs") + + # See: https://github.com/giampaolo/psutil/issues/1307 + if procfs_path == "/proc" and os.path.isfile('/etc/mtab'): + mounts_path = os.path.realpath("/etc/mtab") + else: + mounts_path = os.path.realpath("%s/self/mounts" % procfs_path) retlist = [] - partitions = cext.disk_partitions() + partitions = cext.disk_partitions(mounts_path) for partition in partitions: device, mountpoint, fstype, opts = partition if device == 'none': device = '' + if device in ("/dev/root", "rootfs"): + device = RootFsDeviceFinder().find() or device if not all: if device == '' or fstype not in fstypes: continue - ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) + maxfile = maxpath = None # set later + ntuple = _common.sdiskpart( + device, mountpoint, fstype, opts, maxfile, maxpath + ) retlist.append(ntuple) + return retlist @@ -1193,13 +1392,26 @@ def sensors_temperatures(): basenames.extend(glob.glob('/sys/class/hwmon/hwmon*/device/temp*_*')) basenames = sorted(set([x.split('_')[0] for x in basenames])) + # Only add the coretemp hwmon entries if they're not already in + # /sys/class/hwmon/ + # https://github.com/giampaolo/psutil/issues/1708 + # https://github.com/giampaolo/psutil/pull/1648 + basenames2 = glob.glob( + '/sys/devices/platform/coretemp.*/hwmon/hwmon*/temp*_*' + ) + repl = re.compile('/sys/devices/platform/coretemp.*/hwmon/') + for name in basenames2: + altname = repl.sub('/sys/class/hwmon/', name) + if altname not in basenames: + basenames.append(name) + for base in basenames: try: path = base + '_input' - current = float(cat(path)) / 1000.0 + current = float(bcat(path)) / 1000.0 path = os.path.join(os.path.dirname(base), 'name') - unit_name = cat(path, binary=False) - except (IOError, OSError, ValueError) as err: + unit_name = cat(path).strip() + except (IOError, OSError, ValueError): # A lot of things can go wrong here, so let's just skip the # whole entry. Sure thing is Linux's /sys/class/hwmon really # is a stinky broken mess. @@ -1208,13 +1420,11 @@ def sensors_temperatures(): # https://github.com/giampaolo/psutil/issues/1129 # https://github.com/giampaolo/psutil/issues/1245 # https://github.com/giampaolo/psutil/issues/1323 - warnings.warn("ignoring %r for file %r" % (err, path), - RuntimeWarning) continue - high = cat(base + '_max', fallback=None) - critical = cat(base + '_crit', fallback=None) - label = cat(base + '_label', fallback='', binary=False) + high = bcat(base + '_max', fallback=None) + critical = bcat(base + '_crit', fallback=None) + label = cat(base + '_label', fallback='').strip() if high is not None: try: @@ -1229,7 +1439,54 @@ def sensors_temperatures(): ret[unit_name].append((label, current, high, critical)) - return ret + # Indication that no sensors were detected in /sys/class/hwmon/ + if not basenames: + basenames = glob.glob('/sys/class/thermal/thermal_zone*') + basenames = sorted(set(basenames)) + + for base in basenames: + try: + path = os.path.join(base, 'temp') + current = float(bcat(path)) / 1000.0 + path = os.path.join(base, 'type') + unit_name = cat(path).strip() + except (IOError, OSError, ValueError) as err: + debug(err) + continue + + trip_paths = glob.glob(base + '/trip_point*') + trip_points = set([ + '_'.join(os.path.basename(p).split('_')[0:3]) + for p in trip_paths + ]) + critical = None + high = None + for trip_point in trip_points: + path = os.path.join(base, trip_point + "_type") + trip_type = cat(path, fallback='').strip() + if trip_type == 'critical': + critical = bcat( + os.path.join(base, trip_point + "_temp"), fallback=None + ) + elif trip_type == 'high': + high = bcat( + os.path.join(base, trip_point + "_temp"), fallback=None + ) + + if high is not None: + try: + high = float(high) / 1000.0 + except ValueError: + high = None + if critical is not None: + try: + critical = float(critical) / 1000.0 + except ValueError: + critical = None + + ret[unit_name].append(('', current, high, critical)) + + return dict(ret) def sensors_fans(): @@ -1252,13 +1509,12 @@ def sensors_fans(): basenames = sorted(set([x.split('_')[0] for x in basenames])) for base in basenames: try: - current = int(cat(base + '_input')) + current = int(bcat(base + '_input')) except (IOError, OSError) as err: - warnings.warn("ignoring %r" % err, RuntimeWarning) + debug(err) continue - unit_name = cat(os.path.join(os.path.dirname(base), 'name'), - binary=False) - label = cat(base + '_label', fallback='', binary=False) + unit_name = cat(os.path.join(os.path.dirname(base), 'name')).strip() + label = cat(base + '_label', fallback='').strip() ret[unit_name].append(_common.sfan(label, current)) return dict(ret) @@ -1269,21 +1525,28 @@ def sensors_battery(): Implementation note: it appears /sys/class/power_supply/BAT0/ directory structure may vary and provide files with the same meaning but under different names, see: - https://github.com/giampaolo/psutil/issues/966 + https://github.com/giampaolo/psutil/issues/966. """ null = object() - def multi_cat(*paths): + def multi_bcat(*paths): """Attempt to read the content of multiple files which may not exist. If none of them exist return None. """ for path in paths: - ret = cat(path, fallback=null) + ret = bcat(path, fallback=null) if ret != null: - return int(ret) if ret.isdigit() else ret + try: + return int(ret) + except ValueError: + return ret.strip() return None - bats = [x for x in os.listdir(POWER_SUPPLY_PATH) if x.startswith('BAT')] + bats = [ + x + for x in os.listdir(POWER_SUPPLY_PATH) + if x.startswith('BAT') or 'battery' in x.lower() + ] if not bats: return None # Get the first available battery. Usually this is "BAT0", except @@ -1292,21 +1555,14 @@ def multi_cat(*paths): root = os.path.join(POWER_SUPPLY_PATH, sorted(bats)[0]) # Base metrics. - energy_now = multi_cat( - root + "/energy_now", - root + "/charge_now") - power_now = multi_cat( - root + "/power_now", - root + "/current_now") - energy_full = multi_cat( - root + "/energy_full", - root + "/charge_full") - if energy_now is None or power_now is None: - return None + energy_now = multi_bcat(root + "/energy_now", root + "/charge_now") + power_now = multi_bcat(root + "/power_now", root + "/current_now") + energy_full = multi_bcat(root + "/energy_full", root + "/charge_full") + time_to_empty = multi_bcat(root + "/time_to_empty_now") # Percent. If we have energy_full the percentage will be more # accurate compared to reading /capacity file (float vs. int). - if energy_full is not None: + if energy_full is not None and energy_now is not None: try: percent = 100.0 * energy_now / energy_full except ZeroDivisionError: @@ -1320,13 +1576,14 @@ def multi_cat(*paths): # Note: AC0 is not always available and sometimes (e.g. CentOS7) # it's called "AC". power_plugged = None - online = multi_cat( + online = multi_bcat( os.path.join(POWER_SUPPLY_PATH, "AC0/online"), - os.path.join(POWER_SUPPLY_PATH, "AC/online")) + os.path.join(POWER_SUPPLY_PATH, "AC/online"), + ) if online is not None: power_plugged = online == 1 else: - status = cat(root + "/status", fallback="", binary=False).lower() + status = cat(root + "/status", fallback="").strip().lower() if status == "discharging": power_plugged = False elif status in ("charging", "full"): @@ -1338,11 +1595,17 @@ def multi_cat(*paths): # 013937745fd9050c30146290e8f963d65c0179e6/bin/battery.py#L55 if power_plugged: secsleft = _common.POWER_TIME_UNLIMITED - else: + elif energy_now is not None and power_now is not None: try: secsleft = int(energy_now / power_now * 3600) except ZeroDivisionError: secsleft = _common.POWER_TIME_UNKNOWN + elif time_to_empty is not None: + secsleft = int(time_to_empty * 60) + if secsleft < 0: + secsleft = _common.POWER_TIME_UNKNOWN + else: + secsleft = _common.POWER_TIME_UNKNOWN return _common.sbattery(percent, secsleft, power_plugged) @@ -1357,14 +1620,7 @@ def users(): retlist = [] rawlist = cext.users() for item in rawlist: - user, tty, hostname, tstamp, user_process, pid = item - # note: the underlying C function includes entries about - # system boot, run level and others. We might want - # to use them in the future. - if not user_process: - continue - if hostname in (':0.0', ':0'): - hostname = 'localhost' + user, tty, hostname, tstamp, pid = item nt = _common.suser(user, tty or None, hostname, tstamp, pid) retlist.append(nt) return retlist @@ -1380,8 +1636,7 @@ def boot_time(): ret = float(line.strip().split()[1]) BOOT_TIME = ret return ret - raise RuntimeError( - "line 'btime' not found in %s" % path) + raise RuntimeError("line 'btime' not found in %s" % path) # ===================================================================== @@ -1436,14 +1691,13 @@ def ppid_map(): try: with open_binary("%s/%s/stat" % (procfs_path, pid)) as f: data = f.read() - except EnvironmentError as err: + except (FileNotFoundError, ProcessLookupError): # Note: we should be able to access /stat for all processes # aka it's unlikely we'll bump into EPERM, which is good. - if err.errno not in (errno.ENOENT, errno.ESRCH): - raise + pass else: rpar = data.rfind(b')') - dset = data[rpar + 2:].split() + dset = data[rpar + 2 :].split() ppid = int(dset[1]) ret[pid] = ppid return ret @@ -1453,31 +1707,29 @@ def wrap_exceptions(fun): """Decorator which translates bare OSError and IOError exceptions into NoSuchProcess and AccessDenied. """ + @functools.wraps(fun) def wrapper(self, *args, **kwargs): try: return fun(self, *args, **kwargs) - except EnvironmentError as err: - if err.errno in (errno.EPERM, errno.EACCES): - raise AccessDenied(self.pid, self._name) - # ESRCH (no such process) can be raised on read() if - # process is gone in the meantime. - if err.errno == errno.ESRCH: - raise NoSuchProcess(self.pid, self._name) - # ENOENT (no such file or directory) can be raised on open(). - if err.errno == errno.ENOENT and not os.path.exists("%s/%s" % ( - self._procfs_path, self.pid)): + except PermissionError: + raise AccessDenied(self.pid, self._name) + except ProcessLookupError: + self._raise_if_zombie() + raise NoSuchProcess(self.pid, self._name) + except FileNotFoundError: + self._raise_if_zombie() + if not os.path.exists("%s/%s" % (self._procfs_path, self.pid)): raise NoSuchProcess(self.pid, self._name) - # Note: zombies will keep existing under /proc until they're - # gone so there's no way to distinguish them in here. raise + return wrapper -class Process(object): +class Process: """Linux process implementation.""" - __slots__ = ["pid", "_name", "_ppid", "_procfs_path"] + __slots__ = ["pid", "_name", "_ppid", "_procfs_path", "_cache"] def __init__(self, pid): self.pid = pid @@ -1485,26 +1737,67 @@ def __init__(self, pid): self._ppid = None self._procfs_path = get_procfs_path() + def _is_zombie(self): + # Note: most of the times Linux is able to return info about the + # process even if it's a zombie, and /proc/{pid} will exist. + # There are some exceptions though, like exe(), cmdline() and + # memory_maps(). In these cases /proc/{pid}/{file} exists but + # it's empty. Instead of returning a "null" value we'll raise an + # exception. + try: + data = bcat("%s/%s/stat" % (self._procfs_path, self.pid)) + except (IOError, OSError): + return False + else: + rpar = data.rfind(b')') + status = data[rpar + 2 : rpar + 3] + return status == b"Z" + + def _raise_if_zombie(self): + if self._is_zombie(): + raise ZombieProcess(self.pid, self._name, self._ppid) + + def _raise_if_not_alive(self): + """Raise NSP if the process disappeared on us.""" + # For those C function who do not raise NSP, possibly returning + # incorrect or incomplete result. + os.stat('%s/%s' % (self._procfs_path, self.pid)) + + @wrap_exceptions @memoize_when_activated def _parse_stat_file(self): - """Parse /proc/{pid}/stat file. Return a list of fields where - process name is in position 0. + """Parse /proc/{pid}/stat file and return a dict with various + process info. Using "man proc" as a reference: where "man proc" refers to - position N, always substract 2 (e.g starttime pos 22 in - 'man proc' == pos 20 in the list returned here). + position N always subtract 3 (e.g ppid position 4 in + 'man proc' == position 1 in here). The return value is cached in case oneshot() ctx manager is in use. """ - with open_binary("%s/%s/stat" % (self._procfs_path, self.pid)) as f: - data = f.read() + data = bcat("%s/%s/stat" % (self._procfs_path, self.pid)) # Process name is between parentheses. It can contain spaces and # other parentheses. This is taken into account by looking for - # the first occurrence of "(" and the last occurence of ")". + # the first occurrence of "(" and the last occurrence of ")". rpar = data.rfind(b')') - name = data[data.find(b'(') + 1:rpar] - others = data[rpar + 2:].split() - return [name] + others + name = data[data.find(b'(') + 1 : rpar] + fields = data[rpar + 2 :].split() + + ret = {} + ret['name'] = name + ret['status'] = fields[0] + ret['ppid'] = fields[1] + ret['ttynr'] = fields[4] + ret['utime'] = fields[11] + ret['stime'] = fields[12] + ret['children_utime'] = fields[13] + ret['children_stime'] = fields[14] + ret['create_time'] = fields[19] + ret['cpu_num'] = fields[36] + ret['blkio_ticks'] = fields[39] # aka 'delayacct_blkio_ticks' + + return ret + @wrap_exceptions @memoize_when_activated def _read_status_file(self): """Read /proc/{pid}/stat file and return its content. @@ -1514,47 +1807,41 @@ def _read_status_file(self): with open_binary("%s/%s/status" % (self._procfs_path, self.pid)) as f: return f.read() + @wrap_exceptions @memoize_when_activated def _read_smaps_file(self): - with open_binary("%s/%s/smaps" % (self._procfs_path, self.pid), - buffering=BIGFILE_BUFFERING) as f: + with open_binary("%s/%s/smaps" % (self._procfs_path, self.pid)) as f: return f.read().strip() def oneshot_enter(self): - self._parse_stat_file.cache_activate() - self._read_status_file.cache_activate() - self._read_smaps_file.cache_activate() + self._parse_stat_file.cache_activate(self) + self._read_status_file.cache_activate(self) + self._read_smaps_file.cache_activate(self) def oneshot_exit(self): - self._parse_stat_file.cache_deactivate() - self._read_status_file.cache_deactivate() - self._read_smaps_file.cache_deactivate() + self._parse_stat_file.cache_deactivate(self) + self._read_status_file.cache_deactivate(self) + self._read_smaps_file.cache_deactivate(self) @wrap_exceptions def name(self): - name = self._parse_stat_file()[0] + name = self._parse_stat_file()['name'] if PY3: name = decode(name) # XXX - gets changed later and probably needs refactoring return name + @wrap_exceptions def exe(self): try: return readlink("%s/%s/exe" % (self._procfs_path, self.pid)) - except OSError as err: - if err.errno in (errno.ENOENT, errno.ESRCH): - # no such file error; might be raised also if the - # path actually exists for system processes with - # low pids (about 0-20) - if os.path.lexists("%s/%s" % (self._procfs_path, self.pid)): - return "" - else: - if not pid_exists(self.pid): - raise NoSuchProcess(self.pid, self._name) - else: - raise ZombieProcess(self.pid, self._name, self._ppid) - if err.errno in (errno.EPERM, errno.EACCES): - raise AccessDenied(self.pid, self._name) + except (FileNotFoundError, ProcessLookupError): + self._raise_if_zombie() + # no such file error; might be raised also if the + # path actually exists for system processes with + # low pids (about 0-20) + if os.path.lexists("%s/%s" % (self._procfs_path, self.pid)): + return "" raise @wrap_exceptions @@ -1563,6 +1850,7 @@ def cmdline(self): data = f.read() if not data: # may happen in case of zombie process + self._raise_if_zombie() return [] # 'man proc' states that args are separated by null bytes '\0' # and last char is supposed to be a null byte. Nevertheless @@ -1574,7 +1862,13 @@ def cmdline(self): sep = '\x00' if data.endswith('\x00') else ' ' if data.endswith(sep): data = data[:-1] - return [x for x in data.split(sep)] + cmdline = data.split(sep) + # Sometimes last char is a null byte '\0' but the args are + # separated by spaces, see: https://github.com/giampaolo/psutil/ + # issues/1179#issuecomment-552984549 + if sep == '\x00' and len(cmdline) == 1 and ' ' in data: + cmdline = data.split(' ') + return cmdline @wrap_exceptions def environ(self): @@ -1584,14 +1878,16 @@ def environ(self): @wrap_exceptions def terminal(self): - tty_nr = int(self._parse_stat_file()[5]) + tty_nr = int(self._parse_stat_file()['ttynr']) tmap = _psposix.get_terminal_map() try: return tmap[tty_nr] except KeyError: return None + # May not be available on old kernels. if os.path.exists('/proc/%s/io' % os.getpid()): + @wrap_exceptions def io_counters(self): fname = "%s/%s/io" % (self._procfs_path, self.pid) @@ -1601,36 +1897,44 @@ def io_counters(self): # https://github.com/giampaolo/psutil/issues/1004 line = line.strip() if line: - name, value = line.split(b': ') - fields[name] = int(value) + try: + name, value = line.split(b': ') + except ValueError: + # https://github.com/giampaolo/psutil/issues/1004 + continue + else: + fields[name] = int(value) if not fields: raise RuntimeError("%s file was empty" % fname) - return pio( - fields[b'syscr'], # read syscalls - fields[b'syscw'], # write syscalls - fields[b'read_bytes'], # read bytes - fields[b'write_bytes'], # write bytes - fields[b'rchar'], # read chars - fields[b'wchar'], # write chars - ) - else: - def io_counters(self): - raise NotImplementedError("couldn't find /proc/%s/io (kernel " - "too old?)" % self.pid) + try: + return pio( + fields[b'syscr'], # read syscalls + fields[b'syscw'], # write syscalls + fields[b'read_bytes'], # read bytes + fields[b'write_bytes'], # write bytes + fields[b'rchar'], # read chars + fields[b'wchar'], # write chars + ) + except KeyError as err: + raise ValueError( + "%r field was not found in %s; found fields are %r" + % (err.args[0], fname, fields) + ) @wrap_exceptions def cpu_times(self): values = self._parse_stat_file() - utime = float(values[12]) / CLOCK_TICKS - stime = float(values[13]) / CLOCK_TICKS - children_utime = float(values[14]) / CLOCK_TICKS - children_stime = float(values[15]) / CLOCK_TICKS - return _common.pcputimes(utime, stime, children_utime, children_stime) + utime = float(values['utime']) / CLOCK_TICKS + stime = float(values['stime']) / CLOCK_TICKS + children_utime = float(values['children_utime']) / CLOCK_TICKS + children_stime = float(values['children_stime']) / CLOCK_TICKS + iowait = float(values['blkio_ticks']) / CLOCK_TICKS + return pcputimes(utime, stime, children_utime, children_stime, iowait) @wrap_exceptions def cpu_num(self): """What CPU the process is on.""" - return int(self._parse_stat_file()[37]) + return int(self._parse_stat_file()['cpu_num']) @wrap_exceptions def wait(self, timeout=None): @@ -1638,14 +1942,14 @@ def wait(self, timeout=None): @wrap_exceptions def create_time(self): - values = self._parse_stat_file() + ctime = float(self._parse_stat_file()['create_time']) # According to documentation, starttime is in field 21 and the # unit is jiffies (clock ticks). # We first divide it for clock ticks and then add uptime returning - # seconds since the epoch, in UTC. + # seconds since the epoch. # Also use cached value if available. bt = BOOT_TIME or boot_time() - return (float(values[20]) / CLOCK_TICKS) + bt + return (ctime / CLOCK_TICKS) + bt @wrap_exceptions def memory_info(self): @@ -1661,22 +1965,47 @@ def memory_info(self): # | dirty | dirty pages (unused in Linux 2.6) | dt | | # ============================================================ with open_binary("%s/%s/statm" % (self._procfs_path, self.pid)) as f: - vms, rss, shared, text, lib, data, dirty = \ - [int(x) * PAGESIZE for x in f.readline().split()[:7]] + vms, rss, shared, text, lib, data, dirty = ( + int(x) * PAGESIZE for x in f.readline().split()[:7] + ) return pmem(rss, vms, shared, text, lib, data, dirty) - # /proc/pid/smaps does not exist on kernels < 2.6.14 or if - # CONFIG_MMU kernel configuration option is not enabled. - if HAS_SMAPS: + if HAS_PROC_SMAPS_ROLLUP or HAS_PROC_SMAPS: + + def _parse_smaps_rollup(self): + # /proc/pid/smaps_rollup was added to Linux in 2017. Faster + # than /proc/pid/smaps. It reports higher PSS than */smaps + # (from 1k up to 200k higher; tested against all processes). + # IMPORTANT: /proc/pid/smaps_rollup is weird, because it + # raises ESRCH / ENOENT for many PIDs, even if they're alive + # (also as root). In that case we'll use /proc/pid/smaps as + # fallback, which is slower but has a +50% success rate + # compared to /proc/pid/smaps_rollup. + uss = pss = swap = 0 + with open_binary( + "{}/{}/smaps_rollup".format(self._procfs_path, self.pid) + ) as f: + for line in f: + if line.startswith(b"Private_"): + # Private_Clean, Private_Dirty, Private_Hugetlb + uss += int(line.split()[1]) * 1024 + elif line.startswith(b"Pss:"): + pss = int(line.split()[1]) * 1024 + elif line.startswith(b"Swap:"): + swap = int(line.split()[1]) * 1024 + return (uss, pss, swap) @wrap_exceptions - def memory_full_info( - self, - # Gets Private_Clean, Private_Dirty, Private_Hugetlb. - _private_re=re.compile(br"\nPrivate.*:\s+(\d+)"), - _pss_re=re.compile(br"\nPss\:\s+(\d+)"), - _swap_re=re.compile(br"\nSwap\:\s+(\d+)")): - basic_mem = self.memory_info() + def _parse_smaps( + self, + # Gets Private_Clean, Private_Dirty, Private_Hugetlb. + _private_re=re.compile(br"\nPrivate.*:\s+(\d+)"), + _pss_re=re.compile(br"\nPss\:\s+(\d+)"), + _swap_re=re.compile(br"\nSwap\:\s+(\d+)"), + ): + # /proc/pid/smaps does not exist on kernels < 2.6.14 or if + # CONFIG_MMU kernel configuration option is not enabled. + # Note: using 3 regexes is faster than reading the file # line by line. # XXX: on Python 3 the 2 regexes are 30% slower than on @@ -1695,19 +2024,35 @@ def memory_full_info( uss = sum(map(int, _private_re.findall(smaps_data))) * 1024 pss = sum(map(int, _pss_re.findall(smaps_data))) * 1024 swap = sum(map(int, _swap_re.findall(smaps_data))) * 1024 + return (uss, pss, swap) + + @wrap_exceptions + def memory_full_info(self): + if HAS_PROC_SMAPS_ROLLUP: # faster + try: + uss, pss, swap = self._parse_smaps_rollup() + except (ProcessLookupError, FileNotFoundError): + uss, pss, swap = self._parse_smaps() + else: + uss, pss, swap = self._parse_smaps() + basic_mem = self.memory_info() return pfullmem(*basic_mem + (uss, pss, swap)) else: memory_full_info = memory_info - if HAS_SMAPS: + if HAS_PROC_SMAPS: @wrap_exceptions def memory_maps(self): """Return process's mapped memory regions as a list of named tuples. Fields are explained in 'man proc'; here is an updated - (Apr 2012) version: http://goo.gl/fmebo + (Apr 2012) version: http://goo.gl/fmebo. + + /proc/{PID}/smaps does not exist on kernels < 2.6.14 or if + CONFIG_MMU kernel configuration option is not enabled. """ + def get_blocks(lines, current_block): data = {} for line in lines: @@ -1724,13 +2069,17 @@ def get_blocks(lines, current_block): # see issue #369 continue else: - raise ValueError("don't know how to inte" - "rpret line %r" % line) + raise ValueError( + "don't know how to interpret line %r" + % line + ) yield (current_block.pop(), data) data = self._read_smaps_file() - # Note: smaps file can be empty for certain processes. + # Note: smaps file can be empty for certain processes or for + # zombies. if not data: + self._raise_if_zombie() return [] lines = data.split(b'\n') ls = [] @@ -1741,20 +2090,22 @@ def get_blocks(lines, current_block): try: addr, perms, offset, dev, inode, path = hfields except ValueError: - addr, perms, offset, dev, inode, path = \ - hfields + [''] + addr, perms, offset, dev, inode, path = hfields + [''] if not path: path = '[anon]' else: if PY3: path = decode(path) path = path.strip() - if (path.endswith(' (deleted)') and not - path_exists_strict(path)): + if path.endswith(' (deleted)') and not path_exists_strict( + path + ): path = path[:-10] ls.append(( - decode(addr), decode(perms), path, - data[b'Rss:'], + decode(addr), + decode(perms), + path, + data.get(b'Rss:', 0), data.get(b'Size:', 0), data.get(b'Pss:', 0), data.get(b'Shared_Clean:', 0), @@ -1763,41 +2114,26 @@ def get_blocks(lines, current_block): data.get(b'Private_Dirty:', 0), data.get(b'Referenced:', 0), data.get(b'Anonymous:', 0), - data.get(b'Swap:', 0) + data.get(b'Swap:', 0), )) return ls - else: # pragma: no cover - def memory_maps(self): - raise NotImplementedError( - "/proc/%s/smaps does not exist on kernels < 2.6.14 or " - "if CONFIG_MMU kernel configuration option is not " - "enabled." % self.pid) - @wrap_exceptions def cwd(self): - try: - return readlink("%s/%s/cwd" % (self._procfs_path, self.pid)) - except OSError as err: - # https://github.com/giampaolo/psutil/issues/986 - if err.errno in (errno.ENOENT, errno.ESRCH): - if not pid_exists(self.pid): - raise NoSuchProcess(self.pid, self._name) - else: - raise ZombieProcess(self.pid, self._name, self._ppid) - raise + return readlink("%s/%s/cwd" % (self._procfs_path, self.pid)) @wrap_exceptions - def num_ctx_switches(self, - _ctxsw_re=re.compile(br'ctxt_switches:\t(\d+)')): + def num_ctx_switches( + self, _ctxsw_re=re.compile(br'ctxt_switches:\t(\d+)') + ): data = self._read_status_file() ctxsw = _ctxsw_re.findall(data) if not ctxsw: raise NotImplementedError( "'voluntary_ctxt_switches' and 'nonvoluntary_ctxt_switches'" "lines were not found in %s/%s/status; the kernel is " - "probably older than 2.6.23" % ( - self._procfs_path, self.pid)) + "probably older than 2.6.23" % (self._procfs_path, self.pid) + ) else: return _common.pctxsw(int(ctxsw[0]), int(ctxsw[1])) @@ -1817,27 +2153,27 @@ def threads(self): hit_enoent = False for thread_id in thread_ids: fname = "%s/%s/task/%s/stat" % ( - self._procfs_path, self.pid, thread_id) + self._procfs_path, + self.pid, + thread_id, + ) try: with open_binary(fname) as f: st = f.read().strip() - except IOError as err: - if err.errno == errno.ENOENT: - # no such file or directory; it means thread - # disappeared on us - hit_enoent = True - continue - raise + except (FileNotFoundError, ProcessLookupError): + # no such file or directory or no such process; + # it means thread disappeared on us + hit_enoent = True + continue # ignore the first two values ("pid (exe)") - st = st[st.find(b')') + 2:] + st = st[st.find(b')') + 2 :] values = st.split(b' ') utime = float(values[11]) / CLOCK_TICKS stime = float(values[12]) / CLOCK_TICKS ntuple = _common.pthread(int(thread_id), utime, stime) retlist.append(ntuple) if hit_enoent: - # raise NSP if the process disappeared on us - os.stat('%s/%s' % (self._procfs_path, self.pid)) + self._raise_if_not_alive() return retlist @wrap_exceptions @@ -1853,41 +2189,47 @@ def nice_get(self): def nice_set(self, value): return cext_posix.setpriority(self.pid, value) - @wrap_exceptions - def cpu_affinity_get(self): - return cext.proc_cpu_affinity_get(self.pid) + # starting from CentOS 6. + if HAS_CPU_AFFINITY: - def _get_eligible_cpus( - self, _re=re.compile(br"Cpus_allowed_list:\t(\d+)-(\d+)")): - # See: https://github.com/giampaolo/psutil/issues/956 - data = self._read_status_file() - match = _re.findall(data) - if match: - return list(range(int(match[0][0]), int(match[0][1]) + 1)) - else: - return list(range(len(per_cpu_times()))) + @wrap_exceptions + def cpu_affinity_get(self): + return cext.proc_cpu_affinity_get(self.pid) + + def _get_eligible_cpus( + self, _re=re.compile(br"Cpus_allowed_list:\t(\d+)-(\d+)") + ): + # See: https://github.com/giampaolo/psutil/issues/956 + data = self._read_status_file() + match = _re.findall(data) + if match: + return list(range(int(match[0][0]), int(match[0][1]) + 1)) + else: + return list(range(len(per_cpu_times()))) - @wrap_exceptions - def cpu_affinity_set(self, cpus): - try: - cext.proc_cpu_affinity_set(self.pid, cpus) - except (OSError, ValueError) as err: - if isinstance(err, ValueError) or err.errno == errno.EINVAL: - eligible_cpus = self._get_eligible_cpus() - all_cpus = tuple(range(len(per_cpu_times()))) - for cpu in cpus: - if cpu not in all_cpus: - raise ValueError( - "invalid CPU number %r; choose between %s" % ( - cpu, eligible_cpus)) - if cpu not in eligible_cpus: - raise ValueError( - "CPU number %r is not eligible; choose " - "between %s" % (cpu, eligible_cpus)) - raise + @wrap_exceptions + def cpu_affinity_set(self, cpus): + try: + cext.proc_cpu_affinity_set(self.pid, cpus) + except (OSError, ValueError) as err: + if isinstance(err, ValueError) or err.errno == errno.EINVAL: + eligible_cpus = self._get_eligible_cpus() + all_cpus = tuple(range(len(per_cpu_times()))) + for cpu in cpus: + if cpu not in all_cpus: + raise ValueError( + "invalid CPU number %r; choose between %s" + % (cpu, eligible_cpus) + ) + if cpu not in eligible_cpus: + raise ValueError( + "CPU number %r is not eligible; choose " + "between %s" % (cpu, eligible_cpus) + ) + raise # only starting from kernel 2.6.13 - if hasattr(cext, "proc_ioprio_get"): + if HAS_PROC_IO_PRIORITY: @wrap_exceptions def ionice_get(self): @@ -1898,68 +2240,48 @@ def ionice_get(self): @wrap_exceptions def ionice_set(self, ioclass, value): - if value is not None: - if not PY3 and not isinstance(value, (int, long)): - msg = "value argument is not an integer (gor %r)" % value - raise TypeError(msg) - if not 0 <= value <= 7: - raise ValueError( - "value argument range expected is between 0 and 7") - - if ioclass in (IOPRIO_CLASS_NONE, None): - if value: - msg = "can't specify value with IOPRIO_CLASS_NONE " \ - "(got %r)" % value - raise ValueError(msg) - ioclass = IOPRIO_CLASS_NONE - value = 0 - elif ioclass == IOPRIO_CLASS_IDLE: - if value: - msg = "can't specify value with IOPRIO_CLASS_IDLE " \ - "(got %r)" % value - raise ValueError(msg) + if value is None: value = 0 - elif ioclass in (IOPRIO_CLASS_RT, IOPRIO_CLASS_BE): - if value is None: - # TODO: add comment explaining why this is 4 (?) - value = 4 - else: - # otherwise we would get OSError(EVINAL) - raise ValueError("invalid ioclass argument %r" % ioclass) - + if value and ioclass in (IOPRIO_CLASS_IDLE, IOPRIO_CLASS_NONE): + raise ValueError("%r ioclass accepts no value" % ioclass) + if value < 0 or value > 7: + msg = "value not in 0-7 range" + raise ValueError(msg) return cext.proc_ioprio_set(self.pid, ioclass, value) - if HAS_PRLIMIT: + if prlimit is not None: + @wrap_exceptions - def rlimit(self, resource, limits=None): + def rlimit(self, resource_, limits=None): # If pid is 0 prlimit() applies to the calling process and # we don't want that. We should never get here though as # PID 0 is not supported on Linux. if self.pid == 0: - raise ValueError("can't use prlimit() against PID 0 process") + msg = "can't use prlimit() against PID 0 process" + raise ValueError(msg) try: if limits is None: # get - return cext.linux_prlimit(self.pid, resource) + return prlimit(self.pid, resource_) else: # set if len(limits) != 2: - raise ValueError( - "second argument must be a (soft, hard) tuple, " - "got %s" % repr(limits)) - soft, hard = limits - cext.linux_prlimit(self.pid, resource, soft, hard) + msg = ( + "second argument must be a (soft, hard) " + + "tuple, got %s" % repr(limits) + ) + raise ValueError(msg) + prlimit(self.pid, resource_, limits) except OSError as err: - if err.errno == errno.ENOSYS and pid_exists(self.pid): + if err.errno == errno.ENOSYS: # I saw this happening on Travis: # https://travis-ci.org/giampaolo/psutil/jobs/51368273 - raise ZombieProcess(self.pid, self._name, self._ppid) - else: - raise + self._raise_if_zombie() + raise @wrap_exceptions def status(self): - letter = self._parse_stat_file()[1] + letter = self._parse_stat_file()['status'] if PY3: letter = letter.decode() # XXX is '?' legit? (we're not supposed to return it anyway) @@ -1974,16 +2296,19 @@ def open_files(self): file = "%s/%s/fd/%s" % (self._procfs_path, self.pid, fd) try: path = readlink(file) - except OSError as err: + except (FileNotFoundError, ProcessLookupError): # ENOENT == file which is gone in the meantime - if err.errno in (errno.ENOENT, errno.ESRCH): - hit_enoent = True - continue - elif err.errno == errno.EINVAL: + hit_enoent = True + continue + except OSError as err: + if err.errno == errno.EINVAL: # not a link continue - else: - raise + if err.errno == errno.ENAMETOOLONG: + # file name too long + debug(err) + continue + raise else: # If path is not an absolute there's no way to tell # whether it's a regular file or not, so we skip it. @@ -1992,34 +2317,32 @@ def open_files(self): if path.startswith('/') and isfile_strict(path): # Get file position and flags. file = "%s/%s/fdinfo/%s" % ( - self._procfs_path, self.pid, fd) + self._procfs_path, + self.pid, + fd, + ) try: with open_binary(file) as f: pos = int(f.readline().split()[1]) flags = int(f.readline().split()[1], 8) - except IOError as err: - if err.errno == errno.ENOENT: - # fd gone in the meantime; does not - # necessarily mean the process disappeared - # on us. - hit_enoent = True - else: - raise + except (FileNotFoundError, ProcessLookupError): + # fd gone in the meantime; process may + # still be alive + hit_enoent = True else: mode = file_flags_to_mode(flags) ntuple = popenfile( - path, int(fd), int(pos), mode, flags) + path, int(fd), int(pos), mode, flags + ) retlist.append(ntuple) if hit_enoent: - # raise NSP if the process disappeared on us - os.stat('%s/%s' % (self._procfs_path, self.pid)) + self._raise_if_not_alive() return retlist @wrap_exceptions def connections(self, kind='inet'): ret = _connections.retrieve(kind, self.pid) - # raise NSP if the process disappeared on us - os.stat('%s/%s' % (self._procfs_path, self.pid)) + self._raise_if_not_alive() return ret @wrap_exceptions @@ -2028,7 +2351,7 @@ def num_fds(self): @wrap_exceptions def ppid(self): - return int(self._parse_stat_file()[2]) + return int(self._parse_stat_file()['ppid']) @wrap_exceptions def uids(self, _uids_re=re.compile(br'Uid:\t(\d+)\t(\d+)\t(\d+)')): diff --git a/addon/globalPlugins/soundmanager/psutil/_pslinux.pyc b/addon/globalPlugins/soundmanager/psutil/_pslinux.pyc deleted file mode 100644 index 386f6ad..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/_pslinux.pyc and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/_psosx.py b/addon/globalPlugins/soundmanager/psutil/_psosx.py index fbfedf3..673ac0d 100644 --- a/addon/globalPlugins/soundmanager/psutil/_psosx.py +++ b/addon/globalPlugins/soundmanager/psutil/_psosx.py @@ -4,28 +4,26 @@ """macOS platform implementation.""" -import contextlib import errno import functools import os -from socket import AF_INET from collections import namedtuple from . import _common from . import _psposix from . import _psutil_osx as cext from . import _psutil_posix as cext_posix -from ._common import AF_INET6 +from ._common import AccessDenied +from ._common import NoSuchProcess +from ._common import ZombieProcess from ._common import conn_tmap +from ._common import conn_to_ntuple from ._common import isfile_strict from ._common import memoize_when_activated from ._common import parse_environ_block -from ._common import sockfam_to_enum -from ._common import socktype_to_enum from ._common import usage_percent -from ._exceptions import AccessDenied -from ._exceptions import NoSuchProcess -from ._exceptions import ZombieProcess +from ._compat import PermissionError +from ._compat import ProcessLookupError __extra__all__ = [] @@ -36,7 +34,7 @@ # ===================================================================== -PAGESIZE = os.sysconf("SC_PAGE_SIZE") +PAGESIZE = cext_posix.getpagesize() AF_LINK = cext_posix.AF_LINK TCP_STATUSES = { @@ -93,6 +91,7 @@ # ===================================================================== +# fmt: off # psutil.cpu_times() scputimes = namedtuple('scputimes', ['user', 'nice', 'system', 'idle']) # psutil.virtual_memory() @@ -103,13 +102,7 @@ pmem = namedtuple('pmem', ['rss', 'vms', 'pfaults', 'pageins']) # psutil.Process.memory_full_info() pfullmem = namedtuple('pfullmem', pmem._fields + ('uss', )) -# psutil.Process.memory_maps(grouped=True) -pmmap_grouped = namedtuple( - 'pmmap_grouped', - 'path rss private swapped dirtied ref_count shadow_depth') -# psutil.Process.memory_maps(grouped=False) -pmmap_ext = namedtuple( - 'pmmap_ext', 'addr perms ' + ' '.join(pmmap_grouped._fields)) +# fmt: on # ===================================================================== @@ -119,12 +112,18 @@ def virtual_memory(): """System virtual memory as a namedtuple.""" - total, active, inactive, wired, free = cext.virtual_mem() + total, active, inactive, wired, free, speculative = cext.virtual_mem() + # This is how Zabbix calculate avail and used mem: + # https://github.com/zabbix/zabbix/blob/trunk/src/libs/zbxsysinfo/ + # osx/memory.c + # Also see: https://github.com/giampaolo/psutil/issues/1277 avail = inactive + free - used = active + inactive + wired + used = active + wired + # This is NOT how Zabbix calculates free mem but it matches "free" + # cmdline utility. + free -= speculative percent = usage_percent((total - avail), total, round_=1) - return svmem(total, avail, percent, used, free, - active, inactive, wired) + return svmem(total, avail, percent, used, free, active, inactive, wired) def swap_memory(): @@ -146,7 +145,7 @@ def cpu_times(): def per_cpu_times(): - """Return system CPU times as a named tuple""" + """Return system CPU times as a named tuple.""" ret = [] for cpu_t in cext.per_cpu_times(): user, nice, system, idle = cpu_t @@ -160,23 +159,25 @@ def cpu_count_logical(): return cext.cpu_count_logical() -def cpu_count_physical(): - """Return the number of physical CPUs in the system.""" - return cext.cpu_count_phys() +def cpu_count_cores(): + """Return the number of CPU cores in the system.""" + return cext.cpu_count_cores() def cpu_stats(): - ctx_switches, interrupts, soft_interrupts, syscalls, traps = \ + ctx_switches, interrupts, soft_interrupts, syscalls, traps = ( cext.cpu_stats() + ) return _common.scpustats( - ctx_switches, interrupts, soft_interrupts, syscalls) + ctx_switches, interrupts, soft_interrupts, syscalls + ) def cpu_freq(): """Return CPU frequency. On macOS per-cpu frequency is not supported. Also, the returned frequency never changes, see: - https://arstechnica.com/civis/viewtopic.php?f=19&t=465002 + https://arstechnica.com/civis/viewtopic.php?f=19&t=465002. """ curr, min_, max_ = cext.cpu_freq() return [_common.scpufreq(curr, min_, max_)] @@ -202,7 +203,10 @@ def disk_partitions(all=False): if not all: if not os.path.isabs(device) or not os.path.exists(device): continue - ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) + maxfile = maxpath = None # set later + ntuple = _common.sdiskpart( + device, mountpoint, fstype, opts, maxfile, maxpath + ) retlist.append(ntuple) return retlist @@ -263,7 +267,7 @@ def net_if_stats(): for name in names: try: mtu = cext_posix.net_if_mtu(name) - isup = cext_posix.net_if_flags(name) + flags = cext_posix.net_if_flags(name) duplex, speed = cext_posix.net_if_duplex_speed(name) except OSError as err: # https://github.com/giampaolo/psutil/issues/1279 @@ -272,7 +276,11 @@ def net_if_stats(): else: if hasattr(_common, 'NicDuplex'): duplex = _common.NicDuplex(duplex) - ret[name] = _common.snicstats(isup, duplex, speed, mtu) + output_flags = ','.join(flags) + isup = 'running' in flags + ret[name] = _common.snicstats( + isup, duplex, speed, mtu, output_flags + ) return ret @@ -325,61 +333,45 @@ def pids(): pid_exists = _psposix.pid_exists +def is_zombie(pid): + try: + st = cext.proc_kinfo_oneshot(pid)[kinfo_proc_map['status']] + return st == cext.SZOMB + except OSError: + return False + + def wrap_exceptions(fun): """Decorator which translates bare OSError exceptions into NoSuchProcess and AccessDenied. """ + @functools.wraps(fun) def wrapper(self, *args, **kwargs): try: return fun(self, *args, **kwargs) - except OSError as err: - if err.errno == errno.ESRCH: + except ProcessLookupError: + if is_zombie(self.pid): + raise ZombieProcess(self.pid, self._name, self._ppid) + else: raise NoSuchProcess(self.pid, self._name) - if err.errno in (errno.EPERM, errno.EACCES): - raise AccessDenied(self.pid, self._name) - raise - except cext.ZombieProcessError: - raise ZombieProcess(self.pid, self._name, self._ppid) - return wrapper - + except PermissionError: + raise AccessDenied(self.pid, self._name) -@contextlib.contextmanager -def catch_zombie(proc): - """There are some poor C APIs which incorrectly raise ESRCH when - the process is still alive or it's a zombie, or even RuntimeError - (those who don't set errno). This is here in order to solve: - https://github.com/giampaolo/psutil/issues/1044 - """ - try: - yield - except (OSError, RuntimeError) as err: - if isinstance(err, RuntimeError) or err.errno == errno.ESRCH: - try: - # status() is not supposed to lie and correctly detect - # zombies so if it raises ESRCH it's true. - status = proc.status() - except NoSuchProcess: - raise err - else: - if status == _common.STATUS_ZOMBIE: - raise ZombieProcess(proc.pid, proc._name, proc._ppid) - else: - raise AccessDenied(proc.pid, proc._name) - else: - raise + return wrapper -class Process(object): +class Process: """Wrapper class around underlying C implementation.""" - __slots__ = ["pid", "_name", "_ppid"] + __slots__ = ["pid", "_name", "_ppid", "_cache"] def __init__(self, pid): self.pid = pid self._name = None self._ppid = None + @wrap_exceptions @memoize_when_activated def _get_kinfo_proc(self): # Note: should work with all PIDs without permission issues. @@ -387,21 +379,21 @@ def _get_kinfo_proc(self): assert len(ret) == len(kinfo_proc_map) return ret + @wrap_exceptions @memoize_when_activated def _get_pidtaskinfo(self): # Note: should work for PIDs owned by user only. - with catch_zombie(self): - ret = cext.proc_pidtaskinfo_oneshot(self.pid) + ret = cext.proc_pidtaskinfo_oneshot(self.pid) assert len(ret) == len(pidtaskinfo_map) return ret def oneshot_enter(self): - self._get_kinfo_proc.cache_activate() - self._get_pidtaskinfo.cache_activate() + self._get_kinfo_proc.cache_activate(self) + self._get_pidtaskinfo.cache_activate(self) def oneshot_exit(self): - self._get_kinfo_proc.cache_deactivate() - self._get_pidtaskinfo.cache_deactivate() + self._get_kinfo_proc.cache_deactivate(self) + self._get_pidtaskinfo.cache_deactivate(self) @wrap_exceptions def name(self): @@ -410,18 +402,15 @@ def name(self): @wrap_exceptions def exe(self): - with catch_zombie(self): - return cext.proc_exe(self.pid) + return cext.proc_exe(self.pid) @wrap_exceptions def cmdline(self): - with catch_zombie(self): - return cext.proc_cmdline(self.pid) + return cext.proc_cmdline(self.pid) @wrap_exceptions def environ(self): - with catch_zombie(self): - return parse_environ_block(cext.proc_environ(self.pid)) + return parse_environ_block(cext.proc_environ(self.pid)) @wrap_exceptions def ppid(self): @@ -430,8 +419,7 @@ def ppid(self): @wrap_exceptions def cwd(self): - with catch_zombie(self): - return cext.proc_cwd(self.pid) + return cext.proc_cwd(self.pid) @wrap_exceptions def uids(self): @@ -439,7 +427,8 @@ def uids(self): return _common.puids( rawtuple[kinfo_proc_map['ruid']], rawtuple[kinfo_proc_map['euid']], - rawtuple[kinfo_proc_map['suid']]) + rawtuple[kinfo_proc_map['suid']], + ) @wrap_exceptions def gids(self): @@ -447,7 +436,8 @@ def gids(self): return _common.puids( rawtuple[kinfo_proc_map['rgid']], rawtuple[kinfo_proc_map['egid']], - rawtuple[kinfo_proc_map['sgid']]) + rawtuple[kinfo_proc_map['sgid']], + ) @wrap_exceptions def terminal(self): @@ -472,7 +462,7 @@ def memory_info(self): def memory_full_info(self): basic_mem = self.memory_info() uss = cext.proc_memory_uss(self.pid) - return pfullmem(*basic_mem + (uss, )) + return pfullmem(*basic_mem + (uss,)) @wrap_exceptions def cpu_times(self): @@ -481,7 +471,9 @@ def cpu_times(self): rawtuple[pidtaskinfo_map['cpuutime']], rawtuple[pidtaskinfo_map['cpustime']], # children user / system times are not retrievable (set to 0) - 0.0, 0.0) + 0.0, + 0.0, + ) @wrap_exceptions def create_time(self): @@ -504,8 +496,7 @@ def open_files(self): if self.pid == 0: return [] files = [] - with catch_zombie(self): - rawlist = cext.proc_open_files(self.pid) + rawlist = cext.proc_open_files(self.pid) for path, fd in rawlist: if isfile_strict(path): ntuple = _common.popenfile(path, fd) @@ -515,23 +506,18 @@ def open_files(self): @wrap_exceptions def connections(self, kind='inet'): if kind not in conn_tmap: - raise ValueError("invalid %r kind argument; choose between %s" - % (kind, ', '.join([repr(x) for x in conn_tmap]))) + raise ValueError( + "invalid %r kind argument; choose between %s" + % (kind, ', '.join([repr(x) for x in conn_tmap])) + ) families, types = conn_tmap[kind] - with catch_zombie(self): - rawlist = cext.proc_connections(self.pid, families, types) + rawlist = cext.proc_connections(self.pid, families, types) ret = [] for item in rawlist: fd, fam, type, laddr, raddr, status = item - status = TCP_STATUSES[status] - fam = sockfam_to_enum(fam) - type = socktype_to_enum(type) - if fam in (AF_INET, AF_INET6): - if laddr: - laddr = _common.addr(*laddr) - if raddr: - raddr = _common.addr(*raddr) - nt = _common.pconn(fd, fam, type, laddr, raddr, status) + nt = conn_to_ntuple( + fd, fam, type, laddr, raddr, status, TCP_STATUSES + ) ret.append(nt) return ret @@ -539,8 +525,7 @@ def connections(self, kind='inet'): def num_fds(self): if self.pid == 0: return 0 - with catch_zombie(self): - return cext.proc_num_fds(self.pid) + return cext.proc_num_fds(self.pid) @wrap_exceptions def wait(self, timeout=None): @@ -548,13 +533,11 @@ def wait(self, timeout=None): @wrap_exceptions def nice_get(self): - with catch_zombie(self): - return cext_posix.getpriority(self.pid) + return cext_posix.getpriority(self.pid) @wrap_exceptions def nice_set(self, value): - with catch_zombie(self): - return cext_posix.setpriority(self.pid, value) + return cext_posix.setpriority(self.pid, value) @wrap_exceptions def status(self): @@ -570,7 +553,3 @@ def threads(self): ntuple = _common.pthread(thread_id, utime, stime) retlist.append(ntuple) return retlist - - @wrap_exceptions - def memory_maps(self): - return cext.proc_memory_maps(self.pid) diff --git a/addon/globalPlugins/soundmanager/psutil/_psosx.pyc b/addon/globalPlugins/soundmanager/psutil/_psosx.pyc deleted file mode 100644 index 6062710..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/_psosx.pyc and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/_psposix.py b/addon/globalPlugins/soundmanager/psutil/_psposix.py index 9c3fac2..42bdfa7 100644 --- a/addon/globalPlugins/soundmanager/psutil/_psposix.py +++ b/addon/globalPlugins/soundmanager/psutil/_psposix.py @@ -4,18 +4,34 @@ """Routines common to all posix systems.""" -import errno import glob import os +import signal import sys import time +from ._common import MACOS +from ._common import TimeoutExpired from ._common import memoize from ._common import sdiskusage from ._common import usage_percent from ._compat import PY3 +from ._compat import ChildProcessError +from ._compat import FileNotFoundError +from ._compat import InterruptedError +from ._compat import PermissionError +from ._compat import ProcessLookupError from ._compat import unicode -from ._exceptions import TimeoutExpired + + +if MACOS: + from . import _psutil_osx + + +if PY3: + import enum +else: + enum = None __all__ = ['pid_exists', 'wait_pid', 'disk_usage', 'get_terminal_map'] @@ -32,87 +48,129 @@ def pid_exists(pid): return True try: os.kill(pid, 0) - except OSError as err: - if err.errno == errno.ESRCH: - # ESRCH == No such process - return False - elif err.errno == errno.EPERM: - # EPERM clearly means there's a process to deny access to - return True - else: - # According to "man 2 kill" possible error values are - # (EINVAL, EPERM, ESRCH) therefore we should never get - # here. If we do let's be explicit in considering this - # an error. - raise err + except ProcessLookupError: + return False + except PermissionError: + # EPERM clearly means there's a process to deny access to + return True + # According to "man 2 kill" possible error values are + # (EINVAL, EPERM, ESRCH) else: return True -def wait_pid(pid, timeout=None, proc_name=None): - """Wait for process with pid 'pid' to terminate and return its - exit status code as an integer. +# Python 3.5 signals enum (contributed by me ^^): +# https://bugs.python.org/issue21076 +if enum is not None and hasattr(signal, "Signals"): + Negsignal = enum.IntEnum( + 'Negsignal', dict([(x.name, -x.value) for x in signal.Signals]) + ) + + def negsig_to_enum(num): + """Convert a negative signal value to an enum.""" + try: + return Negsignal(num) + except ValueError: + return num + +else: # pragma: no cover + + def negsig_to_enum(num): + return num + + +def wait_pid( + pid, + timeout=None, + proc_name=None, + _waitpid=os.waitpid, + _timer=getattr(time, 'monotonic', time.time), # noqa: B008 + _min=min, + _sleep=time.sleep, + _pid_exists=pid_exists, +): + """Wait for a process PID to terminate. + + If the process terminated normally by calling exit(3) or _exit(2), + or by returning from main(), the return value is the positive integer + passed to *exit(). - If pid is not a children of os.getpid() (current process) just - waits until the process disappears and return None. + If it was terminated by a signal it returns the negated value of the + signal which caused the termination (e.g. -SIGTERM). - If pid does not exist at all return None immediately. + If PID is not a children of os.getpid() (current process) just + wait until the process disappears and return None. - Raise TimeoutExpired on timeout expired. + If PID does not exist at all return None immediately. + + If *timeout* != None and process is still alive raise TimeoutExpired. + timeout=0 is also possible (either return immediately or raise). """ - def check_timeout(delay): + if pid <= 0: + # see "man waitpid" + msg = "can't wait for PID 0" + raise ValueError(msg) + interval = 0.0001 + flags = 0 + if timeout is not None: + flags |= os.WNOHANG + stop_at = _timer() + timeout + + def sleep(interval): + # Sleep for some time and return a new increased interval. if timeout is not None: - if timer() >= stop_at: + if _timer() >= stop_at: raise TimeoutExpired(timeout, pid=pid, name=proc_name) - time.sleep(delay) - return min(delay * 2, 0.04) - - timer = getattr(time, 'monotonic', time.time) - if timeout is not None: - def waitcall(): - return os.waitpid(pid, os.WNOHANG) - stop_at = timer() + timeout - else: - def waitcall(): - return os.waitpid(pid, 0) + _sleep(interval) + return _min(interval * 2, 0.04) - delay = 0.0001 + # See: https://linux.die.net/man/2/waitpid while True: try: - retpid, status = waitcall() - except OSError as err: - if err.errno == errno.EINTR: - delay = check_timeout(delay) - continue - elif err.errno == errno.ECHILD: - # This has two meanings: - # - pid is not a child of os.getpid() in which case - # we keep polling until it's gone - # - pid never existed in the first place - # In both cases we'll eventually return None as we - # can't determine its exit status code. - while True: - if pid_exists(pid): - delay = check_timeout(delay) - else: - return - else: - raise + retpid, status = os.waitpid(pid, flags) + except InterruptedError: + interval = sleep(interval) + except ChildProcessError: + # This has two meanings: + # - PID is not a child of os.getpid() in which case + # we keep polling until it's gone + # - PID never existed in the first place + # In both cases we'll eventually return None as we + # can't determine its exit status code. + while _pid_exists(pid): + interval = sleep(interval) + return else: if retpid == 0: - # WNOHANG was used, pid is still running - delay = check_timeout(delay) + # WNOHANG flag was used and PID is still running. + interval = sleep(interval) continue - # process exited due to a signal; return the integer of - # that signal - if os.WIFSIGNALED(status): - return -os.WTERMSIG(status) - # process exited using exit(2) system call; return the - # integer exit(2) system call has been called with - elif os.WIFEXITED(status): + + if os.WIFEXITED(status): + # Process terminated normally by calling exit(3) or _exit(2), + # or by returning from main(). The return value is the + # positive integer passed to *exit(). return os.WEXITSTATUS(status) + elif os.WIFSIGNALED(status): + # Process exited due to a signal. Return the negative value + # of that signal. + return negsig_to_enum(-os.WTERMSIG(status)) + # elif os.WIFSTOPPED(status): + # # Process was stopped via SIGSTOP or is being traced, and + # # waitpid() was called with WUNTRACED flag. PID is still + # # alive. From now on waitpid() will keep returning (0, 0) + # # until the process state doesn't change. + # # It may make sense to catch/enable this since stopped PIDs + # # ignore SIGTERM. + # interval = sleep(interval) + # continue + # elif os.WIFCONTINUED(status): + # # Process was resumed via SIGCONT and waitpid() was called + # # with WCONTINUED flag. + # interval = sleep(interval) + # continue else: - # should never happen + # Should never happen. raise ValueError("unknown process exit status %r" % status) @@ -125,7 +183,7 @@ def disk_usage(path): """ if PY3: st = os.statvfs(path) - else: + else: # pragma: no cover # os.statvfs() does not support unicode on Python 2: # - https://github.com/giampaolo/psutil/issues/416 # - http://bugs.python.org/issue18695 @@ -143,13 +201,16 @@ def disk_usage(path): # Total space which is only available to root (unless changed # at system level). - total = (st.f_blocks * st.f_frsize) + total = st.f_blocks * st.f_frsize # Remaining free space usable by root. - avail_to_root = (st.f_bfree * st.f_frsize) + avail_to_root = st.f_bfree * st.f_frsize # Remaining free space usable by user. - avail_to_user = (st.f_bavail * st.f_frsize) + avail_to_user = st.f_bavail * st.f_frsize # Total space being used in general. - used = (total - avail_to_root) + used = total - avail_to_root + if MACOS: + # see: https://github.com/giampaolo/psutil/pull/2152 + used = _psutil_osx.disk_usage_used(path, used) # Total space which is available to user (same as 'total' but # for the user). total_user = used + avail_to_user @@ -162,13 +223,14 @@ def disk_usage(path): # reserved blocks that we are currently not considering: # https://github.com/giampaolo/psutil/issues/829#issuecomment-223750462 return sdiskusage( - total=total, used=used, free=avail_to_user, percent=usage_percent_user) + total=total, used=used, free=avail_to_user, percent=usage_percent_user + ) @memoize def get_terminal_map(): """Get a map of device-id -> path as a dict. - Used by Process.terminal() + Used by Process.terminal(). """ ret = {} ls = glob.glob('/dev/tty*') + glob.glob('/dev/pts/*') @@ -176,7 +238,6 @@ def get_terminal_map(): assert name not in ret, name try: ret[os.stat(name).st_rdev] = name - except OSError as err: - if err.errno != errno.ENOENT: - raise + except FileNotFoundError: + pass return ret diff --git a/addon/globalPlugins/soundmanager/psutil/_psposix.pyc b/addon/globalPlugins/soundmanager/psutil/_psposix.pyc deleted file mode 100644 index 4c66327..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/_psposix.pyc and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/_pssunos.py b/addon/globalPlugins/soundmanager/psutil/_pssunos.py index e2f33a3..dddbece 100644 --- a/addon/globalPlugins/soundmanager/psutil/_pssunos.py +++ b/addon/globalPlugins/soundmanager/psutil/_pssunos.py @@ -5,6 +5,7 @@ """Sun OS Solaris platform implementation.""" import errno +import functools import os import socket import subprocess @@ -17,16 +18,21 @@ from . import _psutil_posix as cext_posix from . import _psutil_sunos as cext from ._common import AF_INET6 +from ._common import AccessDenied +from ._common import NoSuchProcess +from ._common import ZombieProcess +from ._common import debug +from ._common import get_procfs_path from ._common import isfile_strict from ._common import memoize_when_activated from ._common import sockfam_to_enum from ._common import socktype_to_enum from ._common import usage_percent -from ._compat import b from ._compat import PY3 -from ._exceptions import AccessDenied -from ._exceptions import NoSuchProcess -from ._exceptions import ZombieProcess +from ._compat import FileNotFoundError +from ._compat import PermissionError +from ._compat import ProcessLookupError +from ._compat import b __extra__all__ = ["CONN_IDLE", "CONN_BOUND", "PROCFS_PATH"] @@ -37,7 +43,7 @@ # ===================================================================== -PAGE_SIZE = os.sysconf('SC_PAGE_SIZE') +PAGE_SIZE = cext_posix.getpagesize() AF_LINK = cext_posix.AF_LINK IS_64_BIT = sys.maxsize > 2**32 @@ -83,7 +89,8 @@ uid=8, euid=9, gid=10, - egid=11) + egid=11, +) # ===================================================================== @@ -94,29 +101,22 @@ # psutil.cpu_times() scputimes = namedtuple('scputimes', ['user', 'system', 'idle', 'iowait']) # psutil.cpu_times(percpu=True) -pcputimes = namedtuple('pcputimes', - ['user', 'system', 'children_user', 'children_system']) +pcputimes = namedtuple( + 'pcputimes', ['user', 'system', 'children_user', 'children_system'] +) # psutil.virtual_memory() svmem = namedtuple('svmem', ['total', 'available', 'percent', 'used', 'free']) # psutil.Process.memory_info() pmem = namedtuple('pmem', ['rss', 'vms']) pfullmem = pmem # psutil.Process.memory_maps(grouped=True) -pmmap_grouped = namedtuple('pmmap_grouped', - ['path', 'rss', 'anonymous', 'locked']) +pmmap_grouped = namedtuple( + 'pmmap_grouped', ['path', 'rss', 'anonymous', 'locked'] +) # psutil.Process.memory_maps(grouped=False) pmmap_ext = namedtuple( - 'pmmap_ext', 'addr perms ' + ' '.join(pmmap_grouped._fields)) - - -# ===================================================================== -# --- utils -# ===================================================================== - - -def get_procfs_path(): - """Return updated psutil.PROCFS_PATH constant.""" - return sys.modules['psutil'].PROCFS_PATH + 'pmmap_ext', 'addr perms ' + ' '.join(pmmap_grouped._fields) +) # ===================================================================== @@ -144,10 +144,16 @@ def swap_memory(): # usr/src/cmd/swap/swap.c # ...nevertheless I can't manage to obtain the same numbers as 'swap' # cmdline utility, so let's parse its output (sigh!) - p = subprocess.Popen(['/usr/bin/env', 'PATH=/usr/sbin:/sbin:%s' % - os.environ['PATH'], 'swap', '-l'], - stdout=subprocess.PIPE) - stdout, stderr = p.communicate() + p = subprocess.Popen( + [ + '/usr/bin/env', + 'PATH=/usr/sbin:/sbin:%s' % os.environ['PATH'], + 'swap', + '-l', + ], + stdout=subprocess.PIPE, + ) + stdout, _ = p.communicate() if PY3: stdout = stdout.decode(sys.stdout.encoding) if p.returncode != 0: @@ -155,17 +161,19 @@ def swap_memory(): lines = stdout.strip().split('\n')[1:] if not lines: - raise RuntimeError('no swap device(s) configured') + msg = 'no swap device(s) configured' + raise RuntimeError(msg) total = free = 0 for line in lines: line = line.split() - t, f = line[-2:] + t, f = line[3:5] total += int(int(t) * 512) free += int(int(f) * 512) used = total - free percent = usage_percent(used, total, round_=1) - return _common.sswap(total, used, free, percent, - sin * PAGE_SIZE, sout * PAGE_SIZE) + return _common.sswap( + total, used, free, percent, sin * PAGE_SIZE, sout * PAGE_SIZE + ) # ===================================================================== @@ -174,13 +182,13 @@ def swap_memory(): def cpu_times(): - """Return system-wide CPU times as a named tuple""" + """Return system-wide CPU times as a named tuple.""" ret = cext.per_cpu_times() return scputimes(*[sum(x) for x in zip(*ret)]) def per_cpu_times(): - """Return system per-CPU times as a list of named tuples""" + """Return system per-CPU times as a list of named tuples.""" ret = cext.per_cpu_times() return [scputimes(*x) for x in ret] @@ -194,17 +202,18 @@ def cpu_count_logical(): return None -def cpu_count_physical(): - """Return the number of physical CPUs in the system.""" - return cext.cpu_count_phys() +def cpu_count_cores(): + """Return the number of CPU cores in the system.""" + return cext.cpu_count_cores() def cpu_stats(): """Return various CPU stats as a named tuple.""" ctx_switches, interrupts, syscalls, traps = cext.cpu_stats() soft_interrupts = 0 - return _common.scpustats(ctx_switches, interrupts, soft_interrupts, - syscalls) + return _common.scpustats( + ctx_switches, interrupts, soft_interrupts, syscalls + ) # ===================================================================== @@ -230,9 +239,17 @@ def disk_partitions(all=False): # Differently from, say, Linux, we don't have a list of # common fs types so the best we can do, AFAIK, is to # filter by filesystem having a total size > 0. - if not disk_usage(mountpoint).total: + try: + if not disk_usage(mountpoint).total: + continue + except OSError as err: + # https://github.com/giampaolo/psutil/issues/1674 + debug("skipping %r: %s" % (mountpoint, err)) continue - ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) + maxfile = maxpath = None # set later + ntuple = _common.sdiskpart( + device, mountpoint, fstype, opts, maxfile, maxpath + ) retlist.append(ntuple) return retlist @@ -255,8 +272,10 @@ def net_connections(kind, _pid=-1): if _pid == -1: cmap.pop('unix', 0) if kind not in cmap: - raise ValueError("invalid %r kind argument; choose between %s" - % (kind, ', '.join([repr(x) for x in cmap]))) + raise ValueError( + "invalid %r kind argument; choose between %s" + % (kind, ', '.join([repr(x) for x in cmap])) + ) families, types = _common.conn_tmap[kind] rawlist = cext.net_connections(_pid) ret = set() @@ -266,6 +285,7 @@ def net_connections(kind, _pid=-1): continue if type_ not in types: continue + # TODO: refactor and use _common.conn_to_ntuple. if fam in (AF_INET, AF_INET6): if laddr: laddr = _common.addr(*laddr) @@ -289,7 +309,7 @@ def net_if_stats(): isup, duplex, speed, mtu = items if hasattr(_common, 'NicDuplex'): duplex = _common.NicDuplex(duplex) - ret[name] = _common.snicstats(isup, duplex, speed, mtu) + ret[name] = _common.snicstats(isup, duplex, speed, mtu, '') return ret @@ -342,33 +362,35 @@ def wrap_exceptions(fun): EACCES and EPERM in NoSuchProcess or AccessDenied exceptions. """ + @functools.wraps(fun) def wrapper(self, *args, **kwargs): try: return fun(self, *args, **kwargs) - except EnvironmentError as err: + except (FileNotFoundError, ProcessLookupError): + # ENOENT (no such file or directory) gets raised on open(). + # ESRCH (no such process) can get raised on read() if + # process is gone in meantime. + if not pid_exists(self.pid): + raise NoSuchProcess(self.pid, self._name) + else: + raise ZombieProcess(self.pid, self._name, self._ppid) + except PermissionError: + raise AccessDenied(self.pid, self._name) + except OSError: if self.pid == 0: if 0 in pids(): raise AccessDenied(self.pid, self._name) else: raise - # ENOENT (no such file or directory) gets raised on open(). - # ESRCH (no such process) can get raised on read() if - # process is gone in meantime. - if err.errno in (errno.ENOENT, errno.ESRCH): - if not pid_exists(self.pid): - raise NoSuchProcess(self.pid, self._name) - else: - raise ZombieProcess(self.pid, self._name, self._ppid) - if err.errno in (errno.EPERM, errno.EACCES): - raise AccessDenied(self.pid, self._name) raise + return wrapper -class Process(object): +class Process: """Wrapper class around underlying C implementation.""" - __slots__ = ["pid", "_name", "_ppid", "_procfs_path"] + __slots__ = ["pid", "_name", "_ppid", "_procfs_path", "_cache"] def __init__(self, pid): self.pid = pid @@ -376,32 +398,42 @@ def __init__(self, pid): self._ppid = None self._procfs_path = get_procfs_path() + def _assert_alive(self): + """Raise NSP if the process disappeared on us.""" + # For those C function who do not raise NSP, possibly returning + # incorrect or incomplete result. + os.stat('%s/%s' % (self._procfs_path, self.pid)) + def oneshot_enter(self): - self._proc_name_and_args.cache_activate() - self._proc_basic_info.cache_activate() - self._proc_cred.cache_activate() + self._proc_name_and_args.cache_activate(self) + self._proc_basic_info.cache_activate(self) + self._proc_cred.cache_activate(self) def oneshot_exit(self): - self._proc_name_and_args.cache_deactivate() - self._proc_basic_info.cache_deactivate() - self._proc_cred.cache_deactivate() + self._proc_name_and_args.cache_deactivate(self) + self._proc_basic_info.cache_deactivate(self) + self._proc_cred.cache_deactivate(self) + @wrap_exceptions @memoize_when_activated def _proc_name_and_args(self): return cext.proc_name_and_args(self.pid, self._procfs_path) + @wrap_exceptions @memoize_when_activated def _proc_basic_info(self): + if self.pid == 0 and not os.path.exists( + '%s/%s/psinfo' % (self._procfs_path, self.pid) + ): + raise AccessDenied(self.pid) ret = cext.proc_basic_info(self.pid, self._procfs_path) assert len(ret) == len(proc_info_map) return ret + @wrap_exceptions @memoize_when_activated def _proc_cred(self): - @wrap_exceptions - def proc_cred(self): - return cext.proc_cred(self.pid, self._procfs_path) - return proc_cred(self) + return cext.proc_cred(self.pid, self._procfs_path) @wrap_exceptions def name(self): @@ -412,9 +444,10 @@ def name(self): def exe(self): try: return os.readlink( - "%s/%s/path/a.out" % (self._procfs_path, self.pid)) + "%s/%s/path/a.out" % (self._procfs_path, self.pid) + ) except OSError: - pass # continue and guess the exe name from the cmdline + pass # continue and guess the exe name from the cmdline # Will be guessed later from cmdline but we want to explicitly # invoke cmdline here in order to get an AccessDenied # exception if the user has not enough privileges. @@ -505,21 +538,18 @@ def cpu_num(self): def terminal(self): procfs_path = self._procfs_path hit_enoent = False - tty = wrap_exceptions( - self._proc_basic_info()[proc_info_map['ttynr']]) + tty = wrap_exceptions(self._proc_basic_info()[proc_info_map['ttynr']]) if tty != cext.PRNODEV: for x in (0, 1, 2, 255): try: return os.readlink( - '%s/%d/path/%d' % (procfs_path, self.pid, x)) - except OSError as err: - if err.errno == errno.ENOENT: - hit_enoent = True - continue - raise + '%s/%d/path/%d' % (procfs_path, self.pid, x) + ) + except FileNotFoundError: + hit_enoent = True + continue if hit_enoent: - # raise NSP if the process disappeared on us - os.stat('%s/%s' % (procfs_path, self.pid)) + self._assert_alive() @wrap_exceptions def cwd(self): @@ -530,11 +560,9 @@ def cwd(self): procfs_path = self._procfs_path try: return os.readlink("%s/%s/path/cwd" % (procfs_path, self.pid)) - except OSError as err: - if err.errno == errno.ENOENT: - os.stat("%s/%s" % (procfs_path, self.pid)) # raise NSP or AD - return None - raise + except FileNotFoundError: + os.stat("%s/%s" % (procfs_path, self.pid)) # raise NSP or AD + return "" @wrap_exceptions def memory_info(self): @@ -561,7 +589,8 @@ def threads(self): tid = int(tid) try: utime, stime = cext.query_process_thread( - self.pid, tid, procfs_path) + self.pid, tid, procfs_path + ) except EnvironmentError as err: if err.errno == errno.EOVERFLOW and not IS_64_BIT: # We may get here if we attempt to query a 64bit process @@ -581,8 +610,7 @@ def threads(self): nt = _common.pthread(tid, utime, stime) ret.append(nt) if hit_enoent: - # raise NSP if the process disappeared on us - os.stat('%s/%s' % (procfs_path, self.pid)) + self._assert_alive() return ret @wrap_exceptions @@ -596,31 +624,29 @@ def open_files(self): if os.path.islink(path): try: file = os.readlink(path) - except OSError as err: - # ENOENT == file which is gone in the meantime - if err.errno == errno.ENOENT: - hit_enoent = True - continue - raise + except FileNotFoundError: + hit_enoent = True + continue else: if isfile_strict(file): retlist.append(_common.popenfile(file, int(fd))) if hit_enoent: - # raise NSP if the process disappeared on us - os.stat('%s/%s' % (procfs_path, self.pid)) + self._assert_alive() return retlist def _get_unix_sockets(self, pid): """Get UNIX sockets used by process by parsing 'pfiles' output.""" # TODO: rewrite this in C (...but the damn netstat source code # does not include this part! Argh!!) - cmd = "pfiles %s" % pid - p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + cmd = ["pfiles", str(pid)] + p = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) stdout, stderr = p.communicate() if PY3: - stdout, stderr = [x.decode(sys.stdout.encoding) - for x in (stdout, stderr)] + stdout, stderr = ( + x.decode(sys.stdout.encoding) for x in (stdout, stderr) + ) if p.returncode != 0: if 'permission denied' in stderr.lower(): raise AccessDenied(self.pid, self._name) @@ -656,8 +682,10 @@ def connections(self, kind='inet'): # UNIX sockets if kind in ('all', 'unix'): - ret.extend([_common.pconn(*conn) for conn in - self._get_unix_sockets(self.pid)]) + ret.extend([ + _common.pconn(*conn) + for conn in self._get_unix_sockets(self.pid) + ]) return ret nt_mmap_grouped = namedtuple('mmap', 'path rss anon locked') @@ -666,8 +694,10 @@ def connections(self, kind='inet'): @wrap_exceptions def memory_maps(self): def toaddr(start, end): - return '%s-%s' % (hex(start)[2:].strip('L'), - hex(end)[2:].strip('L')) + return '%s-%s' % ( + hex(start)[2:].strip('L'), + hex(end)[2:].strip('L'), + ) procfs_path = self._procfs_path retlist = [] @@ -692,7 +722,8 @@ def toaddr(start, end): if not name.startswith('['): try: name = os.readlink( - '%s/%s/path/%s' % (procfs_path, self.pid, name)) + '%s/%s/path/%s' % (procfs_path, self.pid, name) + ) except OSError as err: if err.errno == errno.ENOENT: # sometimes the link may not be resolved by @@ -707,8 +738,7 @@ def toaddr(start, end): raise retlist.append((addr, perm, name, rss, anon, locked)) if hit_enoent: - # raise NSP if the process disappeared on us - os.stat('%s/%s' % (procfs_path, self.pid)) + self._assert_alive() return retlist @wrap_exceptions @@ -718,7 +748,8 @@ def num_fds(self): @wrap_exceptions def num_ctx_switches(self): return _common.pctxsw( - *cext.proc_num_ctx_switches(self.pid, self._procfs_path)) + *cext.proc_num_ctx_switches(self.pid, self._procfs_path) + ) @wrap_exceptions def wait(self, timeout=None): diff --git a/addon/globalPlugins/soundmanager/psutil/_pssunos.pyc b/addon/globalPlugins/soundmanager/psutil/_pssunos.pyc deleted file mode 100644 index 84c9137..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/_pssunos.pyc and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/_psutil_linux.so b/addon/globalPlugins/soundmanager/psutil/_psutil_linux.so deleted file mode 100755 index c33b8f9..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/_psutil_linux.so and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/_psutil_posix.so b/addon/globalPlugins/soundmanager/psutil/_psutil_posix.so deleted file mode 100755 index 4fcc900..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/_psutil_posix.so and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/_psutil_windows.cp37-win32.pyd b/addon/globalPlugins/soundmanager/psutil/_psutil_windows.cp37-win32.pyd deleted file mode 100755 index feb16eb..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/_psutil_windows.cp37-win32.pyd and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/_psutil_windows.pyd b/addon/globalPlugins/soundmanager/psutil/_psutil_windows.pyd new file mode 100644 index 0000000..bf4fbaa Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/_psutil_windows.pyd differ diff --git a/addon/globalPlugins/soundmanager/psutil/_pswindows.py b/addon/globalPlugins/soundmanager/psutil/_pswindows.py index 18651d6..2d3a0c9 100644 --- a/addon/globalPlugins/soundmanager/psutil/_pswindows.py +++ b/addon/globalPlugins/soundmanager/psutil/_pswindows.py @@ -8,48 +8,30 @@ import errno import functools import os +import signal import sys import time from collections import namedtuple from . import _common -try: - from . import _psutil_windows as cext -except ImportError as err: - if str(err).lower().startswith("dll load failed") and \ - sys.getwindowsversion()[0] < 6: - # We may get here if: - # 1) we are on an old Windows version - # 2) psutil was installed via pip + wheel - # See: https://github.com/giampaolo/psutil/issues/811 - # It must be noted that psutil can still (kind of) work - # on outdated systems if compiled / installed from sources, - # but if we get here it means this this was a wheel (or exe). - msg = "this Windows version is too old (< Windows Vista); " - msg += "psutil 3.4.2 is the latest version which supports Windows " - msg += "2000, XP and 2003 server; it may be possible that psutil " - msg += "will work if compiled from sources though" - raise RuntimeError(msg) - else: - raise - -from ._common import conn_tmap from ._common import ENCODING from ._common import ENCODING_ERRS +from ._common import AccessDenied +from ._common import NoSuchProcess +from ._common import TimeoutExpired +from ._common import conn_tmap +from ._common import conn_to_ntuple +from ._common import debug from ._common import isfile_strict +from ._common import memoize from ._common import memoize_when_activated from ._common import parse_environ_block -from ._common import sockfam_to_enum -from ._common import socktype_to_enum from ._common import usage_percent +from ._compat import PY3 from ._compat import long from ._compat import lru_cache -from ._compat import PY3 +from ._compat import range from ._compat import unicode -from ._compat import xrange -from ._exceptions import AccessDenied -from ._exceptions import NoSuchProcess -from ._exceptions import TimeoutExpired from ._psutil_windows import ABOVE_NORMAL_PRIORITY_CLASS from ._psutil_windows import BELOW_NORMAL_PRIORITY_CLASS from ._psutil_windows import HIGH_PRIORITY_CLASS @@ -57,21 +39,45 @@ from ._psutil_windows import NORMAL_PRIORITY_CLASS from ._psutil_windows import REALTIME_PRIORITY_CLASS -if sys.version_info >= (3, 4): + +try: + from . import _psutil_windows as cext +except ImportError as err: + if ( + str(err).lower().startswith("dll load failed") + and sys.getwindowsversion()[0] < 6 + ): + # We may get here if: + # 1) we are on an old Windows version + # 2) psutil was installed via pip + wheel + # See: https://github.com/giampaolo/psutil/issues/811 + msg = "this Windows version is too old (< Windows Vista); " + msg += "psutil 3.4.2 is the latest version which supports Windows " + msg += "2000, XP and 2003 server" + raise RuntimeError(msg) + else: + raise + +if PY3: import enum else: enum = None # process priority constants, import from __init__.py: # http://msdn.microsoft.com/en-us/library/ms686219(v=vs.85).aspx +# fmt: off __extra__all__ = [ "win_service_iter", "win_service_get", + # Process priority "ABOVE_NORMAL_PRIORITY_CLASS", "BELOW_NORMAL_PRIORITY_CLASS", - "HIGH_PRIORITY_CLASS", "IDLE_PRIORITY_CLASS", - "NORMAL_PRIORITY_CLASS", "REALTIME_PRIORITY_CLASS", - "CONN_DELETE_TCB", - "AF_LINK", + "HIGH_PRIORITY_CLASS", "IDLE_PRIORITY_CLASS", "NORMAL_PRIORITY_CLASS", + "REALTIME_PRIORITY_CLASS", + # IO priority + "IOPRIO_VERYLOW", "IOPRIO_LOW", "IOPRIO_NORMAL", "IOPRIO_HIGH", + # others + "CONN_DELETE_TCB", "AF_LINK", ] +# fmt: on # ===================================================================== @@ -79,11 +85,8 @@ # ===================================================================== CONN_DELETE_TCB = "DELETE_TCB" -ACCESS_DENIED_ERRSET = frozenset([errno.EPERM, errno.EACCES, - cext.ERROR_ACCESS_DENIED]) -NO_SUCH_SERVICE_ERRSET = frozenset([cext.ERROR_INVALID_NAME, - cext.ERROR_SERVICE_DOES_NOT_EXIST]) - +ERROR_PARTIAL_COPY = 299 +PYPY = '__pypy__' in sys.builtin_module_names if enum is None: AF_LINK = -1 @@ -108,6 +111,7 @@ } if enum is not None: + class Priority(enum.IntEnum): ABOVE_NORMAL_PRIORITY_CLASS = ABOVE_NORMAL_PRIORITY_CLASS BELOW_NORMAL_PRIORITY_CLASS = BELOW_NORMAL_PRIORITY_CLASS @@ -118,6 +122,21 @@ class Priority(enum.IntEnum): globals().update(Priority.__members__) +if enum is None: + IOPRIO_VERYLOW = 0 + IOPRIO_LOW = 1 + IOPRIO_NORMAL = 2 + IOPRIO_HIGH = 3 +else: + + class IOPriority(enum.IntEnum): + IOPRIO_VERYLOW = 0 + IOPRIO_LOW = 1 + IOPRIO_NORMAL = 2 + IOPRIO_HIGH = 3 + + globals().update(IOPriority.__members__) + pinfo_map = dict( num_handles=0, ctx_switches=1, @@ -149,6 +168,7 @@ class Priority(enum.IntEnum): # ===================================================================== +# fmt: off # psutil.cpu_times() scputimes = namedtuple('scputimes', ['user', 'system', 'idle', 'interrupt', 'dpc']) @@ -171,6 +191,7 @@ class Priority(enum.IntEnum): pio = namedtuple('pio', ['read_count', 'write_count', 'read_bytes', 'write_bytes', 'other_count', 'other_bytes']) +# fmt: on # ===================================================================== @@ -183,11 +204,12 @@ def convert_dos_path(s): r"""Convert paths using native DOS format like: "\Device\HarddiskVolume1\Windows\systemew\file.txt" into: - "C:\Windows\systemew\file.txt" + "C:\Windows\systemew\file.txt". """ rawdrive = '\\'.join(s.split('\\')[:3]) - driveletter = cext.win32_QueryDosDevice(rawdrive) - return os.path.join(driveletter, s[len(rawdrive):]) + driveletter = cext.QueryDosDevice(rawdrive) + remainder = s[len(rawdrive) :] + return os.path.join(driveletter, remainder) def py2_strencode(s): @@ -203,6 +225,11 @@ def py2_strencode(s): return s.encode(ENCODING, ENCODING_ERRS) +@memoize +def getpagesize(): + return cext.getpagesize() + + # ===================================================================== # --- memory # ===================================================================== @@ -211,7 +238,7 @@ def py2_strencode(s): def virtual_memory(): """System virtual memory as a namedtuple.""" mem = cext.virtual_mem() - totphys, availphys, totpagef, availpagef, totvirt, freevirt = mem + totphys, availphys, totsys, availsys = mem # total = totphys avail = availphys @@ -224,10 +251,26 @@ def virtual_memory(): def swap_memory(): """Swap system memory as a (total, used, free, sin, sout) tuple.""" mem = cext.virtual_mem() - total = mem[2] - free = mem[3] - used = total - free - percent = usage_percent(used, total, round_=1) + + total_phys = mem[0] + total_system = mem[2] + + # system memory (commit total/limit) is the sum of physical and swap + # thus physical memory values need to be subtracted to get swap values + total = total_system - total_phys + # commit total is incremented immediately (decrementing free_system) + # while the corresponding free physical value is not decremented until + # pages are accessed, so we can't use free system memory for swap. + # instead, we calculate page file usage based on performance counter + if total > 0: + percentswap = cext.swap_percent() + used = int(0.01 * percentswap * total) + else: + percentswap = 0.0 + used = 0 + + free = total - used + percent = round(percentswap, 1) return _common.sswap(total, used, free, percent, 0, 0) @@ -269,8 +312,9 @@ def cpu_times(): # interrupt and dpc times. cext.per_cpu_times() does, so we # rely on it to get those only. percpu_summed = scputimes(*[sum(n) for n in zip(*cext.per_cpu_times())]) - return scputimes(user, system, idle, - percpu_summed.interrupt, percpu_summed.dpc) + return scputimes( + user, system, idle, percpu_summed.interrupt, percpu_summed.dpc + ) def per_cpu_times(): @@ -287,17 +331,18 @@ def cpu_count_logical(): return cext.cpu_count_logical() -def cpu_count_physical(): - """Return the number of physical CPU cores in the system.""" - return cext.cpu_count_phys() +def cpu_count_cores(): + """Return the number of CPU cores in the system.""" + return cext.cpu_count_cores() def cpu_stats(): """Return CPU statistics.""" ctx_switches, interrupts, dpcs, syscalls = cext.cpu_stats() soft_interrupts = 0 - return _common.scpustats(ctx_switches, interrupts, soft_interrupts, - syscalls) + return _common.scpustats( + ctx_switches, interrupts, soft_interrupts, syscalls + ) def cpu_freq(): @@ -309,6 +354,24 @@ def cpu_freq(): return [_common.scpufreq(float(curr), min_, float(max_))] +_loadavg_inititialized = False + + +def getloadavg(): + """Return the number of processes in the system run queue averaged + over the last 1, 5, and 15 minutes respectively as a tuple. + """ + global _loadavg_inititialized + + if not _loadavg_inititialized: + cext.init_loadavg_counter() + _loadavg_inititialized = True + + # Drop to 2 decimal points which is what Linux does + raw_loads = cext.getloadavg() + return tuple([round(load, 2) for load in raw_loads]) + + # ===================================================================== # --- network # ===================================================================== @@ -319,24 +382,25 @@ def net_connections(kind, _pid=-1): connections (as opposed to connections opened by one process only). """ if kind not in conn_tmap: - raise ValueError("invalid %r kind argument; choose between %s" - % (kind, ', '.join([repr(x) for x in conn_tmap]))) + raise ValueError( + "invalid %r kind argument; choose between %s" + % (kind, ', '.join([repr(x) for x in conn_tmap])) + ) families, types = conn_tmap[kind] rawlist = cext.net_connections(_pid, families, types) ret = set() for item in rawlist: fd, fam, type, laddr, raddr, status, pid = item - if laddr: - laddr = _common.addr(*laddr) - if raddr: - raddr = _common.addr(*raddr) - status = TCP_STATUSES[status] - fam = sockfam_to_enum(fam) - type = socktype_to_enum(type) - if _pid == -1: - nt = _common.sconn(fd, fam, type, laddr, raddr, status, pid) - else: - nt = _common.pconn(fd, fam, type, laddr, raddr, status) + nt = conn_to_ntuple( + fd, + fam, + type, + laddr, + raddr, + status, + TCP_STATUSES, + pid=pid if _pid == -1 else None, + ) ret.add(nt) return list(ret) @@ -352,7 +416,7 @@ def net_if_stats(): isup, duplex, speed, mtu = items if hasattr(_common, 'NicDuplex'): duplex = _common.NicDuplex(duplex) - ret[name] = _common.snicstats(isup, duplex, speed, mtu) + ret[name] = _common.snicstats(isup, duplex, speed, mtu, '') return ret @@ -451,7 +515,7 @@ def win_service_get(name): return service -class WindowsService(object): +class WindowsService: """Represents an installed Windows service.""" def __init__(self, name, display_name): @@ -460,7 +524,9 @@ def __init__(self, name, display_name): def __str__(self): details = "(name=%r, display_name=%r)" % ( - self._name, self._display_name) + self._name, + self._display_name, + ) return "%s%s" % (self.__class__.__name__, details) def __repr__(self): @@ -478,14 +544,16 @@ def __ne__(self, other): def _query_config(self): with self._wrap_exceptions(): - display_name, binpath, username, start_type = \ + display_name, binpath, username, start_type = ( cext.winservice_query_config(self._name) + ) # XXX - update _self.display_name? return dict( display_name=py2_strencode(display_name), binpath=py2_strencode(binpath), username=py2_strencode(username), - start_type=py2_strencode(start_type)) + start_type=py2_strencode(start_type), + ) def _query_status(self): with self._wrap_exceptions(): @@ -501,17 +569,19 @@ def _wrap_exceptions(self): """ try: yield - except WindowsError as err: - if err.errno in ACCESS_DENIED_ERRSET: - raise AccessDenied( - pid=None, name=self._name, - msg="service %r is not querable (not enough privileges)" % - self._name) - elif err.errno in NO_SUCH_SERVICE_ERRSET or \ - err.winerror in NO_SUCH_SERVICE_ERRSET: - raise NoSuchProcess( - pid=None, name=self._name, - msg="service %r does not exist)" % self._name) + except OSError as err: + if is_permission_err(err): + msg = ( + "service %r is not querable (not enough privileges)" + % self._name + ) + raise AccessDenied(pid=None, name=self._name, msg=msg) + elif err.winerror in ( + cext.ERROR_INVALID_NAME, + cext.ERROR_SERVICE_DOES_NOT_EXIST, + ): + msg = "service %r does not exist" % self._name + raise NoSuchProcess(pid=None, name=self._name, msg=msg) else: raise @@ -625,27 +695,77 @@ def as_dict(self): ppid_map = cext.ppid_map # used internally by Process.children() +def is_permission_err(exc): + """Return True if this is a permission error.""" + assert isinstance(exc, OSError), exc + if exc.errno in (errno.EPERM, errno.EACCES): + return True + # On Python 2 OSError doesn't always have 'winerror'. Sometimes + # it does, in which case the original exception was WindowsError + # (which is a subclass of OSError). + if getattr(exc, "winerror", -1) in ( + cext.ERROR_ACCESS_DENIED, + cext.ERROR_PRIVILEGE_NOT_HELD, + ): + return True + return False + + +def convert_oserror(exc, pid=None, name=None): + """Convert OSError into NoSuchProcess or AccessDenied.""" + assert isinstance(exc, OSError), exc + if is_permission_err(exc): + return AccessDenied(pid=pid, name=name) + if exc.errno == errno.ESRCH: + return NoSuchProcess(pid=pid, name=name) + raise exc + + def wrap_exceptions(fun): - """Decorator which translates bare OSError and WindowsError - exceptions into NoSuchProcess and AccessDenied. - """ + """Decorator which converts OSError into NoSuchProcess or AccessDenied.""" + @functools.wraps(fun) def wrapper(self, *args, **kwargs): try: return fun(self, *args, **kwargs) except OSError as err: - if err.errno in ACCESS_DENIED_ERRSET: - raise AccessDenied(self.pid, self._name) - if err.errno == errno.ESRCH: - raise NoSuchProcess(self.pid, self._name) - raise + raise convert_oserror(err, pid=self.pid, name=self._name) + return wrapper -class Process(object): +def retry_error_partial_copy(fun): + """Workaround for https://github.com/giampaolo/psutil/issues/875. + See: https://stackoverflow.com/questions/4457745#4457745. + """ + + @functools.wraps(fun) + def wrapper(self, *args, **kwargs): + delay = 0.0001 + times = 33 + for _ in range(times): # retries for roughly 1 second + try: + return fun(self, *args, **kwargs) + except WindowsError as _: + err = _ + if err.winerror == ERROR_PARTIAL_COPY: + time.sleep(delay) + delay = min(delay * 2, 0.04) + continue + raise + msg = ( + "{} retried {} times, converted to AccessDenied as it's still" + "returning {}".format(fun, times, err) + ) + raise AccessDenied(pid=self.pid, name=self._name, msg=msg) + + return wrapper + + +class Process: """Wrapper class around underlying C implementation.""" - __slots__ = ["pid", "_name", "_ppid"] + __slots__ = ["pid", "_name", "_ppid", "_cache"] def __init__(self, pid): self.pid = pid @@ -655,13 +775,15 @@ def __init__(self, pid): # --- oneshot() stuff def oneshot_enter(self): - self.oneshot_info.cache_activate() + self._proc_info.cache_activate(self) + self.exe.cache_activate(self) def oneshot_exit(self): - self.oneshot_info.cache_deactivate() + self._proc_info.cache_deactivate(self) + self.exe.cache_deactivate(self) @memoize_when_activated - def oneshot_info(self): + def _proc_info(self): """Return multiple information about this process as a raw tuple. """ @@ -669,7 +791,6 @@ def oneshot_info(self): assert len(ret) == len(pinfo_map) return ret - @wrap_exceptions def name(self): """Return process name, which on Windows is always the final part of the executable. @@ -678,37 +799,53 @@ def name(self): # and process-hacker. if self.pid == 0: return "System Idle Process" - elif self.pid == 4: + if self.pid == 4: return "System" - else: - try: - # Note: this will fail with AD for most PIDs owned - # by another user but it's faster. - return py2_strencode(os.path.basename(self.exe())) - except AccessDenied: - return py2_strencode(cext.proc_name(self.pid)) + return os.path.basename(self.exe()) @wrap_exceptions + @memoize_when_activated def exe(self): - # Note: os.path.exists(path) may return False even if the file - # is there, see: - # http://stackoverflow.com/questions/3112546/os-path-exists-lies - - # see https://github.com/giampaolo/psutil/issues/414 - # see https://github.com/giampaolo/psutil/issues/528 - if self.pid in (0, 4): - raise AccessDenied(self.pid, self._name) - return py2_strencode(convert_dos_path(cext.proc_exe(self.pid))) + if PYPY: + try: + exe = cext.proc_exe(self.pid) + except WindowsError as err: + # 24 = ERROR_TOO_MANY_OPEN_FILES. Not sure why this happens + # (perhaps PyPy's JIT delaying garbage collection of files?). + if err.errno == 24: + debug("%r translated into AccessDenied" % err) + raise AccessDenied(self.pid, self._name) + raise + else: + exe = cext.proc_exe(self.pid) + if not PY3: + exe = py2_strencode(exe) + if exe.startswith('\\'): + return convert_dos_path(exe) + return exe # May be "Registry", "MemCompression", ... @wrap_exceptions + @retry_error_partial_copy def cmdline(self): - ret = cext.proc_cmdline(self.pid) + if cext.WINVER >= cext.WINDOWS_8_1: + # PEB method detects cmdline changes but requires more + # privileges: https://github.com/giampaolo/psutil/pull/1398 + try: + ret = cext.proc_cmdline(self.pid, use_peb=True) + except OSError as err: + if is_permission_err(err): + ret = cext.proc_cmdline(self.pid, use_peb=False) + else: + raise + else: + ret = cext.proc_cmdline(self.pid, use_peb=True) if PY3: return ret else: return [py2_strencode(s) for s in ret] @wrap_exceptions + @retry_error_partial_copy def environ(self): ustr = cext.proc_environ(self.pid) if ustr and not PY3: @@ -725,10 +862,10 @@ def _get_raw_meminfo(self): try: return cext.proc_memory_info(self.pid) except OSError as err: - if err.errno in ACCESS_DENIED_ERRSET: + if is_permission_err(err): # TODO: the C ext can probably be refactored in order # to get this from cext.proc_info() - info = self.oneshot_info() + info = self._proc_info() return ( info[pinfo_map['num_page_faults']], info[pinfo_map['peak_wset']], @@ -751,13 +888,14 @@ def memory_info(self): t = self._get_raw_meminfo() rss = t[2] # wset vms = t[7] # pagefile - return pmem(*(rss, vms, ) + t) + return pmem(*(rss, vms) + t) @wrap_exceptions def memory_full_info(self): basic_mem = self.memory_info() uss = cext.proc_memory_uss(self.pid) - return pfullmem(*basic_mem + (uss, )) + uss *= getpagesize() + return pfullmem(*basic_mem + (uss,)) def memory_maps(self): try: @@ -765,16 +903,11 @@ def memory_maps(self): except OSError as err: # XXX - can't use wrap_exceptions decorator as we're # returning a generator; probably needs refactoring. - if err.errno in ACCESS_DENIED_ERRSET: - raise AccessDenied(self.pid, self._name) - if err.errno == errno.ESRCH: - raise NoSuchProcess(self.pid, self._name) - raise + raise convert_oserror(err, self.pid, self._name) else: for addr, perm, path, rss in raw: path = convert_dos_path(path) if not PY3: - assert isinstance(path, unicode), type(path) path = py2_strencode(path) addr = hex(addr) yield (addr, perm, path, rss) @@ -785,7 +918,20 @@ def kill(self): @wrap_exceptions def send_signal(self, sig): - os.kill(self.pid, sig) + if sig == signal.SIGTERM: + cext.proc_kill(self.pid) + # py >= 2.7 + elif sig in ( + getattr(signal, "CTRL_C_EVENT", object()), + getattr(signal, "CTRL_BREAK_EVENT", object()), + ): + os.kill(self.pid, sig) + else: + msg = ( + "only SIGTERM, CTRL_C_EVENT and CTRL_BREAK_EVENT signals " + "are supported on Windows" + ) + raise ValueError(msg) @wrap_exceptions def wait(self, timeout=None): @@ -839,19 +985,19 @@ def username(self): @wrap_exceptions def create_time(self): - # special case for kernel process PIDs; return system boot time - if self.pid in (0, 4): - return boot_time() + # Note: proc_times() not put under oneshot() 'cause create_time() + # is already cached by the main Process class. try: - return cext.proc_create_time(self.pid) + user, system, created = cext.proc_times(self.pid) + return created except OSError as err: - if err.errno in ACCESS_DENIED_ERRSET: - return self.oneshot_info()[pinfo_map['create_time']] + if is_permission_err(err): + return self._proc_info()[pinfo_map['create_time']] raise @wrap_exceptions def num_threads(self): - return self.oneshot_info()[pinfo_map['num_threads']] + return self._proc_info()[pinfo_map['num_threads']] @wrap_exceptions def threads(self): @@ -865,26 +1011,26 @@ def threads(self): @wrap_exceptions def cpu_times(self): try: - user, system = cext.proc_cpu_times(self.pid) + user, system, created = cext.proc_times(self.pid) except OSError as err: - if err.errno in ACCESS_DENIED_ERRSET: - info = self.oneshot_info() - user = info[pinfo_map['user_time']] - system = info[pinfo_map['kernel_time']] - else: + if not is_permission_err(err): raise + info = self._proc_info() + user = info[pinfo_map['user_time']] + system = info[pinfo_map['kernel_time']] # Children user/system times are not retrievable (set to 0). return _common.pcputimes(user, system, 0.0, 0.0) @wrap_exceptions def suspend(self): - return cext.proc_suspend(self.pid) + cext.proc_suspend_or_resume(self.pid, True) @wrap_exceptions def resume(self): - return cext.proc_resume(self.pid) + cext.proc_suspend_or_resume(self.pid, False) @wrap_exceptions + @retry_error_partial_copy def cwd(self): if self.pid in (0, 4): raise AccessDenied(self.pid, self._name) @@ -927,39 +1073,43 @@ def nice_get(self): def nice_set(self, value): return cext.proc_priority_set(self.pid, value) - # available on Windows >= Vista - if hasattr(cext, "proc_io_priority_get"): - @wrap_exceptions - def ionice_get(self): - return cext.proc_io_priority_get(self.pid) - - @wrap_exceptions - def ionice_set(self, value, _): - if _: - raise TypeError("set_proc_ionice() on Windows takes only " - "1 argument (2 given)") - if value not in (2, 1, 0): - raise ValueError("value must be 2 (normal), 1 (low) or 0 " - "(very low); got %r" % value) - return cext.proc_io_priority_set(self.pid, value) + @wrap_exceptions + def ionice_get(self): + ret = cext.proc_io_priority_get(self.pid) + if enum is not None: + ret = IOPriority(ret) + return ret + + @wrap_exceptions + def ionice_set(self, ioclass, value): + if value: + msg = "value argument not accepted on Windows" + raise TypeError(msg) + if ioclass not in ( + IOPRIO_VERYLOW, + IOPRIO_LOW, + IOPRIO_NORMAL, + IOPRIO_HIGH, + ): + raise ValueError("%s is not a valid priority" % ioclass) + cext.proc_io_priority_set(self.pid, ioclass) @wrap_exceptions def io_counters(self): try: ret = cext.proc_io_counters(self.pid) except OSError as err: - if err.errno in ACCESS_DENIED_ERRSET: - info = self.oneshot_info() - ret = ( - info[pinfo_map['io_rcount']], - info[pinfo_map['io_wcount']], - info[pinfo_map['io_rbytes']], - info[pinfo_map['io_wbytes']], - info[pinfo_map['io_count_others']], - info[pinfo_map['io_bytes_others']], - ) - else: + if not is_permission_err(err): raise + info = self._proc_info() + ret = ( + info[pinfo_map['io_rcount']], + info[pinfo_map['io_wcount']], + info[pinfo_map['io_rbytes']], + info[pinfo_map['io_wbytes']], + info[pinfo_map['io_count_others']], + info[pinfo_map['io_bytes_others']], + ) return pio(*ret) @wrap_exceptions @@ -973,18 +1123,19 @@ def status(self): @wrap_exceptions def cpu_affinity_get(self): def from_bitmask(x): - return [i for i in xrange(64) if (1 << i) & x] + return [i for i in range(64) if (1 << i) & x] + bitmask = cext.proc_cpu_affinity_get(self.pid) return from_bitmask(bitmask) @wrap_exceptions def cpu_affinity_set(self, value): - def to_bitmask(l): - if not l: - raise ValueError("invalid argument %r" % l) + def to_bitmask(ls): + if not ls: + raise ValueError("invalid argument %r" % ls) out = 0 - for b in l: - out |= 2 ** b + for b in ls: + out |= 2**b return out # SetProcessAffinityMask() states that ERROR_INVALID_PARAMETER @@ -995,7 +1146,8 @@ def to_bitmask(l): if cpu not in allcpus: if not isinstance(cpu, (int, long)): raise TypeError( - "invalid CPU %r; an integer is required" % cpu) + "invalid CPU %r; an integer is required" % cpu + ) else: raise ValueError("invalid CPU %r" % cpu) @@ -1007,12 +1159,12 @@ def num_handles(self): try: return cext.proc_num_handles(self.pid) except OSError as err: - if err.errno in ACCESS_DENIED_ERRSET: - return self.oneshot_info()[pinfo_map['num_handles']] + if is_permission_err(err): + return self._proc_info()[pinfo_map['num_handles']] raise @wrap_exceptions def num_ctx_switches(self): - ctx_switches = self.oneshot_info()[pinfo_map['ctx_switches']] + ctx_switches = self._proc_info()[pinfo_map['ctx_switches']] # only voluntary ctx switches are supported return _common.pctxsw(ctx_switches, 0) diff --git a/addon/globalPlugins/soundmanager/psutil/_pswindows.pyc b/addon/globalPlugins/soundmanager/psutil/_pswindows.pyc deleted file mode 100644 index 609f421..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/_pswindows.pyc and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/python3.dll b/addon/globalPlugins/soundmanager/psutil/python3.dll new file mode 100644 index 0000000..5cfbd1b Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/python3.dll differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/__init__.py b/addon/globalPlugins/soundmanager/psutil/tests/__init__.py index 7968171..32c06e1 100644 --- a/addon/globalPlugins/soundmanager/psutil/tests/__init__.py +++ b/addon/globalPlugins/soundmanager/psutil/tests/__init__.py @@ -1,1206 +1,2043 @@ -# -*- coding: utf-8 -*- - -# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -""" -Test utilities. -""" - -from __future__ import print_function - -import atexit -import contextlib -import ctypes -import errno -import functools -import os -import random -import re -import select -import shutil -import socket -import stat -import subprocess -import sys -import tempfile -import textwrap -import threading -import time -import traceback -import warnings -from socket import AF_INET -from socket import AF_INET6 -from socket import SOCK_DGRAM -from socket import SOCK_STREAM - -import psutil -from psutil import MACOS -from psutil import POSIX -from psutil import SUNOS -from psutil import WINDOWS -from psutil._common import supports_ipv6 -from psutil._compat import PY3 -from psutil._compat import u -from psutil._compat import unicode -from psutil._compat import which - -if sys.version_info < (2, 7): - import unittest2 as unittest # requires "pip install unittest2" -else: - import unittest - -try: - from unittest import mock # py3 -except ImportError: - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - import mock # NOQA - requires "pip install mock" - -if sys.version_info >= (3, 4): - import enum -else: - enum = None - - -__all__ = [ - # constants - 'APPVEYOR', 'DEVNULL', 'GLOBAL_TIMEOUT', 'MEMORY_TOLERANCE', 'NO_RETRIES', - 'PYPY', 'PYTHON_EXE', 'ROOT_DIR', 'SCRIPTS_DIR', 'TESTFILE_PREFIX', - 'TESTFN', 'TESTFN_UNICODE', 'TOX', 'TRAVIS', 'VALID_PROC_STATUSES', - "HAS_CPU_AFFINITY", "HAS_CPU_FREQ", "HAS_ENVIRON", "HAS_PROC_IO_COUNTERS", - "HAS_IONICE", "HAS_MEMORY_MAPS", "HAS_PROC_CPU_NUM", "HAS_RLIMIT", - "HAS_SENSORS_BATTERY", "HAS_BATTERY", "HAS_SENSORS_FANS", - "HAS_SENSORS_TEMPERATURES", "HAS_MEMORY_FULL_INFO", - # subprocesses - 'pyrun', 'reap_children', 'get_test_subprocess', 'create_zombie_proc', - 'create_proc_children_pair', - # threads - 'ThreadTask' - # test utils - 'unittest', 'skip_on_access_denied', 'skip_on_not_implemented', - 'retry_on_failure', - # install utils - 'install_pip', 'install_test_deps', - # fs utils - 'chdir', 'safe_rmpath', 'create_exe', 'decode_path', 'encode_path', - 'unique_filename', - # os - 'get_winver', 'get_kernel_version', - # sync primitives - 'call_until', 'wait_for_pid', 'wait_for_file', - # network - 'check_connection_ntuple', 'check_net_address', - 'get_free_port', 'unix_socket_path', 'bind_socket', 'bind_unix_socket', - 'tcp_socketpair', 'unix_socketpair', 'create_sockets', - # compat - 'reload_module', 'import_module_by_path', - # others - 'warn', 'copyload_shared_lib', 'is_namedtuple', -] - - -# =================================================================== -# --- constants -# =================================================================== - -# --- platforms - -TOX = os.getenv('TOX') or '' in ('1', 'true') -PYPY = '__pypy__' in sys.builtin_module_names -WIN_VISTA = (6, 0, 0) if WINDOWS else None -# whether we're running this test suite on Travis (https://travis-ci.org/) -TRAVIS = bool(os.environ.get('TRAVIS')) -# whether we're running this test suite on Appveyor for Windows -# (http://www.appveyor.com/) -APPVEYOR = bool(os.environ.get('APPVEYOR')) -PYPY = '__pypy__' in sys.builtin_module_names - -# --- configurable defaults - -# how many times retry_on_failure() decorator will retry -NO_RETRIES = 10 -# bytes tolerance for system-wide memory related tests -MEMORY_TOLERANCE = 500 * 1024 # 500KB -# the timeout used in functions which have to wait -GLOBAL_TIMEOUT = 3 if TRAVIS or APPVEYOR else 0.5 -# be more tolerant if we're on travis / appveyor in order to avoid -# false positives -if TRAVIS or APPVEYOR: - NO_RETRIES *= 3 - GLOBAL_TIMEOUT *= 3 - -# --- files - -TESTFILE_PREFIX = '$testfn' -if os.name == 'java': - # Jython disallows @ in module names - TESTFILE_PREFIX = '$psutil-test-' -else: - TESTFILE_PREFIX = '@psutil-test-' -TESTFN = os.path.join(os.path.realpath(os.getcwd()), TESTFILE_PREFIX) -# Disambiguate TESTFN for parallel testing, while letting it remain a valid -# module name. -TESTFN = TESTFN + str(os.getpid()) - -_TESTFN = TESTFN + '-internal' -TESTFN_UNICODE = TESTFN + u("-ƒőő") -ASCII_FS = sys.getfilesystemencoding().lower() in ('ascii', 'us-ascii') - -# --- paths - -ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) -SCRIPTS_DIR = os.path.join(ROOT_DIR, 'scripts') -HERE = os.path.abspath(os.path.dirname(__file__)) - -# --- support - -HAS_CONNECTIONS_UNIX = POSIX and not SUNOS -HAS_CPU_AFFINITY = hasattr(psutil.Process, "cpu_affinity") -HAS_CPU_FREQ = hasattr(psutil, "cpu_freq") -HAS_GETLOADAVG = hasattr(psutil, "getloadavg") -HAS_ENVIRON = hasattr(psutil.Process, "environ") -HAS_IONICE = hasattr(psutil.Process, "ionice") -HAS_MEMORY_MAPS = hasattr(psutil.Process, "memory_maps") -HAS_NET_IO_COUNTERS = hasattr(psutil, "net_io_counters") -HAS_PROC_CPU_NUM = hasattr(psutil.Process, "cpu_num") -HAS_PROC_IO_COUNTERS = hasattr(psutil.Process, "io_counters") -HAS_RLIMIT = hasattr(psutil.Process, "rlimit") -HAS_SENSORS_BATTERY = hasattr(psutil, "sensors_battery") -try: - HAS_BATTERY = HAS_SENSORS_BATTERY and bool(psutil.sensors_battery()) -except Exception: - HAS_BATTERY = True -HAS_SENSORS_FANS = hasattr(psutil, "sensors_fans") -HAS_SENSORS_TEMPERATURES = hasattr(psutil, "sensors_temperatures") -HAS_THREADS = hasattr(psutil.Process, "threads") - -# --- misc - - -def _get_py_exe(): - def attempt(exe): - try: - subprocess.check_call( - [exe, "-V"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - except Exception: - return None - else: - return exe - - if MACOS: - exe = \ - attempt(sys.executable) or \ - attempt(os.path.realpath(sys.executable)) or \ - attempt(which("python%s.%s" % sys.version_info[:2])) or \ - attempt(psutil.Process().exe()) - if not exe: - raise ValueError("can't find python exe real abspath") - return exe - else: - exe = os.path.realpath(sys.executable) - if WINDOWS: - # avoid subprocess warnings - exe = exe.replace('\\', '\\\\') - assert os.path.exists(exe), exe - return exe - - -PYTHON_EXE = _get_py_exe() -DEVNULL = open(os.devnull, 'r+') -VALID_PROC_STATUSES = [getattr(psutil, x) for x in dir(psutil) - if x.startswith('STATUS_')] -AF_UNIX = getattr(socket, "AF_UNIX", object()) -SOCK_SEQPACKET = getattr(socket, "SOCK_SEQPACKET", object()) - -_subprocesses_started = set() -_pids_started = set() -_testfiles_created = set() - - -@atexit.register -def cleanup_test_files(): - DEVNULL.close() - for name in os.listdir(u('.')): - if isinstance(name, unicode): - prefix = u(TESTFILE_PREFIX) - else: - prefix = TESTFILE_PREFIX - if name.startswith(prefix): - try: - safe_rmpath(name) - except Exception: - traceback.print_exc() - for path in _testfiles_created: - try: - safe_rmpath(path) - except Exception: - traceback.print_exc() - - -# this is executed first -@atexit.register -def cleanup_test_procs(): - reap_children(recursive=True) - - -# =================================================================== -# --- threads -# =================================================================== - - -class ThreadTask(threading.Thread): - """A thread task which does nothing expect staying alive.""" - - def __init__(self): - threading.Thread.__init__(self) - self._running = False - self._interval = 0.001 - self._flag = threading.Event() - - def __repr__(self): - name = self.__class__.__name__ - return '<%s running=%s at %#x>' % (name, self._running, id(self)) - - def __enter__(self): - self.start() - return self - - def __exit__(self, *args, **kwargs): - self.stop() - - def start(self): - """Start thread and keep it running until an explicit - stop() request. Polls for shutdown every 'timeout' seconds. - """ - if self._running: - raise ValueError("already started") - threading.Thread.start(self) - self._flag.wait() - - def run(self): - self._running = True - self._flag.set() - while self._running: - time.sleep(self._interval) - - def stop(self): - """Stop thread execution and and waits until it is stopped.""" - if not self._running: - raise ValueError("already stopped") - self._running = False - self.join() - - -# =================================================================== -# --- subprocesses -# =================================================================== - - -def _reap_children_on_err(fun): - @functools.wraps(fun) - def wrapper(*args, **kwargs): - try: - return fun(*args, **kwargs) - except Exception: - reap_children() - raise - return wrapper - - -@_reap_children_on_err -def get_test_subprocess(cmd=None, **kwds): - """Creates a python subprocess which does nothing for 60 secs and - return it as subprocess.Popen instance. - If "cmd" is specified that is used instead of python. - By default stdin and stdout are redirected to /dev/null. - It also attemps to make sure the process is in a reasonably - initialized state. - The process is registered for cleanup on reap_children(). - """ - kwds.setdefault("stdin", DEVNULL) - kwds.setdefault("stdout", DEVNULL) - kwds.setdefault("cwd", os.getcwd()) - kwds.setdefault("env", os.environ) - if WINDOWS: - # Prevents the subprocess to open error dialogs. This will also - # cause stderr to be suppressed, which is suboptimal in order - # to debug broken tests. - CREATE_NO_WINDOW = 0x8000000 - kwds.setdefault("creationflags", CREATE_NO_WINDOW) - if cmd is None: - safe_rmpath(_TESTFN) - pyline = "from time import sleep;" \ - "open(r'%s', 'w').close();" \ - "sleep(60);" % _TESTFN - cmd = [PYTHON_EXE, "-c", pyline] - sproc = subprocess.Popen(cmd, **kwds) - _subprocesses_started.add(sproc) - wait_for_file(_TESTFN, delete=True, empty=True) - else: - sproc = subprocess.Popen(cmd, **kwds) - _subprocesses_started.add(sproc) - wait_for_pid(sproc.pid) - return sproc - - -@_reap_children_on_err -def create_proc_children_pair(): - """Create a subprocess which creates another one as in: - A (us) -> B (child) -> C (grandchild). - Return a (child, grandchild) tuple. - The 2 processes are fully initialized and will live for 60 secs - and are registered for cleanup on reap_children(). - """ - _TESTFN2 = os.path.basename(_TESTFN) + '2' # need to be relative - s = textwrap.dedent("""\ - import subprocess, os, sys, time - s = "import os, time;" - s += "f = open('%s', 'w');" - s += "f.write(str(os.getpid()));" - s += "f.close();" - s += "time.sleep(60);" - p = subprocess.Popen(['%s', '-c', s]) - p.wait() - """ % (_TESTFN2, PYTHON_EXE)) - # On Windows if we create a subprocess with CREATE_NO_WINDOW flag - # set (which is the default) a "conhost.exe" extra process will be - # spawned as a child. We don't want that. - if WINDOWS: - subp = pyrun(s, creationflags=0) - else: - subp = pyrun(s) - child1 = psutil.Process(subp.pid) - data = wait_for_file(_TESTFN2, delete=False, empty=False) - safe_rmpath(_TESTFN2) - child2_pid = int(data) - _pids_started.add(child2_pid) - child2 = psutil.Process(child2_pid) - return (child1, child2) - - -def create_zombie_proc(): - """Create a zombie process and return its PID.""" - assert psutil.POSIX - unix_file = tempfile.mktemp(prefix=TESTFILE_PREFIX) if MACOS else TESTFN - src = textwrap.dedent("""\ - import os, sys, time, socket, contextlib - child_pid = os.fork() - if child_pid > 0: - time.sleep(3000) - else: - # this is the zombie process - s = socket.socket(socket.AF_UNIX) - with contextlib.closing(s): - s.connect('%s') - if sys.version_info < (3, ): - pid = str(os.getpid()) - else: - pid = bytes(str(os.getpid()), 'ascii') - s.sendall(pid) - """ % unix_file) - with contextlib.closing(socket.socket(socket.AF_UNIX)) as sock: - sock.settimeout(GLOBAL_TIMEOUT) - sock.bind(unix_file) - sock.listen(1) - pyrun(src) - conn, _ = sock.accept() - try: - select.select([conn.fileno()], [], [], GLOBAL_TIMEOUT) - zpid = int(conn.recv(1024)) - _pids_started.add(zpid) - zproc = psutil.Process(zpid) - call_until(lambda: zproc.status(), "ret == psutil.STATUS_ZOMBIE") - return zpid - finally: - conn.close() - - -@_reap_children_on_err -def pyrun(src, **kwds): - """Run python 'src' code string in a separate interpreter. - Returns a subprocess.Popen instance. - """ - kwds.setdefault("stdout", None) - kwds.setdefault("stderr", None) - with tempfile.NamedTemporaryFile( - prefix=TESTFILE_PREFIX, mode="wt", delete=False) as f: - _testfiles_created.add(f.name) - f.write(src) - f.flush() - subp = get_test_subprocess([PYTHON_EXE, f.name], **kwds) - wait_for_pid(subp.pid) - return subp - - -@_reap_children_on_err -def sh(cmd, **kwds): - """run cmd in a subprocess and return its output. - raises RuntimeError on error. - """ - shell = True if isinstance(cmd, (str, unicode)) else False - # Prevents subprocess to open error dialogs in case of error. - flags = 0x8000000 if WINDOWS and shell else 0 - kwds.setdefault("shell", shell) - kwds.setdefault("stdout", subprocess.PIPE) - kwds.setdefault("stderr", subprocess.PIPE) - kwds.setdefault("universal_newlines", True) - kwds.setdefault("creationflags", flags) - p = subprocess.Popen(cmd, **kwds) - _subprocesses_started.add(p) - stdout, stderr = p.communicate() - if p.returncode != 0: - raise RuntimeError(stderr) - if stderr: - warn(stderr) - if stdout.endswith('\n'): - stdout = stdout[:-1] - return stdout - - -def reap_children(recursive=False): - """Terminate and wait() any subprocess started by this test suite - and ensure that no zombies stick around to hog resources and - create problems when looking for refleaks. - - If resursive is True it also tries to terminate and wait() - all grandchildren started by this process. - """ - # This is here to make sure wait_procs() behaves properly and - # investigate: - # https://ci.appveyor.com/project/giampaolo/psutil/build/job/ - # jiq2cgd6stsbtn60 - def assert_gone(pid): - assert not psutil.pid_exists(pid), pid - assert pid not in psutil.pids(), pid - try: - p = psutil.Process(pid) - assert not p.is_running(), pid - except psutil.NoSuchProcess: - pass - else: - assert 0, "pid %s is not gone" % pid - - # Get the children here, before terminating the children sub - # processes as we don't want to lose the intermediate reference - # in case of grandchildren. - if recursive: - children = set(psutil.Process().children(recursive=True)) - else: - children = set() - - # Terminate subprocess.Popen instances "cleanly" by closing their - # fds and wiat()ing for them in order to avoid zombies. - while _subprocesses_started: - subp = _subprocesses_started.pop() - _pids_started.add(subp.pid) - try: - subp.terminate() - except OSError as err: - if WINDOWS and err.winerror == 6: # "invalid handle" - pass - elif err.errno != errno.ESRCH: - raise - if subp.stdout: - subp.stdout.close() - if subp.stderr: - subp.stderr.close() - try: - # Flushing a BufferedWriter may raise an error. - if subp.stdin: - subp.stdin.close() - finally: - # Wait for the process to terminate, to avoid zombies. - try: - subp.wait() - except OSError as err: - if err.errno != errno.ECHILD: - raise - - # Terminate started pids. - while _pids_started: - pid = _pids_started.pop() - try: - p = psutil.Process(pid) - except psutil.NoSuchProcess: - assert_gone(pid) - else: - children.add(p) - - # Terminate children. - if children: - for p in children: - try: - p.terminate() - except psutil.NoSuchProcess: - pass - gone, alive = psutil.wait_procs(children, timeout=GLOBAL_TIMEOUT) - for p in alive: - warn("couldn't terminate process %r; attempting kill()" % p) - try: - p.kill() - except psutil.NoSuchProcess: - pass - gone, alive = psutil.wait_procs(alive, timeout=GLOBAL_TIMEOUT) - if alive: - for p in alive: - warn("process %r survived kill()" % p) - - for p in children: - assert_gone(p.pid) - - -# =================================================================== -# --- OS -# =================================================================== - - -def get_kernel_version(): - """Return a tuple such as (2, 6, 36).""" - if not POSIX: - raise NotImplementedError("not POSIX") - s = "" - uname = os.uname()[2] - for c in uname: - if c.isdigit() or c == '.': - s += c - else: - break - if not s: - raise ValueError("can't parse %r" % uname) - minor = 0 - micro = 0 - nums = s.split('.') - major = int(nums[0]) - if len(nums) >= 2: - minor = int(nums[1]) - if len(nums) >= 3: - micro = int(nums[2]) - return (major, minor, micro) - - -def get_winver(): - if not WINDOWS: - raise NotImplementedError("not WINDOWS") - wv = sys.getwindowsversion() - if hasattr(wv, 'service_pack_major'): # python >= 2.7 - sp = wv.service_pack_major or 0 - else: - r = re.search(r"\s\d$", wv[4]) - if r: - sp = int(r.group(0)) - else: - sp = 0 - return (wv[0], wv[1], sp) - - -# =================================================================== -# --- sync primitives -# =================================================================== - - -class retry(object): - """A retry decorator.""" - - def __init__(self, - exception=Exception, - timeout=None, - retries=None, - interval=0.001, - logfun=None, - ): - if timeout and retries: - raise ValueError("timeout and retries args are mutually exclusive") - self.exception = exception - self.timeout = timeout - self.retries = retries - self.interval = interval - self.logfun = logfun - - def __iter__(self): - if self.timeout: - stop_at = time.time() + self.timeout - while time.time() < stop_at: - yield - elif self.retries: - for _ in range(self.retries): - yield - else: - while True: - yield - - def sleep(self): - if self.interval is not None: - time.sleep(self.interval) - - def __call__(self, fun): - @functools.wraps(fun) - def wrapper(*args, **kwargs): - exc = None - for _ in self: - try: - return fun(*args, **kwargs) - except self.exception as _: # NOQA - exc = _ - if self.logfun is not None: - self.logfun(exc) - self.sleep() - continue - if PY3: - raise exc - else: - raise - - # This way the user of the decorated function can change config - # parameters. - wrapper.decorator = self - return wrapper - - -@retry(exception=psutil.NoSuchProcess, logfun=None, timeout=GLOBAL_TIMEOUT, - interval=0.001) -def wait_for_pid(pid): - """Wait for pid to show up in the process list then return. - Used in the test suite to give time the sub process to initialize. - """ - psutil.Process(pid) - if WINDOWS: - # give it some more time to allow better initialization - time.sleep(0.01) - - -@retry(exception=(EnvironmentError, AssertionError), logfun=None, - timeout=GLOBAL_TIMEOUT, interval=0.001) -def wait_for_file(fname, delete=True, empty=False): - """Wait for a file to be written on disk with some content.""" - with open(fname, "rb") as f: - data = f.read() - if not empty: - assert data - if delete: - safe_rmpath(fname) - return data - - -@retry(exception=AssertionError, logfun=None, timeout=GLOBAL_TIMEOUT, - interval=0.001) -def call_until(fun, expr): - """Keep calling function for timeout secs and exit if eval() - expression is True. - """ - ret = fun() - assert eval(expr) - return ret - - -# =================================================================== -# --- fs -# =================================================================== - - -def safe_rmpath(path): - "Convenience function for removing temporary test files or dirs" - def retry_fun(fun): - # On Windows it could happen that the file or directory has - # open handles or references preventing the delete operation - # to succeed immediately, so we retry for a while. See: - # https://bugs.python.org/issue33240 - stop_at = time.time() + 1 - while time.time() < stop_at: - try: - return fun() - except WindowsError as _: - err = _ - if err.errno != errno.ENOENT: - raise - else: - warn("ignoring %s" % (str(err))) - time.sleep(0.01) - raise err - - try: - st = os.stat(path) - if stat.S_ISDIR(st.st_mode): - fun = functools.partial(shutil.rmtree, path) - else: - fun = functools.partial(os.remove, path) - if POSIX: - fun() - else: - retry_fun(fun) - except OSError as err: - if err.errno != errno.ENOENT: - raise - - -def safe_mkdir(dir): - "Convenience function for creating a directory" - try: - os.mkdir(dir) - except OSError as err: - if err.errno != errno.EEXIST: - raise - - -@contextlib.contextmanager -def chdir(dirname): - "Context manager which temporarily changes the current directory." - curdir = os.getcwd() - try: - os.chdir(dirname) - yield - finally: - os.chdir(curdir) - - -def create_exe(outpath, c_code=None): - """Creates an executable file in the given location.""" - assert not os.path.exists(outpath), outpath - if c_code: - if not which("gcc"): - raise ValueError("gcc is not installed") - if isinstance(c_code, bool): # c_code is True - c_code = textwrap.dedent( - """ - #include - int main() { - pause(); - return 1; - } - """) - assert isinstance(c_code, str), c_code - with tempfile.NamedTemporaryFile( - suffix='.c', delete=False, mode='wt') as f: - f.write(c_code) - try: - subprocess.check_call(["gcc", f.name, "-o", outpath]) - finally: - safe_rmpath(f.name) - else: - # copy python executable - shutil.copyfile(PYTHON_EXE, outpath) - if POSIX: - st = os.stat(outpath) - os.chmod(outpath, st.st_mode | stat.S_IEXEC) - - -def unique_filename(prefix=TESTFILE_PREFIX, suffix=""): - return tempfile.mktemp(prefix=prefix, suffix=suffix) - - -# =================================================================== -# --- testing -# =================================================================== - - -class TestCase(unittest.TestCase): - - # Print a full path representation of the single unit tests - # being run. - def __str__(self): - fqmod = self.__class__.__module__ - if not fqmod.startswith('psutil.'): - fqmod = 'psutil.tests.' + fqmod - return "%s.%s.%s" % ( - fqmod, self.__class__.__name__, self._testMethodName) - - # assertRaisesRegexp renamed to assertRaisesRegex in 3.3; - # add support for the new name. - if not hasattr(unittest.TestCase, 'assertRaisesRegex'): - assertRaisesRegex = unittest.TestCase.assertRaisesRegexp - - -# override default unittest.TestCase -unittest.TestCase = TestCase - - -def retry_on_failure(retries=NO_RETRIES): - """Decorator which runs a test function and retries N times before - actually failing. - """ - def logfun(exc): - print("%r, retrying" % exc, file=sys.stderr) - - return retry(exception=AssertionError, timeout=None, retries=retries, - logfun=logfun) - - -def skip_on_access_denied(only_if=None): - """Decorator to Ignore AccessDenied exceptions.""" - def decorator(fun): - @functools.wraps(fun) - def wrapper(*args, **kwargs): - try: - return fun(*args, **kwargs) - except psutil.AccessDenied: - if only_if is not None: - if not only_if: - raise - raise unittest.SkipTest("raises AccessDenied") - return wrapper - return decorator - - -def skip_on_not_implemented(only_if=None): - """Decorator to Ignore NotImplementedError exceptions.""" - def decorator(fun): - @functools.wraps(fun) - def wrapper(*args, **kwargs): - try: - return fun(*args, **kwargs) - except NotImplementedError: - if only_if is not None: - if not only_if: - raise - msg = "%r was skipped because it raised NotImplementedError" \ - % fun.__name__ - raise unittest.SkipTest(msg) - return wrapper - return decorator - - -# =================================================================== -# --- network -# =================================================================== - - -def get_free_port(host='127.0.0.1'): - """Return an unused TCP port.""" - with contextlib.closing(socket.socket()) as sock: - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.bind((host, 0)) - return sock.getsockname()[1] - - -@contextlib.contextmanager -def unix_socket_path(suffix=""): - """A context manager which returns a non-existent file name - and tries to delete it on exit. - """ - assert psutil.POSIX - path = unique_filename(suffix=suffix) - try: - yield path - finally: - try: - os.unlink(path) - except OSError: - pass - - -def bind_socket(family=AF_INET, type=SOCK_STREAM, addr=None): - """Binds a generic socket.""" - if addr is None and family in (AF_INET, AF_INET6): - addr = ("", 0) - sock = socket.socket(family, type) - try: - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.bind(addr) - if type == socket.SOCK_STREAM: - sock.listen(10) - return sock - except Exception: - sock.close() - raise - - -def bind_unix_socket(name, type=socket.SOCK_STREAM): - """Bind a UNIX socket.""" - assert psutil.POSIX - assert not os.path.exists(name), name - sock = socket.socket(socket.AF_UNIX, type) - try: - sock.bind(name) - if type == socket.SOCK_STREAM: - sock.listen(10) - except Exception: - sock.close() - raise - return sock - - -def tcp_socketpair(family, addr=("", 0)): - """Build a pair of TCP sockets connected to each other. - Return a (server, client) tuple. - """ - with contextlib.closing(socket.socket(family, SOCK_STREAM)) as ll: - ll.bind(addr) - ll.listen(10) - addr = ll.getsockname() - c = socket.socket(family, SOCK_STREAM) - try: - c.connect(addr) - caddr = c.getsockname() - while True: - a, addr = ll.accept() - # check that we've got the correct client - if addr == caddr: - return (a, c) - a.close() - except OSError: - c.close() - raise - - -def unix_socketpair(name): - """Build a pair of UNIX sockets connected to each other through - the same UNIX file name. - Return a (server, client) tuple. - """ - assert psutil.POSIX - server = client = None - try: - server = bind_unix_socket(name, type=socket.SOCK_STREAM) - server.setblocking(0) - client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - client.setblocking(0) - client.connect(name) - # new = server.accept() - except Exception: - if server is not None: - server.close() - if client is not None: - client.close() - raise - return (server, client) - - -@contextlib.contextmanager -def create_sockets(): - """Open as many socket families / types as possible.""" - socks = [] - fname1 = fname2 = None - try: - socks.append(bind_socket(socket.AF_INET, socket.SOCK_STREAM)) - socks.append(bind_socket(socket.AF_INET, socket.SOCK_DGRAM)) - if supports_ipv6(): - socks.append(bind_socket(socket.AF_INET6, socket.SOCK_STREAM)) - socks.append(bind_socket(socket.AF_INET6, socket.SOCK_DGRAM)) - if POSIX and HAS_CONNECTIONS_UNIX: - fname1 = unix_socket_path().__enter__() - fname2 = unix_socket_path().__enter__() - s1, s2 = unix_socketpair(fname1) - s3 = bind_unix_socket(fname2, type=socket.SOCK_DGRAM) - # self.addCleanup(safe_rmpath, fname1) - # self.addCleanup(safe_rmpath, fname2) - for s in (s1, s2, s3): - socks.append(s) - yield socks - finally: - for s in socks: - s.close() - if fname1 is not None: - safe_rmpath(fname1) - if fname2 is not None: - safe_rmpath(fname2) - - -def check_net_address(addr, family): - """Check a net address validity. Supported families are IPv4, - IPv6 and MAC addresses. - """ - import ipaddress # python >= 3.3 / requires "pip install ipaddress" - if enum and PY3: - assert isinstance(family, enum.IntEnum), family - if family == socket.AF_INET: - octs = [int(x) for x in addr.split('.')] - assert len(octs) == 4, addr - for num in octs: - assert 0 <= num <= 255, addr - if not PY3: - addr = unicode(addr) - ipaddress.IPv4Address(addr) - elif family == socket.AF_INET6: - assert isinstance(addr, str), addr - if not PY3: - addr = unicode(addr) - ipaddress.IPv6Address(addr) - elif family == psutil.AF_LINK: - assert re.match(r'([a-fA-F0-9]{2}[:|\-]?){6}', addr) is not None, addr - else: - raise ValueError("unknown family %r", family) - - -def check_connection_ntuple(conn): - """Check validity of a connection namedtuple.""" - # check ntuple - assert len(conn) in (6, 7), conn - has_pid = len(conn) == 7 - has_fd = getattr(conn, 'fd', -1) != -1 - assert conn[0] == conn.fd - assert conn[1] == conn.family - assert conn[2] == conn.type - assert conn[3] == conn.laddr - assert conn[4] == conn.raddr - assert conn[5] == conn.status - if has_pid: - assert conn[6] == conn.pid - - # check fd - if has_fd: - assert conn.fd >= 0, conn - if hasattr(socket, 'fromfd') and not WINDOWS: - try: - dupsock = socket.fromfd(conn.fd, conn.family, conn.type) - except (socket.error, OSError) as err: - if err.args[0] != errno.EBADF: - raise - else: - with contextlib.closing(dupsock): - assert dupsock.family == conn.family - assert dupsock.type == conn.type - - # check family - assert conn.family in (AF_INET, AF_INET6, AF_UNIX), repr(conn.family) - if conn.family in (AF_INET, AF_INET6): - # actually try to bind the local socket; ignore IPv6 - # sockets as their address might be represented as - # an IPv4-mapped-address (e.g. "::127.0.0.1") - # and that's rejected by bind() - if conn.family == AF_INET: - s = socket.socket(conn.family, conn.type) - with contextlib.closing(s): - try: - s.bind((conn.laddr[0], 0)) - except socket.error as err: - if err.errno != errno.EADDRNOTAVAIL: - raise - elif conn.family == AF_UNIX: - assert conn.status == psutil.CONN_NONE, conn.status - - # check type (SOCK_SEQPACKET may happen in case of AF_UNIX socks) - assert conn.type in (SOCK_STREAM, SOCK_DGRAM, SOCK_SEQPACKET), \ - repr(conn.type) - if conn.type == SOCK_DGRAM: - assert conn.status == psutil.CONN_NONE, conn.status - - # check laddr (IP address and port sanity) - for addr in (conn.laddr, conn.raddr): - if conn.family in (AF_INET, AF_INET6): - assert isinstance(addr, tuple), addr - if not addr: - continue - assert isinstance(addr.port, int), addr.port - assert 0 <= addr.port <= 65535, addr.port - check_net_address(addr.ip, conn.family) - elif conn.family == AF_UNIX: - assert isinstance(addr, str), addr - - # check status - assert isinstance(conn.status, str), conn - valids = [getattr(psutil, x) for x in dir(psutil) if x.startswith('CONN_')] - assert conn.status in valids, conn - - -# =================================================================== -# --- compatibility -# =================================================================== - - -def reload_module(module): - """Backport of importlib.reload of Python 3.3+.""" - try: - import importlib - if not hasattr(importlib, 'reload'): # python <=3.3 - raise ImportError - except ImportError: - import imp - return imp.reload(module) - else: - return importlib.reload(module) - - -def import_module_by_path(path): - name = os.path.splitext(os.path.basename(path))[0] - if sys.version_info[0] == 2: - import imp - return imp.load_source(name, path) - elif sys.version_info[:2] <= (3, 4): - from importlib.machinery import SourceFileLoader - return SourceFileLoader(name, path).load_module() - else: - import importlib.util - spec = importlib.util.spec_from_file_location(name, path) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod - - -# =================================================================== -# --- others -# =================================================================== - - -def warn(msg): - """Raise a warning msg.""" - warnings.warn(msg, UserWarning) - - -def is_namedtuple(x): - """Check if object is an instance of namedtuple.""" - t = type(x) - b = t.__bases__ - if len(b) != 1 or b[0] != tuple: - return False - f = getattr(t, '_fields', None) - if not isinstance(f, tuple): - return False - return all(type(n) == str for n in f) - - -if POSIX: - @contextlib.contextmanager - def copyload_shared_lib(dst_prefix=TESTFILE_PREFIX): - """Ctx manager which picks up a random shared CO lib used - by this process, copies it in another location and loads it - in memory via ctypes. Return the new absolutized path. - """ - exe = 'pypy' if PYPY else 'python' - ext = ".so" - dst = tempfile.mktemp(prefix=dst_prefix, suffix=ext) - libs = [x.path for x in psutil.Process().memory_maps() if - os.path.splitext(x.path)[1] == ext and - exe in x.path.lower()] - src = random.choice(libs) - shutil.copyfile(src, dst) - try: - ctypes.CDLL(dst) - yield dst - finally: - safe_rmpath(dst) -else: - @contextlib.contextmanager - def copyload_shared_lib(dst_prefix=TESTFILE_PREFIX): - """Ctx manager which picks up a random shared DLL lib used - by this process, copies it in another location and loads it - in memory via ctypes. - Return the new absolutized, normcased path. - """ - from ctypes import wintypes - from ctypes import WinError - ext = ".dll" - dst = tempfile.mktemp(prefix=dst_prefix, suffix=ext) - libs = [x.path for x in psutil.Process().memory_maps() if - os.path.splitext(x.path)[1].lower() == ext and - 'python' in os.path.basename(x.path).lower() and - 'wow64' not in x.path.lower()] - src = random.choice(libs) - shutil.copyfile(src, dst) - cfile = None - try: - cfile = ctypes.WinDLL(dst) - yield dst - finally: - # Work around OverflowError: - # - https://ci.appveyor.com/project/giampaolo/psutil/build/1207/ - # job/o53330pbnri9bcw7 - # - http://bugs.python.org/issue30286 - # - http://stackoverflow.com/questions/23522055 - if cfile is not None: - FreeLibrary = ctypes.windll.kernel32.FreeLibrary - FreeLibrary.argtypes = [wintypes.HMODULE] - ret = FreeLibrary(cfile._handle) - if ret == 0: - WinError() - safe_rmpath(dst) +# -*- coding: utf-8 -*- + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Test utilities.""" + +from __future__ import print_function + +import atexit +import contextlib +import ctypes +import errno +import functools +import gc +import inspect +import os +import platform +import random +import re +import select +import shlex +import shutil +import signal +import socket +import stat +import subprocess +import sys +import tempfile +import textwrap +import threading +import time +import unittest +import warnings +from socket import AF_INET +from socket import AF_INET6 +from socket import SOCK_STREAM + +import psutil +from psutil import AIX +from psutil import LINUX +from psutil import MACOS +from psutil import NETBSD +from psutil import OPENBSD +from psutil import POSIX +from psutil import SUNOS +from psutil import WINDOWS +from psutil._common import bytes2human +from psutil._common import debug +from psutil._common import memoize +from psutil._common import print_color +from psutil._common import supports_ipv6 +from psutil._compat import PY3 +from psutil._compat import FileExistsError +from psutil._compat import FileNotFoundError +from psutil._compat import range +from psutil._compat import super +from psutil._compat import u +from psutil._compat import unicode +from psutil._compat import which + + +try: + from unittest import mock # py3 +except ImportError: + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + import mock # NOQA - requires "pip install mock" + +if PY3: + import enum +else: + enum = None + +if POSIX: + from psutil._psposix import wait_pid + + +# fmt: off +__all__ = [ + # constants + 'APPVEYOR', 'DEVNULL', 'GLOBAL_TIMEOUT', 'TOLERANCE_SYS_MEM', 'NO_RETRIES', + 'PYPY', 'PYTHON_EXE', 'PYTHON_EXE_ENV', 'ROOT_DIR', 'SCRIPTS_DIR', + 'TESTFN_PREFIX', 'UNICODE_SUFFIX', 'INVALID_UNICODE_SUFFIX', + 'CI_TESTING', 'VALID_PROC_STATUSES', 'TOLERANCE_DISK_USAGE', 'IS_64BIT', + "HAS_CPU_AFFINITY", "HAS_CPU_FREQ", "HAS_ENVIRON", "HAS_PROC_IO_COUNTERS", + "HAS_IONICE", "HAS_MEMORY_MAPS", "HAS_PROC_CPU_NUM", "HAS_RLIMIT", + "HAS_SENSORS_BATTERY", "HAS_BATTERY", "HAS_SENSORS_FANS", + "HAS_SENSORS_TEMPERATURES", "MACOS_11PLUS", + "MACOS_12PLUS", "COVERAGE", + # subprocesses + 'pyrun', 'terminate', 'reap_children', 'spawn_testproc', 'spawn_zombie', + 'spawn_children_pair', + # threads + 'ThreadTask', + # test utils + 'unittest', 'skip_on_access_denied', 'skip_on_not_implemented', + 'retry_on_failure', 'TestMemoryLeak', 'PsutilTestCase', + 'process_namespace', 'system_namespace', 'print_sysinfo', + 'is_win_secure_system_proc', + # fs utils + 'chdir', 'safe_rmpath', 'create_py_exe', 'create_c_exe', 'get_testfn', + # os + 'get_winver', 'kernel_version', + # sync primitives + 'call_until', 'wait_for_pid', 'wait_for_file', + # network + 'check_net_address', 'filter_proc_connections', + 'get_free_port', 'bind_socket', 'bind_unix_socket', 'tcp_socketpair', + 'unix_socketpair', 'create_sockets', + # compat + 'reload_module', 'import_module_by_path', + # others + 'warn', 'copyload_shared_lib', 'is_namedtuple', +] +# fmt: on + + +# =================================================================== +# --- constants +# =================================================================== + +# --- platforms + +PYPY = '__pypy__' in sys.builtin_module_names +# whether we're running this test suite on a Continuous Integration service +APPVEYOR = 'APPVEYOR' in os.environ +GITHUB_ACTIONS = 'GITHUB_ACTIONS' in os.environ or 'CIBUILDWHEEL' in os.environ +CI_TESTING = APPVEYOR or GITHUB_ACTIONS +COVERAGE = 'COVERAGE_RUN' in os.environ +# are we a 64 bit process? +IS_64BIT = sys.maxsize > 2**32 + + +@memoize +def macos_version(): + version_str = platform.mac_ver()[0] + version = tuple(map(int, version_str.split(".")[:2])) + if version == (10, 16): + # When built against an older macOS SDK, Python will report + # macOS 10.16 instead of the real version. + version_str = subprocess.check_output( + [ + sys.executable, + "-sS", + "-c", + "import platform; print(platform.mac_ver()[0])", + ], + env={"SYSTEM_VERSION_COMPAT": "0"}, + universal_newlines=True, + ) + version = tuple(map(int, version_str.split(".")[:2])) + return version + + +if MACOS: + MACOS_11PLUS = macos_version() > (10, 15) + MACOS_12PLUS = macos_version() >= (12, 0) +else: + MACOS_11PLUS = False + MACOS_12PLUS = False + + +# --- configurable defaults + +# how many times retry_on_failure() decorator will retry +NO_RETRIES = 10 +# bytes tolerance for system-wide related tests +TOLERANCE_SYS_MEM = 5 * 1024 * 1024 # 5MB +TOLERANCE_DISK_USAGE = 10 * 1024 * 1024 # 10MB +# the timeout used in functions which have to wait +GLOBAL_TIMEOUT = 5 +# be more tolerant if we're on CI in order to avoid false positives +if CI_TESTING: + NO_RETRIES *= 3 + GLOBAL_TIMEOUT *= 3 + TOLERANCE_SYS_MEM *= 4 + TOLERANCE_DISK_USAGE *= 3 + +# --- file names + +# Disambiguate TESTFN for parallel testing. +if os.name == 'java': + # Jython disallows @ in module names + TESTFN_PREFIX = '$psutil-%s-' % os.getpid() +else: + TESTFN_PREFIX = '@psutil-%s-' % os.getpid() +UNICODE_SUFFIX = u("-ƒőő") +# An invalid unicode string. +if PY3: + INVALID_UNICODE_SUFFIX = b"f\xc0\x80".decode('utf8', 'surrogateescape') +else: + INVALID_UNICODE_SUFFIX = "f\xc0\x80" +ASCII_FS = sys.getfilesystemencoding().lower() in ('ascii', 'us-ascii') + +# --- paths + +ROOT_DIR = os.path.realpath( + os.path.join(os.path.dirname(__file__), '..', '..') +) +SCRIPTS_DIR = os.environ.get( + "PSUTIL_SCRIPTS_DIR", os.path.join(ROOT_DIR, 'scripts') +) +HERE = os.path.realpath(os.path.dirname(__file__)) + +# --- support + +HAS_CONNECTIONS_UNIX = POSIX and not SUNOS +HAS_CPU_AFFINITY = hasattr(psutil.Process, "cpu_affinity") +HAS_CPU_FREQ = hasattr(psutil, "cpu_freq") +HAS_GETLOADAVG = hasattr(psutil, "getloadavg") +HAS_ENVIRON = hasattr(psutil.Process, "environ") +HAS_IONICE = hasattr(psutil.Process, "ionice") +HAS_MEMORY_MAPS = hasattr(psutil.Process, "memory_maps") +HAS_NET_IO_COUNTERS = hasattr(psutil, "net_io_counters") +HAS_PROC_CPU_NUM = hasattr(psutil.Process, "cpu_num") +HAS_PROC_IO_COUNTERS = hasattr(psutil.Process, "io_counters") +HAS_RLIMIT = hasattr(psutil.Process, "rlimit") +HAS_SENSORS_BATTERY = hasattr(psutil, "sensors_battery") +try: + HAS_BATTERY = HAS_SENSORS_BATTERY and bool(psutil.sensors_battery()) +except Exception: # noqa: BLE001 + HAS_BATTERY = False +HAS_SENSORS_FANS = hasattr(psutil, "sensors_fans") +HAS_SENSORS_TEMPERATURES = hasattr(psutil, "sensors_temperatures") +HAS_THREADS = hasattr(psutil.Process, "threads") +SKIP_SYSCONS = (MACOS or AIX) and os.getuid() != 0 + +# --- misc + + +def _get_py_exe(): + def attempt(exe): + try: + subprocess.check_call( + [exe, "-V"], stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + except subprocess.CalledProcessError: + return None + else: + return exe + + env = os.environ.copy() + + # On Windows, starting with python 3.7, virtual environments use a + # venv launcher startup process. This does not play well when + # counting spawned processes, or when relying on the PID of the + # spawned process to do some checks, e.g. connections check per PID. + # Let's use the base python in this case. + base = getattr(sys, "_base_executable", None) + if WINDOWS and sys.version_info >= (3, 7) and base is not None: + # We need to set __PYVENV_LAUNCHER__ to sys.executable for the + # base python executable to know about the environment. + env["__PYVENV_LAUNCHER__"] = sys.executable + return base, env + elif GITHUB_ACTIONS: + return sys.executable, env + elif MACOS: + exe = ( + attempt(sys.executable) + or attempt(os.path.realpath(sys.executable)) + or attempt(which("python%s.%s" % sys.version_info[:2])) + or attempt(psutil.Process().exe()) + ) + if not exe: + raise ValueError("can't find python exe real abspath") + return exe, env + else: + exe = os.path.realpath(sys.executable) + assert os.path.exists(exe), exe + return exe, env + + +PYTHON_EXE, PYTHON_EXE_ENV = _get_py_exe() +DEVNULL = open(os.devnull, 'r+') +atexit.register(DEVNULL.close) + +VALID_PROC_STATUSES = [ + getattr(psutil, x) for x in dir(psutil) if x.startswith('STATUS_') +] +AF_UNIX = getattr(socket, "AF_UNIX", object()) + +_subprocesses_started = set() +_pids_started = set() + + +# =================================================================== +# --- threads +# =================================================================== + + +class ThreadTask(threading.Thread): + """A thread task which does nothing expect staying alive.""" + + def __init__(self): + super().__init__() + self._running = False + self._interval = 0.001 + self._flag = threading.Event() + + def __repr__(self): + name = self.__class__.__name__ + return '<%s running=%s at %#x>' % (name, self._running, id(self)) + + def __enter__(self): + self.start() + return self + + def __exit__(self, *args, **kwargs): + self.stop() + + def start(self): + """Start thread and keep it running until an explicit + stop() request. Polls for shutdown every 'timeout' seconds. + """ + if self._running: + raise ValueError("already started") + threading.Thread.start(self) + self._flag.wait() + + def run(self): + self._running = True + self._flag.set() + while self._running: + time.sleep(self._interval) + + def stop(self): + """Stop thread execution and and waits until it is stopped.""" + if not self._running: + raise ValueError("already stopped") + self._running = False + self.join() + + +# =================================================================== +# --- subprocesses +# =================================================================== + + +def _reap_children_on_err(fun): + @functools.wraps(fun) + def wrapper(*args, **kwargs): + try: + return fun(*args, **kwargs) + except Exception: + reap_children() + raise + + return wrapper + + +@_reap_children_on_err +def spawn_testproc(cmd=None, **kwds): + """Create a python subprocess which does nothing for some secs and + return it as a subprocess.Popen instance. + If "cmd" is specified that is used instead of python. + By default stdin and stdout are redirected to /dev/null. + It also attempts to make sure the process is in a reasonably + initialized state. + The process is registered for cleanup on reap_children(). + """ + kwds.setdefault("stdin", DEVNULL) + kwds.setdefault("stdout", DEVNULL) + kwds.setdefault("cwd", os.getcwd()) + kwds.setdefault("env", PYTHON_EXE_ENV) + if WINDOWS: + # Prevents the subprocess to open error dialogs. This will also + # cause stderr to be suppressed, which is suboptimal in order + # to debug broken tests. + CREATE_NO_WINDOW = 0x8000000 + kwds.setdefault("creationflags", CREATE_NO_WINDOW) + if cmd is None: + testfn = get_testfn(dir=os.getcwd()) + try: + safe_rmpath(testfn) + pyline = ( + "import time;" + + "open(r'%s', 'w').close();" % testfn + + "[time.sleep(0.1) for x in range(100)];" # 10 secs + ) + cmd = [PYTHON_EXE, "-c", pyline] + sproc = subprocess.Popen(cmd, **kwds) + _subprocesses_started.add(sproc) + wait_for_file(testfn, delete=True, empty=True) + finally: + safe_rmpath(testfn) + else: + sproc = subprocess.Popen(cmd, **kwds) + _subprocesses_started.add(sproc) + wait_for_pid(sproc.pid) + return sproc + + +@_reap_children_on_err +def spawn_children_pair(): + """Create a subprocess which creates another one as in: + A (us) -> B (child) -> C (grandchild). + Return a (child, grandchild) tuple. + The 2 processes are fully initialized and will live for 60 secs + and are registered for cleanup on reap_children(). + """ + tfile = None + testfn = get_testfn(dir=os.getcwd()) + try: + s = textwrap.dedent("""\ + import subprocess, os, sys, time + s = "import os, time;" + s += "f = open('%s', 'w');" + s += "f.write(str(os.getpid()));" + s += "f.close();" + s += "time.sleep(60);" + p = subprocess.Popen([r'%s', '-c', s]) + p.wait() + """ % (os.path.basename(testfn), PYTHON_EXE)) + # On Windows if we create a subprocess with CREATE_NO_WINDOW flag + # set (which is the default) a "conhost.exe" extra process will be + # spawned as a child. We don't want that. + if WINDOWS: + subp, tfile = pyrun(s, creationflags=0) + else: + subp, tfile = pyrun(s) + child = psutil.Process(subp.pid) + grandchild_pid = int(wait_for_file(testfn, delete=True, empty=False)) + _pids_started.add(grandchild_pid) + grandchild = psutil.Process(grandchild_pid) + return (child, grandchild) + finally: + safe_rmpath(testfn) + if tfile is not None: + safe_rmpath(tfile) + + +def spawn_zombie(): + """Create a zombie process and return a (parent, zombie) process tuple. + In order to kill the zombie parent must be terminate()d first, then + zombie must be wait()ed on. + """ + assert psutil.POSIX + unix_file = get_testfn() + src = textwrap.dedent("""\ + import os, sys, time, socket, contextlib + child_pid = os.fork() + if child_pid > 0: + time.sleep(3000) + else: + # this is the zombie process + s = socket.socket(socket.AF_UNIX) + with contextlib.closing(s): + s.connect('%s') + if sys.version_info < (3, ): + pid = str(os.getpid()) + else: + pid = bytes(str(os.getpid()), 'ascii') + s.sendall(pid) + """ % unix_file) + tfile = None + sock = bind_unix_socket(unix_file) + try: + sock.settimeout(GLOBAL_TIMEOUT) + parent, tfile = pyrun(src) + conn, _ = sock.accept() + try: + select.select([conn.fileno()], [], [], GLOBAL_TIMEOUT) + zpid = int(conn.recv(1024)) + _pids_started.add(zpid) + zombie = psutil.Process(zpid) + call_until(zombie.status, "ret == psutil.STATUS_ZOMBIE") + return (parent, zombie) + finally: + conn.close() + finally: + sock.close() + safe_rmpath(unix_file) + if tfile is not None: + safe_rmpath(tfile) + + +@_reap_children_on_err +def pyrun(src, **kwds): + """Run python 'src' code string in a separate interpreter. + Returns a subprocess.Popen instance and the test file where the source + code was written. + """ + kwds.setdefault("stdout", None) + kwds.setdefault("stderr", None) + srcfile = get_testfn() + try: + with open(srcfile, "w") as f: + f.write(src) + subp = spawn_testproc([PYTHON_EXE, f.name], **kwds) + wait_for_pid(subp.pid) + return (subp, srcfile) + except Exception: + safe_rmpath(srcfile) + raise + + +@_reap_children_on_err +def sh(cmd, **kwds): + """Run cmd in a subprocess and return its output. + raises RuntimeError on error. + """ + # Prevents subprocess to open error dialogs in case of error. + flags = 0x8000000 if WINDOWS else 0 + kwds.setdefault("stdout", subprocess.PIPE) + kwds.setdefault("stderr", subprocess.PIPE) + kwds.setdefault("universal_newlines", True) + kwds.setdefault("creationflags", flags) + if isinstance(cmd, str): + cmd = shlex.split(cmd) + p = subprocess.Popen(cmd, **kwds) + _subprocesses_started.add(p) + if PY3: + stdout, stderr = p.communicate(timeout=GLOBAL_TIMEOUT) + else: + stdout, stderr = p.communicate() + if p.returncode != 0: + raise RuntimeError(stderr) + if stderr: + warn(stderr) + if stdout.endswith('\n'): + stdout = stdout[:-1] + return stdout + + +def terminate(proc_or_pid, sig=signal.SIGTERM, wait_timeout=GLOBAL_TIMEOUT): + """Terminate a process and wait() for it. + Process can be a PID or an instance of psutil.Process(), + subprocess.Popen() or psutil.Popen(). + If it's a subprocess.Popen() or psutil.Popen() instance also closes + its stdin / stdout / stderr fds. + PID is wait()ed even if the process is already gone (kills zombies). + Does nothing if the process does not exist. + Return process exit status. + """ + + def wait(proc, timeout): + if isinstance(proc, subprocess.Popen) and not PY3: + proc.wait() + else: + proc.wait(timeout) + if WINDOWS and isinstance(proc, subprocess.Popen): + # Otherwise PID may still hang around. + try: + return psutil.Process(proc.pid).wait(timeout) + except psutil.NoSuchProcess: + pass + + def sendsig(proc, sig): + # XXX: otherwise the build hangs for some reason. + if MACOS and GITHUB_ACTIONS: + sig = signal.SIGKILL + # If the process received SIGSTOP, SIGCONT is necessary first, + # otherwise SIGTERM won't work. + if POSIX and sig != signal.SIGKILL: + proc.send_signal(signal.SIGCONT) + proc.send_signal(sig) + + def term_subprocess_proc(proc, timeout): + try: + sendsig(proc, sig) + except OSError as err: + if WINDOWS and err.winerror == 6: # "invalid handle" + pass + elif err.errno != errno.ESRCH: + raise + return wait(proc, timeout) + + def term_psutil_proc(proc, timeout): + try: + sendsig(proc, sig) + except psutil.NoSuchProcess: + pass + return wait(proc, timeout) + + def term_pid(pid, timeout): + try: + proc = psutil.Process(pid) + except psutil.NoSuchProcess: + # Needed to kill zombies. + if POSIX: + return wait_pid(pid, timeout) + else: + return term_psutil_proc(proc, timeout) + + def flush_popen(proc): + if proc.stdout: + proc.stdout.close() + if proc.stderr: + proc.stderr.close() + # Flushing a BufferedWriter may raise an error. + if proc.stdin: + proc.stdin.close() + + p = proc_or_pid + try: + if isinstance(p, int): + return term_pid(p, wait_timeout) + elif isinstance(p, (psutil.Process, psutil.Popen)): + return term_psutil_proc(p, wait_timeout) + elif isinstance(p, subprocess.Popen): + return term_subprocess_proc(p, wait_timeout) + else: + raise TypeError("wrong type %r" % p) + finally: + if isinstance(p, (subprocess.Popen, psutil.Popen)): + flush_popen(p) + pid = p if isinstance(p, int) else p.pid + assert not psutil.pid_exists(pid), pid + + +def reap_children(recursive=False): + """Terminate and wait() any subprocess started by this test suite + and any children currently running, ensuring that no processes stick + around to hog resources. + If recursive is True it also tries to terminate and wait() + all grandchildren started by this process. + """ + # Get the children here before terminating them, as in case of + # recursive=True we don't want to lose the intermediate reference + # pointing to the grandchildren. + children = psutil.Process().children(recursive=recursive) + + # Terminate subprocess.Popen. + while _subprocesses_started: + subp = _subprocesses_started.pop() + terminate(subp) + + # Collect started pids. + while _pids_started: + pid = _pids_started.pop() + terminate(pid) + + # Terminate children. + if children: + for p in children: + terminate(p, wait_timeout=None) + _, alive = psutil.wait_procs(children, timeout=GLOBAL_TIMEOUT) + for p in alive: + warn("couldn't terminate process %r; attempting kill()" % p) + terminate(p, sig=signal.SIGKILL) + + +# =================================================================== +# --- OS +# =================================================================== + + +def kernel_version(): + """Return a tuple such as (2, 6, 36).""" + if not POSIX: + raise NotImplementedError("not POSIX") + s = "" + uname = os.uname()[2] + for c in uname: + if c.isdigit() or c == '.': + s += c + else: + break + if not s: + raise ValueError("can't parse %r" % uname) + minor = 0 + micro = 0 + nums = s.split('.') + major = int(nums[0]) + if len(nums) >= 2: + minor = int(nums[1]) + if len(nums) >= 3: + micro = int(nums[2]) + return (major, minor, micro) + + +def get_winver(): + if not WINDOWS: + raise NotImplementedError("not WINDOWS") + wv = sys.getwindowsversion() + if hasattr(wv, 'service_pack_major'): # python >= 2.7 + sp = wv.service_pack_major or 0 + else: + r = re.search(r"\s\d$", wv[4]) + sp = int(r.group(0)) if r else 0 + return (wv[0], wv[1], sp) + + +# =================================================================== +# --- sync primitives +# =================================================================== + + +class retry: + """A retry decorator.""" + + def __init__( + self, + exception=Exception, + timeout=None, + retries=None, + interval=0.001, + logfun=None, + ): + if timeout and retries: + raise ValueError("timeout and retries args are mutually exclusive") + self.exception = exception + self.timeout = timeout + self.retries = retries + self.interval = interval + self.logfun = logfun + + def __iter__(self): + if self.timeout: + stop_at = time.time() + self.timeout + while time.time() < stop_at: + yield + elif self.retries: + for _ in range(self.retries): + yield + else: + while True: + yield + + def sleep(self): + if self.interval is not None: + time.sleep(self.interval) + + def __call__(self, fun): + @functools.wraps(fun) + def wrapper(*args, **kwargs): + exc = None + for _ in self: + try: + return fun(*args, **kwargs) + except self.exception as _: # NOQA + exc = _ + if self.logfun is not None: + self.logfun(exc) + self.sleep() + continue + if PY3: + raise exc + else: + raise + + # This way the user of the decorated function can change config + # parameters. + wrapper.decorator = self + return wrapper + + +@retry( + exception=psutil.NoSuchProcess, + logfun=None, + timeout=GLOBAL_TIMEOUT, + interval=0.001, +) +def wait_for_pid(pid): + """Wait for pid to show up in the process list then return. + Used in the test suite to give time the sub process to initialize. + """ + if pid not in psutil.pids(): + raise psutil.NoSuchProcess(pid) + psutil.Process(pid) + + +@retry( + exception=(FileNotFoundError, AssertionError), + logfun=None, + timeout=GLOBAL_TIMEOUT, + interval=0.001, +) +def wait_for_file(fname, delete=True, empty=False): + """Wait for a file to be written on disk with some content.""" + with open(fname, "rb") as f: + data = f.read() + if not empty: + assert data + if delete: + safe_rmpath(fname) + return data + + +@retry( + exception=AssertionError, + logfun=None, + timeout=GLOBAL_TIMEOUT, + interval=0.001, +) +def call_until(fun, expr): + """Keep calling function for timeout secs and exit if eval() + expression is True. + """ + ret = fun() + assert eval(expr) # noqa + return ret + + +# =================================================================== +# --- fs +# =================================================================== + + +def safe_rmpath(path): + """Convenience function for removing temporary test files or dirs.""" + + def retry_fun(fun): + # On Windows it could happen that the file or directory has + # open handles or references preventing the delete operation + # to succeed immediately, so we retry for a while. See: + # https://bugs.python.org/issue33240 + stop_at = time.time() + GLOBAL_TIMEOUT + while time.time() < stop_at: + try: + return fun() + except FileNotFoundError: + pass + except WindowsError as _: + err = _ + warn("ignoring %s" % (str(err))) + time.sleep(0.01) + raise err + + try: + st = os.stat(path) + if stat.S_ISDIR(st.st_mode): + fun = functools.partial(shutil.rmtree, path) + else: + fun = functools.partial(os.remove, path) + if POSIX: + fun() + else: + retry_fun(fun) + except FileNotFoundError: + pass + + +def safe_mkdir(dir): + """Convenience function for creating a directory.""" + try: + os.mkdir(dir) + except FileExistsError: + pass + + +@contextlib.contextmanager +def chdir(dirname): + """Context manager which temporarily changes the current directory.""" + curdir = os.getcwd() + try: + os.chdir(dirname) + yield + finally: + os.chdir(curdir) + + +def create_py_exe(path): + """Create a Python executable file in the given location.""" + assert not os.path.exists(path), path + atexit.register(safe_rmpath, path) + shutil.copyfile(PYTHON_EXE, path) + if POSIX: + st = os.stat(path) + os.chmod(path, st.st_mode | stat.S_IEXEC) + return path + + +def create_c_exe(path, c_code=None): + """Create a compiled C executable in the given location.""" + assert not os.path.exists(path), path + if not which("gcc"): + raise unittest.SkipTest("gcc is not installed") + if c_code is None: + c_code = textwrap.dedent(""" + #include + int main() { + pause(); + return 1; + } + """) + else: + assert isinstance(c_code, str), c_code + + atexit.register(safe_rmpath, path) + with open(get_testfn(suffix='.c'), "w") as f: + f.write(c_code) + try: + subprocess.check_call(["gcc", f.name, "-o", path]) + finally: + safe_rmpath(f.name) + return path + + +def get_testfn(suffix="", dir=None): + """Return an absolute pathname of a file or dir that did not + exist at the time this call is made. Also schedule it for safe + deletion at interpreter exit. It's technically racy but probably + not really due to the time variant. + """ + while True: + name = tempfile.mktemp(prefix=TESTFN_PREFIX, suffix=suffix, dir=dir) + if not os.path.exists(name): # also include dirs + path = os.path.realpath(name) # needed for OSX + atexit.register(safe_rmpath, path) + return path + + +# =================================================================== +# --- testing +# =================================================================== + + +class TestCase(unittest.TestCase): + + # Print a full path representation of the single unit tests + # being run. + def __str__(self): + fqmod = self.__class__.__module__ + if not fqmod.startswith('psutil.'): + fqmod = 'psutil.tests.' + fqmod + return "%s.%s.%s" % ( + fqmod, + self.__class__.__name__, + self._testMethodName, + ) + + # assertRaisesRegexp renamed to assertRaisesRegex in 3.3; + # add support for the new name. + if not hasattr(unittest.TestCase, 'assertRaisesRegex'): + assertRaisesRegex = unittest.TestCase.assertRaisesRegexp # noqa + + # ...otherwise multiprocessing.Pool complains + if not PY3: + + def runTest(self): + pass + + @contextlib.contextmanager + def subTest(self, *args, **kw): + # fake it for python 2.7 + yield + + +# monkey patch default unittest.TestCase +unittest.TestCase = TestCase + + +class PsutilTestCase(TestCase): + """Test class providing auto-cleanup wrappers on top of process + test utilities. + """ + + def get_testfn(self, suffix="", dir=None): + fname = get_testfn(suffix=suffix, dir=dir) + self.addCleanup(safe_rmpath, fname) + return fname + + def spawn_testproc(self, *args, **kwds): + sproc = spawn_testproc(*args, **kwds) + self.addCleanup(terminate, sproc) + return sproc + + def spawn_children_pair(self): + child1, child2 = spawn_children_pair() + self.addCleanup(terminate, child2) + self.addCleanup(terminate, child1) # executed first + return (child1, child2) + + def spawn_zombie(self): + parent, zombie = spawn_zombie() + self.addCleanup(terminate, zombie) + self.addCleanup(terminate, parent) # executed first + return (parent, zombie) + + def pyrun(self, *args, **kwds): + sproc, srcfile = pyrun(*args, **kwds) + self.addCleanup(safe_rmpath, srcfile) + self.addCleanup(terminate, sproc) # executed first + return sproc + + def _check_proc_exc(self, proc, exc): + self.assertIsInstance(exc, psutil.Error) + self.assertEqual(exc.pid, proc.pid) + self.assertEqual(exc.name, proc._name) + if exc.name: + self.assertNotEqual(exc.name, "") + if isinstance(exc, psutil.ZombieProcess): + self.assertEqual(exc.ppid, proc._ppid) + if exc.ppid is not None: + self.assertGreaterEqual(exc.ppid, 0) + str(exc) + repr(exc) + + def assertPidGone(self, pid): + with self.assertRaises(psutil.NoSuchProcess) as cm: + try: + psutil.Process(pid) + except psutil.ZombieProcess: + raise AssertionError("wasn't supposed to raise ZombieProcess") + self.assertEqual(cm.exception.pid, pid) + self.assertEqual(cm.exception.name, None) + assert not psutil.pid_exists(pid), pid + self.assertNotIn(pid, psutil.pids()) + self.assertNotIn(pid, [x.pid for x in psutil.process_iter()]) + + def assertProcessGone(self, proc): + self.assertPidGone(proc.pid) + ns = process_namespace(proc) + for fun, name in ns.iter(ns.all, clear_cache=True): + with self.subTest(proc=proc, name=name): + try: + ret = fun() + except psutil.ZombieProcess: + raise + except psutil.NoSuchProcess as exc: + self._check_proc_exc(proc, exc) + else: + msg = "Process.%s() didn't raise NSP and returned %r" % ( + name, + ret, + ) + raise AssertionError(msg) + proc.wait(timeout=0) # assert not raise TimeoutExpired + + def assertProcessZombie(self, proc): + # A zombie process should always be instantiable. + clone = psutil.Process(proc.pid) + # Cloned zombie on Open/NetBSD has null creation time, see: + # https://github.com/giampaolo/psutil/issues/2287 + self.assertEqual(proc, clone) + if not (OPENBSD or NETBSD): + self.assertEqual(hash(proc), hash(clone)) + # Its status always be querable. + self.assertEqual(proc.status(), psutil.STATUS_ZOMBIE) + # It should be considered 'running'. + assert proc.is_running() + assert psutil.pid_exists(proc.pid) + # as_dict() shouldn't crash. + proc.as_dict() + # It should show up in pids() and process_iter(). + self.assertIn(proc.pid, psutil.pids()) + self.assertIn(proc.pid, [x.pid for x in psutil.process_iter()]) + psutil._pmap = {} + self.assertIn(proc.pid, [x.pid for x in psutil.process_iter()]) + # Call all methods. + ns = process_namespace(proc) + for fun, name in ns.iter(ns.all, clear_cache=True): + with self.subTest(proc=proc, name=name): + try: + fun() + except (psutil.ZombieProcess, psutil.AccessDenied) as exc: + self._check_proc_exc(proc, exc) + if LINUX: + # https://github.com/giampaolo/psutil/pull/2288 + with self.assertRaises(psutil.ZombieProcess) as cm: + proc.cmdline() + self._check_proc_exc(proc, cm.exception) + with self.assertRaises(psutil.ZombieProcess) as cm: + proc.exe() + self._check_proc_exc(proc, cm.exception) + with self.assertRaises(psutil.ZombieProcess) as cm: + proc.memory_maps() + self._check_proc_exc(proc, cm.exception) + # Zombie cannot be signaled or terminated. + proc.suspend() + proc.resume() + proc.terminate() + proc.kill() + assert proc.is_running() + assert psutil.pid_exists(proc.pid) + self.assertIn(proc.pid, psutil.pids()) + self.assertIn(proc.pid, [x.pid for x in psutil.process_iter()]) + psutil._pmap = {} + self.assertIn(proc.pid, [x.pid for x in psutil.process_iter()]) + + # Its parent should 'see' it (edit: not true on BSD and MACOS). + # descendants = [x.pid for x in psutil.Process().children( + # recursive=True)] + # self.assertIn(proc.pid, descendants) + + # __eq__ can't be relied upon because creation time may not be + # querable. + # self.assertEqual(proc, psutil.Process(proc.pid)) + + # XXX should we also assume ppid() to be usable? Note: this + # would be an important use case as the only way to get + # rid of a zombie is to kill its parent. + # self.assertEqual(proc.ppid(), os.getpid()) + + +@unittest.skipIf(PYPY, "unreliable on PYPY") +class TestMemoryLeak(PsutilTestCase): + """Test framework class for detecting function memory leaks, + typically functions implemented in C which forgot to free() memory + from the heap. It does so by checking whether the process memory + usage increased before and after calling the function many times. + + Note that this is hard (probably impossible) to do reliably, due + to how the OS handles memory, the GC and so on (memory can even + decrease!). In order to avoid false positives, in case of failure + (mem > 0) we retry the test for up to 5 times, increasing call + repetitions each time. If the memory keeps increasing then it's a + failure. + + If available (Linux, OSX, Windows), USS memory is used for comparison, + since it's supposed to be more precise, see: + https://gmpy.dev/blog/2016/real-process-memory-and-environ-in-python + If not, RSS memory is used. mallinfo() on Linux and _heapwalk() on + Windows may give even more precision, but at the moment are not + implemented. + + PyPy appears to be completely unstable for this framework, probably + because of its JIT, so tests on PYPY are skipped. + + Usage: + + class TestLeaks(psutil.tests.TestMemoryLeak): + + def test_fun(self): + self.execute(some_function) + """ + + # Configurable class attrs. + times = 200 + warmup_times = 10 + tolerance = 0 # memory + retries = 10 if CI_TESTING else 5 + verbose = True + _thisproc = psutil.Process() + _psutil_debug_orig = bool(os.getenv('PSUTIL_DEBUG')) + + @classmethod + def setUpClass(cls): + psutil._set_debug(False) # avoid spamming to stderr + + @classmethod + def tearDownClass(cls): + psutil._set_debug(cls._psutil_debug_orig) + + def _get_mem(self): + # USS is the closest thing we have to "real" memory usage and it + # should be less likely to produce false positives. + mem = self._thisproc.memory_full_info() + return getattr(mem, "uss", mem.rss) + + def _get_num_fds(self): + if POSIX: + return self._thisproc.num_fds() + else: + return self._thisproc.num_handles() + + def _log(self, msg): + if self.verbose: + print_color(msg, color="yellow", file=sys.stderr) + + def _check_fds(self, fun): + """Makes sure num_fds() (POSIX) or num_handles() (Windows) does + not increase after calling a function. Used to discover forgotten + close(2) and CloseHandle syscalls. + """ + before = self._get_num_fds() + self.call(fun) + after = self._get_num_fds() + diff = after - before + if diff < 0: + raise self.fail( + "negative diff %r (gc probably collected a " + "resource from a previous test)" % diff + ) + if diff > 0: + type_ = "fd" if POSIX else "handle" + if diff > 1: + type_ += "s" + msg = "%s unclosed %s after calling %r" % (diff, type_, fun) + raise self.fail(msg) + + def _call_ntimes(self, fun, times): + """Get 2 distinct memory samples, before and after having + called fun repeatedly, and return the memory difference. + """ + gc.collect(generation=1) + mem1 = self._get_mem() + for x in range(times): + ret = self.call(fun) + del x, ret + gc.collect(generation=1) + mem2 = self._get_mem() + self.assertEqual(gc.garbage, []) + diff = mem2 - mem1 # can also be negative + return diff + + def _check_mem(self, fun, times, retries, tolerance): + messages = [] + prev_mem = 0 + increase = times + for idx in range(1, retries + 1): + mem = self._call_ntimes(fun, times) + msg = "Run #%s: extra-mem=%s, per-call=%s, calls=%s" % ( + idx, + bytes2human(mem), + bytes2human(mem / times), + times, + ) + messages.append(msg) + success = mem <= tolerance or mem <= prev_mem + if success: + if idx > 1: + self._log(msg) + return + else: + if idx == 1: + print() # NOQA + self._log(msg) + times += increase + prev_mem = mem + raise self.fail(". ".join(messages)) + + # --- + + def call(self, fun): + return fun() + + def execute( + self, fun, times=None, warmup_times=None, retries=None, tolerance=None + ): + """Test a callable.""" + times = times if times is not None else self.times + warmup_times = ( + warmup_times if warmup_times is not None else self.warmup_times + ) + retries = retries if retries is not None else self.retries + tolerance = tolerance if tolerance is not None else self.tolerance + try: + assert times >= 1, "times must be >= 1" + assert warmup_times >= 0, "warmup_times must be >= 0" + assert retries >= 0, "retries must be >= 0" + assert tolerance >= 0, "tolerance must be >= 0" + except AssertionError as err: + raise ValueError(str(err)) + + self._call_ntimes(fun, warmup_times) # warm up + self._check_fds(fun) + self._check_mem(fun, times=times, retries=retries, tolerance=tolerance) + + def execute_w_exc(self, exc, fun, **kwargs): + """Convenience method to test a callable while making sure it + raises an exception on every call. + """ + + def call(): + self.assertRaises(exc, fun) + + self.execute(call, **kwargs) + + +def print_sysinfo(): + import collections + import datetime + import getpass + import locale + import pprint + + try: + import pip + except ImportError: + pip = None + try: + import wheel + except ImportError: + wheel = None + + info = collections.OrderedDict() + + # OS + if psutil.LINUX and which('lsb_release'): + info['OS'] = sh('lsb_release -d -s') + elif psutil.OSX: + info['OS'] = 'Darwin %s' % platform.mac_ver()[0] + elif psutil.WINDOWS: + info['OS'] = "Windows " + ' '.join(map(str, platform.win32_ver())) + if hasattr(platform, 'win32_edition'): + info['OS'] += ", " + platform.win32_edition() + else: + info['OS'] = "%s %s" % (platform.system(), platform.version()) + info['arch'] = ', '.join( + list(platform.architecture()) + [platform.machine()] + ) + if psutil.POSIX: + info['kernel'] = platform.uname()[2] + + # python + info['python'] = ', '.join([ + platform.python_implementation(), + platform.python_version(), + platform.python_compiler(), + ]) + info['pip'] = getattr(pip, '__version__', 'not installed') + if wheel is not None: + info['pip'] += " (wheel=%s)" % wheel.__version__ + + # UNIX + if psutil.POSIX: + if which('gcc'): + out = sh(['gcc', '--version']) + info['gcc'] = str(out).split('\n')[0] + else: + info['gcc'] = 'not installed' + s = platform.libc_ver()[1] + if s: + info['glibc'] = s + + # system + info['fs-encoding'] = sys.getfilesystemencoding() + lang = locale.getlocale() + info['lang'] = '%s, %s' % (lang[0], lang[1]) + info['boot-time'] = datetime.datetime.fromtimestamp( + psutil.boot_time() + ).strftime("%Y-%m-%d %H:%M:%S") + info['time'] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + info['user'] = getpass.getuser() + info['home'] = os.path.expanduser("~") + info['cwd'] = os.getcwd() + info['pyexe'] = PYTHON_EXE + info['hostname'] = platform.node() + info['PID'] = os.getpid() + + # metrics + info['cpus'] = psutil.cpu_count() + info['loadavg'] = "%.1f%%, %.1f%%, %.1f%%" % ( + tuple([x / psutil.cpu_count() * 100 for x in psutil.getloadavg()]) + ) + mem = psutil.virtual_memory() + info['memory'] = "%s%%, used=%s, total=%s" % ( + int(mem.percent), + bytes2human(mem.used), + bytes2human(mem.total), + ) + swap = psutil.swap_memory() + info['swap'] = "%s%%, used=%s, total=%s" % ( + int(swap.percent), + bytes2human(swap.used), + bytes2human(swap.total), + ) + info['pids'] = len(psutil.pids()) + pinfo = psutil.Process().as_dict() + pinfo.pop('memory_maps', None) + info['proc'] = pprint.pformat(pinfo) + + print("=" * 70, file=sys.stderr) # NOQA + for k, v in info.items(): + print("%-17s %s" % (k + ':', v), file=sys.stderr) # NOQA + print("=" * 70, file=sys.stderr) # NOQA + sys.stdout.flush() + + if WINDOWS: + os.system("tasklist") + elif which("ps"): + os.system("ps aux") + print("=" * 70, file=sys.stderr) # NOQA + sys.stdout.flush() + + +def is_win_secure_system_proc(pid): + # see: https://github.com/giampaolo/psutil/issues/2338 + @memoize + def get_procs(): + ret = {} + out = sh("tasklist.exe /NH /FO csv") + for line in out.splitlines()[1:]: + bits = [x.replace('"', "") for x in line.split(",")] + name, pid = bits[0], int(bits[1]) + ret[pid] = name + return ret + + try: + return get_procs()[pid] == "Secure System" + except KeyError: + return False + + +def _get_eligible_cpu(): + p = psutil.Process() + if hasattr(p, "cpu_num"): + return p.cpu_num() + elif hasattr(p, "cpu_affinity"): + return random.choice(p.cpu_affinity()) + return 0 + + +class process_namespace: + """A container that lists all Process class method names + some + reasonable parameters to be called with. Utility methods (parent(), + children(), ...) are excluded. + + >>> ns = process_namespace(psutil.Process()) + >>> for fun, name in ns.iter(ns.getters): + ... fun() + """ + + utils = [('cpu_percent', (), {}), ('memory_percent', (), {})] + + ignored = [ + ('as_dict', (), {}), + ('children', (), {'recursive': True}), + ('is_running', (), {}), + ('memory_info_ex', (), {}), + ('oneshot', (), {}), + ('parent', (), {}), + ('parents', (), {}), + ('pid', (), {}), + ('wait', (0,), {}), + ] + + getters = [ + ('cmdline', (), {}), + ('connections', (), {'kind': 'all'}), + ('cpu_times', (), {}), + ('create_time', (), {}), + ('cwd', (), {}), + ('exe', (), {}), + ('memory_full_info', (), {}), + ('memory_info', (), {}), + ('name', (), {}), + ('nice', (), {}), + ('num_ctx_switches', (), {}), + ('num_threads', (), {}), + ('open_files', (), {}), + ('ppid', (), {}), + ('status', (), {}), + ('threads', (), {}), + ('username', (), {}), + ] + if POSIX: + getters += [('uids', (), {})] + getters += [('gids', (), {})] + getters += [('terminal', (), {})] + getters += [('num_fds', (), {})] + if HAS_PROC_IO_COUNTERS: + getters += [('io_counters', (), {})] + if HAS_IONICE: + getters += [('ionice', (), {})] + if HAS_RLIMIT: + getters += [('rlimit', (psutil.RLIMIT_NOFILE,), {})] + if HAS_CPU_AFFINITY: + getters += [('cpu_affinity', (), {})] + if HAS_PROC_CPU_NUM: + getters += [('cpu_num', (), {})] + if HAS_ENVIRON: + getters += [('environ', (), {})] + if WINDOWS: + getters += [('num_handles', (), {})] + if HAS_MEMORY_MAPS: + getters += [('memory_maps', (), {'grouped': False})] + + setters = [] + if POSIX: + setters += [('nice', (0,), {})] + else: + setters += [('nice', (psutil.NORMAL_PRIORITY_CLASS,), {})] + if HAS_RLIMIT: + setters += [('rlimit', (psutil.RLIMIT_NOFILE, (1024, 4096)), {})] + if HAS_IONICE: + if LINUX: + setters += [('ionice', (psutil.IOPRIO_CLASS_NONE, 0), {})] + else: + setters += [('ionice', (psutil.IOPRIO_NORMAL,), {})] + if HAS_CPU_AFFINITY: + setters += [('cpu_affinity', ([_get_eligible_cpu()],), {})] + + killers = [ + ('send_signal', (signal.SIGTERM,), {}), + ('suspend', (), {}), + ('resume', (), {}), + ('terminate', (), {}), + ('kill', (), {}), + ] + if WINDOWS: + killers += [('send_signal', (signal.CTRL_C_EVENT,), {})] + killers += [('send_signal', (signal.CTRL_BREAK_EVENT,), {})] + + all = utils + getters + setters + killers + + def __init__(self, proc): + self._proc = proc + + def iter(self, ls, clear_cache=True): + """Given a list of tuples yields a set of (fun, fun_name) tuples + in random order. + """ + ls = list(ls) + random.shuffle(ls) + for fun_name, args, kwds in ls: + if clear_cache: + self.clear_cache() + fun = getattr(self._proc, fun_name) + fun = functools.partial(fun, *args, **kwds) + yield (fun, fun_name) + + def clear_cache(self): + """Clear the cache of a Process instance.""" + self._proc._init(self._proc.pid, _ignore_nsp=True) + + @classmethod + def test_class_coverage(cls, test_class, ls): + """Given a TestCase instance and a list of tuples checks that + the class defines the required test method names. + """ + for fun_name, _, _ in ls: + meth_name = 'test_' + fun_name + if not hasattr(test_class, meth_name): + msg = "%r class should define a '%s' method" % ( + test_class.__class__.__name__, + meth_name, + ) + raise AttributeError(msg) + + @classmethod + def test(cls): + this = set([x[0] for x in cls.all]) + ignored = set([x[0] for x in cls.ignored]) + klass = set([x for x in dir(psutil.Process) if x[0] != '_']) + leftout = (this | ignored) ^ klass + if leftout: + raise ValueError("uncovered Process class names: %r" % leftout) + + +class system_namespace: + """A container that lists all the module-level, system-related APIs. + Utilities such as cpu_percent() are excluded. Usage: + + >>> ns = system_namespace + >>> for fun, name in ns.iter(ns.getters): + ... fun() + """ + + getters = [ + ('boot_time', (), {}), + ('cpu_count', (), {'logical': False}), + ('cpu_count', (), {'logical': True}), + ('cpu_stats', (), {}), + ('cpu_times', (), {'percpu': False}), + ('cpu_times', (), {'percpu': True}), + ('disk_io_counters', (), {'perdisk': True}), + ('disk_partitions', (), {'all': True}), + ('disk_usage', (os.getcwd(),), {}), + ('net_connections', (), {'kind': 'all'}), + ('net_if_addrs', (), {}), + ('net_if_stats', (), {}), + ('net_io_counters', (), {'pernic': True}), + ('pid_exists', (os.getpid(),), {}), + ('pids', (), {}), + ('swap_memory', (), {}), + ('users', (), {}), + ('virtual_memory', (), {}), + ] + if HAS_CPU_FREQ: + getters += [('cpu_freq', (), {'percpu': True})] + if HAS_GETLOADAVG: + getters += [('getloadavg', (), {})] + if HAS_SENSORS_TEMPERATURES: + getters += [('sensors_temperatures', (), {})] + if HAS_SENSORS_FANS: + getters += [('sensors_fans', (), {})] + if HAS_SENSORS_BATTERY: + getters += [('sensors_battery', (), {})] + if WINDOWS: + getters += [('win_service_iter', (), {})] + getters += [('win_service_get', ('alg',), {})] + + ignored = [ + ('process_iter', (), {}), + ('wait_procs', ([psutil.Process()],), {}), + ('cpu_percent', (), {}), + ('cpu_times_percent', (), {}), + ] + + all = getters + + @staticmethod + def iter(ls): + """Given a list of tuples yields a set of (fun, fun_name) tuples + in random order. + """ + ls = list(ls) + random.shuffle(ls) + for fun_name, args, kwds in ls: + fun = getattr(psutil, fun_name) + fun = functools.partial(fun, *args, **kwds) + yield (fun, fun_name) + + test_class_coverage = process_namespace.test_class_coverage + + +def serialrun(klass): + """A decorator to mark a TestCase class. When running parallel tests, + class' unit tests will be run serially (1 process). + """ + # assert issubclass(klass, unittest.TestCase), klass + assert inspect.isclass(klass), klass + klass._serialrun = True + return klass + + +def retry_on_failure(retries=NO_RETRIES): + """Decorator which runs a test function and retries N times before + actually failing. + """ + + def logfun(exc): + print("%r, retrying" % exc, file=sys.stderr) # NOQA + + return retry( + exception=AssertionError, timeout=None, retries=retries, logfun=logfun + ) + + +def skip_on_access_denied(only_if=None): + """Decorator to Ignore AccessDenied exceptions.""" + + def decorator(fun): + @functools.wraps(fun) + def wrapper(*args, **kwargs): + try: + return fun(*args, **kwargs) + except psutil.AccessDenied: + if only_if is not None: + if not only_if: + raise + raise unittest.SkipTest("raises AccessDenied") + + return wrapper + + return decorator + + +def skip_on_not_implemented(only_if=None): + """Decorator to Ignore NotImplementedError exceptions.""" + + def decorator(fun): + @functools.wraps(fun) + def wrapper(*args, **kwargs): + try: + return fun(*args, **kwargs) + except NotImplementedError: + if only_if is not None: + if not only_if: + raise + msg = ( + "%r was skipped because it raised NotImplementedError" + % fun.__name__ + ) + raise unittest.SkipTest(msg) + + return wrapper + + return decorator + + +# =================================================================== +# --- network +# =================================================================== + + +# XXX: no longer used +def get_free_port(host='127.0.0.1'): + """Return an unused TCP port. Subject to race conditions.""" + with contextlib.closing(socket.socket()) as sock: + sock.bind((host, 0)) + return sock.getsockname()[1] + + +def bind_socket(family=AF_INET, type=SOCK_STREAM, addr=None): + """Binds a generic socket.""" + if addr is None and family in (AF_INET, AF_INET6): + addr = ("", 0) + sock = socket.socket(family, type) + try: + if os.name not in ('nt', 'cygwin'): + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(addr) + if type == socket.SOCK_STREAM: + sock.listen(5) + return sock + except Exception: + sock.close() + raise + + +def bind_unix_socket(name, type=socket.SOCK_STREAM): + """Bind a UNIX socket.""" + assert psutil.POSIX + assert not os.path.exists(name), name + sock = socket.socket(socket.AF_UNIX, type) + try: + sock.bind(name) + if type == socket.SOCK_STREAM: + sock.listen(5) + except Exception: + sock.close() + raise + return sock + + +def tcp_socketpair(family, addr=("", 0)): + """Build a pair of TCP sockets connected to each other. + Return a (server, client) tuple. + """ + with contextlib.closing(socket.socket(family, SOCK_STREAM)) as ll: + ll.bind(addr) + ll.listen(5) + addr = ll.getsockname() + c = socket.socket(family, SOCK_STREAM) + try: + c.connect(addr) + caddr = c.getsockname() + while True: + a, addr = ll.accept() + # check that we've got the correct client + if addr == caddr: + return (a, c) + a.close() + except OSError: + c.close() + raise + + +def unix_socketpair(name): + """Build a pair of UNIX sockets connected to each other through + the same UNIX file name. + Return a (server, client) tuple. + """ + assert psutil.POSIX + server = client = None + try: + server = bind_unix_socket(name, type=socket.SOCK_STREAM) + server.setblocking(0) + client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + client.setblocking(0) + client.connect(name) + # new = server.accept() + except Exception: + if server is not None: + server.close() + if client is not None: + client.close() + raise + return (server, client) + + +@contextlib.contextmanager +def create_sockets(): + """Open as many socket families / types as possible.""" + socks = [] + fname1 = fname2 = None + try: + socks.append(bind_socket(socket.AF_INET, socket.SOCK_STREAM)) + socks.append(bind_socket(socket.AF_INET, socket.SOCK_DGRAM)) + if supports_ipv6(): + socks.append(bind_socket(socket.AF_INET6, socket.SOCK_STREAM)) + socks.append(bind_socket(socket.AF_INET6, socket.SOCK_DGRAM)) + if POSIX and HAS_CONNECTIONS_UNIX: + fname1 = get_testfn() + fname2 = get_testfn() + s1, s2 = unix_socketpair(fname1) + s3 = bind_unix_socket(fname2, type=socket.SOCK_DGRAM) + for s in (s1, s2, s3): + socks.append(s) + yield socks + finally: + for s in socks: + s.close() + for fname in (fname1, fname2): + if fname is not None: + safe_rmpath(fname) + + +def check_net_address(addr, family): + """Check a net address validity. Supported families are IPv4, + IPv6 and MAC addresses. + """ + import ipaddress # python >= 3.3 / requires "pip install ipaddress" + + if enum and PY3 and not PYPY: + assert isinstance(family, enum.IntEnum), family + if family == socket.AF_INET: + octs = [int(x) for x in addr.split('.')] + assert len(octs) == 4, addr + for num in octs: + assert 0 <= num <= 255, addr + if not PY3: + addr = unicode(addr) + ipaddress.IPv4Address(addr) + elif family == socket.AF_INET6: + assert isinstance(addr, str), addr + if not PY3: + addr = unicode(addr) + ipaddress.IPv6Address(addr) + elif family == psutil.AF_LINK: + assert re.match(r'([a-fA-F0-9]{2}[:|\-]?){6}', addr) is not None, addr + else: + raise ValueError("unknown family %r" % family) + + +def check_connection_ntuple(conn): + """Check validity of a connection namedtuple.""" + + def check_ntuple(conn): + has_pid = len(conn) == 7 + assert len(conn) in (6, 7), len(conn) + assert conn[0] == conn.fd, conn.fd + assert conn[1] == conn.family, conn.family + assert conn[2] == conn.type, conn.type + assert conn[3] == conn.laddr, conn.laddr + assert conn[4] == conn.raddr, conn.raddr + assert conn[5] == conn.status, conn.status + if has_pid: + assert conn[6] == conn.pid, conn.pid + + def check_family(conn): + assert conn.family in (AF_INET, AF_INET6, AF_UNIX), conn.family + if enum is not None: + assert isinstance(conn.family, enum.IntEnum), conn + else: + assert isinstance(conn.family, int), conn + if conn.family == AF_INET: + # actually try to bind the local socket; ignore IPv6 + # sockets as their address might be represented as + # an IPv4-mapped-address (e.g. "::127.0.0.1") + # and that's rejected by bind() + s = socket.socket(conn.family, conn.type) + with contextlib.closing(s): + try: + s.bind((conn.laddr[0], 0)) + except socket.error as err: + if err.errno != errno.EADDRNOTAVAIL: + raise + elif conn.family == AF_UNIX: + assert conn.status == psutil.CONN_NONE, conn.status + + def check_type(conn): + # SOCK_SEQPACKET may happen in case of AF_UNIX socks + SOCK_SEQPACKET = getattr(socket, "SOCK_SEQPACKET", object()) + assert conn.type in ( + socket.SOCK_STREAM, + socket.SOCK_DGRAM, + SOCK_SEQPACKET, + ), conn.type + if enum is not None: + assert isinstance(conn.type, enum.IntEnum), conn + else: + assert isinstance(conn.type, int), conn + if conn.type == socket.SOCK_DGRAM: + assert conn.status == psutil.CONN_NONE, conn.status + + def check_addrs(conn): + # check IP address and port sanity + for addr in (conn.laddr, conn.raddr): + if conn.family in (AF_INET, AF_INET6): + assert isinstance(addr, tuple), type(addr) + if not addr: + continue + assert isinstance(addr.port, int), type(addr.port) + assert 0 <= addr.port <= 65535, addr.port + check_net_address(addr.ip, conn.family) + elif conn.family == AF_UNIX: + assert isinstance(addr, str), type(addr) + + def check_status(conn): + assert isinstance(conn.status, str), conn.status + valids = [ + getattr(psutil, x) for x in dir(psutil) if x.startswith('CONN_') + ] + assert conn.status in valids, conn.status + if conn.family in (AF_INET, AF_INET6) and conn.type == SOCK_STREAM: + assert conn.status != psutil.CONN_NONE, conn.status + else: + assert conn.status == psutil.CONN_NONE, conn.status + + check_ntuple(conn) + check_family(conn) + check_type(conn) + check_addrs(conn) + check_status(conn) + + +def filter_proc_connections(cons): + """Our process may start with some open UNIX sockets which are not + initialized by us, invalidating unit tests. + """ + new = [] + for conn in cons: + if POSIX and conn.family == socket.AF_UNIX: + if MACOS and "/syslog" in conn.raddr: + debug("skipping %s" % str(conn)) + continue + new.append(conn) + return new + + +# =================================================================== +# --- compatibility +# =================================================================== + + +def reload_module(module): + """Backport of importlib.reload of Python 3.3+.""" + try: + import importlib + + if not hasattr(importlib, 'reload'): # python <=3.3 + raise ImportError + except ImportError: + import imp + + return imp.reload(module) + else: + return importlib.reload(module) + + +def import_module_by_path(path): + name = os.path.splitext(os.path.basename(path))[0] + if sys.version_info[0] < 3: + import imp + + return imp.load_source(name, path) + else: + import importlib.util + + spec = importlib.util.spec_from_file_location(name, path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +# =================================================================== +# --- others +# =================================================================== + + +def warn(msg): + """Raise a warning msg.""" + warnings.warn(msg, UserWarning, stacklevel=2) + + +def is_namedtuple(x): + """Check if object is an instance of namedtuple.""" + t = type(x) + b = t.__bases__ + if len(b) != 1 or b[0] != tuple: + return False + f = getattr(t, '_fields', None) + if not isinstance(f, tuple): + return False + return all(isinstance(n, str) for n in f) + + +if POSIX: + + @contextlib.contextmanager + def copyload_shared_lib(suffix=""): + """Ctx manager which picks up a random shared CO lib used + by this process, copies it in another location and loads it + in memory via ctypes. Return the new absolutized path. + """ + exe = 'pypy' if PYPY else 'python' + ext = ".so" + dst = get_testfn(suffix=suffix + ext) + libs = [ + x.path + for x in psutil.Process().memory_maps() + if os.path.splitext(x.path)[1] == ext and exe in x.path.lower() + ] + src = random.choice(libs) + shutil.copyfile(src, dst) + try: + ctypes.CDLL(dst) + yield dst + finally: + safe_rmpath(dst) + +else: + + @contextlib.contextmanager + def copyload_shared_lib(suffix=""): + """Ctx manager which picks up a random shared DLL lib used + by this process, copies it in another location and loads it + in memory via ctypes. + Return the new absolutized, normcased path. + """ + from ctypes import WinError + from ctypes import wintypes + + ext = ".dll" + dst = get_testfn(suffix=suffix + ext) + libs = [ + x.path + for x in psutil.Process().memory_maps() + if x.path.lower().endswith(ext) + and 'python' in os.path.basename(x.path).lower() + and 'wow64' not in x.path.lower() + ] + if PYPY and not libs: + libs = [ + x.path + for x in psutil.Process().memory_maps() + if 'pypy' in os.path.basename(x.path).lower() + ] + src = random.choice(libs) + shutil.copyfile(src, dst) + cfile = None + try: + cfile = ctypes.WinDLL(dst) + yield dst + finally: + # Work around OverflowError: + # - https://ci.appveyor.com/project/giampaolo/psutil/build/1207/ + # job/o53330pbnri9bcw7 + # - http://bugs.python.org/issue30286 + # - http://stackoverflow.com/questions/23522055 + if cfile is not None: + FreeLibrary = ctypes.windll.kernel32.FreeLibrary + FreeLibrary.argtypes = [wintypes.HMODULE] + ret = FreeLibrary(cfile._handle) + if ret == 0: + WinError() + safe_rmpath(dst) + + +# =================================================================== +# --- Exit funs (first is executed last) +# =================================================================== + + +# this is executed first +@atexit.register +def cleanup_test_procs(): + reap_children(recursive=True) + + +# atexit module does not execute exit functions in case of SIGTERM, which +# gets sent to test subprocesses, which is a problem if they import this +# module. With this it will. See: +# https://gmpy.dev/blog/2016/how-to-always-execute-exit-functions-in-python +if POSIX: + signal.signal(signal.SIGTERM, lambda sig, _: sys.exit(sig)) diff --git a/addon/globalPlugins/soundmanager/psutil/tests/__init__.pyc b/addon/globalPlugins/soundmanager/psutil/tests/__init__.pyc deleted file mode 100644 index b312a34..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/tests/__init__.pyc and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/__main__.py b/addon/globalPlugins/soundmanager/psutil/tests/__main__.py index 68710d4..8f832ab 100644 --- a/addon/globalPlugins/soundmanager/psutil/tests/__main__.py +++ b/addon/globalPlugins/soundmanager/psutil/tests/__main__.py @@ -1,94 +1,12 @@ -#!/usr/bin/env python - -# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -""" -Run unit tests. This is invoked by: - -$ python -m psutil.tests -""" - -import contextlib -import optparse -import os -import sys -import tempfile -try: - from urllib.request import urlopen # py3 -except ImportError: - from urllib2 import urlopen - -from psutil.tests import PYTHON_EXE -from psutil.tests.runner import run - - -HERE = os.path.abspath(os.path.dirname(__file__)) -GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" -TEST_DEPS = [] -if sys.version_info[:2] == (2, 6): - TEST_DEPS.extend(["ipaddress", "unittest2", "argparse", "mock==1.0.1"]) -elif sys.version_info[:2] == (2, 7) or sys.version_info[:2] <= (3, 2): - TEST_DEPS.extend(["ipaddress", "mock"]) - - -def install_pip(): - try: - import pip # NOQA - except ImportError: - import ssl - f = tempfile.NamedTemporaryFile(suffix='.py') - with contextlib.closing(f): - print("downloading %s to %s" % (GET_PIP_URL, f.name)) - if hasattr(ssl, '_create_unverified_context'): - ctx = ssl._create_unverified_context() - else: - ctx = None - kwargs = dict(context=ctx) if ctx else {} - req = urlopen(GET_PIP_URL, **kwargs) - data = req.read() - f.write(data) - f.flush() - - print("installing pip") - code = os.system('%s %s --user' % (PYTHON_EXE, f.name)) - return code - - -def install_test_deps(deps=None): - """Install test dependencies via pip.""" - if deps is None: - deps = TEST_DEPS - deps = set(deps) - if deps: - is_venv = hasattr(sys, 'real_prefix') - opts = "--user" if not is_venv else "" - install_pip() - code = os.system('%s -m pip install %s --upgrade %s' % ( - PYTHON_EXE, opts, " ".join(deps))) - return code - - -def main(): - usage = "%s -m psutil.tests [opts]" % PYTHON_EXE - parser = optparse.OptionParser(usage=usage, description="run unit tests") - parser.add_option("-i", "--install-deps", - action="store_true", default=False, - help="don't print status messages to stdout") - - opts, args = parser.parse_args() - if opts.install_deps: - install_pip() - install_test_deps() - else: - for dep in TEST_DEPS: - try: - __import__(dep.split("==")[0]) - except ImportError: - sys.exit("%r lib is not installed; run %s -m psutil.tests " - "--install-deps" % (dep, PYTHON_EXE)) - run() - - -main() +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Run unit tests. This is invoked by: +$ python -m psutil.tests. +""" + +from .runner import main + + +main() diff --git a/addon/globalPlugins/soundmanager/psutil/tests/__main__.pyc b/addon/globalPlugins/soundmanager/psutil/tests/__main__.pyc deleted file mode 100644 index 8d16960..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/tests/__main__.pyc and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/__init__.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..ce82afd Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/__init__.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/__main__.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/__main__.cpython-311.pyc new file mode 100644 index 0000000..072d9ea Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/__main__.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/runner.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/runner.cpython-311.pyc new file mode 100644 index 0000000..45576b3 Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/runner.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_aix.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_aix.cpython-311.pyc new file mode 100644 index 0000000..cf7608b Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_aix.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_bsd.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_bsd.cpython-311.pyc new file mode 100644 index 0000000..18c0bdd Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_bsd.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_connections.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_connections.cpython-311.pyc new file mode 100644 index 0000000..999140c Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_connections.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_contracts.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_contracts.cpython-311.pyc new file mode 100644 index 0000000..44404fc Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_contracts.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_linux.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_linux.cpython-311.pyc new file mode 100644 index 0000000..c90b74a Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_linux.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_memleaks.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_memleaks.cpython-311.pyc new file mode 100644 index 0000000..3df3a12 Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_memleaks.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_misc.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_misc.cpython-311.pyc new file mode 100644 index 0000000..8ef13ca Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_misc.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_osx.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_osx.cpython-311.pyc new file mode 100644 index 0000000..4f663d0 Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_osx.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_posix.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_posix.cpython-311.pyc new file mode 100644 index 0000000..a794b40 Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_posix.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_process.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_process.cpython-311.pyc new file mode 100644 index 0000000..5da0f19 Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_process.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_process_all.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_process_all.cpython-311.pyc new file mode 100644 index 0000000..6e751b1 Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_process_all.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_sunos.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_sunos.cpython-311.pyc new file mode 100644 index 0000000..6e665ce Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_sunos.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_system.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_system.cpython-311.pyc new file mode 100644 index 0000000..953a2c2 Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_system.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_testutils.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_testutils.cpython-311.pyc new file mode 100644 index 0000000..267e9db Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_testutils.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_unicode.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_unicode.cpython-311.pyc new file mode 100644 index 0000000..08652f9 Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_unicode.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_windows.cpython-311.pyc b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_windows.cpython-311.pyc new file mode 100644 index 0000000..39936f9 Binary files /dev/null and b/addon/globalPlugins/soundmanager/psutil/tests/__pycache__/test_windows.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/runner.py b/addon/globalPlugins/soundmanager/psutil/tests/runner.py index 1a28aa4..f180fef 100644 --- a/addon/globalPlugins/soundmanager/psutil/tests/runner.py +++ b/addon/globalPlugins/soundmanager/psutil/tests/runner.py @@ -1,196 +1,385 @@ -#!/usr/bin/env python - -# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -""" -Unit test runner, providing colourized output and printing failures -on KeyboardInterrupt. -""" - -from __future__ import print_function -import atexit -import os -import sys -import unittest -from unittest import TestResult -from unittest import TextTestResult -from unittest import TextTestRunner -try: - import ctypes -except ImportError: - ctypes = None - -import psutil -from psutil._common import memoize -from psutil.tests import safe_rmpath -from psutil.tests import TOX - - -HERE = os.path.abspath(os.path.dirname(__file__)) -VERBOSITY = 1 if TOX else 2 -FAILED_TESTS_FNAME = '.failed-tests.txt' -if os.name == 'posix': - GREEN = 1 - RED = 2 - BROWN = 94 -else: - GREEN = 2 - RED = 4 - BROWN = 6 - DEFAULT_COLOR = 7 - - -def term_supports_colors(file=sys.stdout): - if os.name == 'nt': - return ctypes is not None - try: - import curses - assert file.isatty() - curses.setupterm() - assert curses.tigetnum("colors") > 0 - except Exception: - return False - else: - return True - - -def hilite(s, color, bold=False): - """Return an highlighted version of 'string'.""" - attr = [] - if color == GREEN: - attr.append('32') - elif color == RED: - attr.append('91') - elif color == BROWN: - attr.append('33') - else: - raise ValueError("unrecognized color") - if bold: - attr.append('1') - return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), s) - - -@memoize -def _stderr_handle(): - GetStdHandle = ctypes.windll.Kernel32.GetStdHandle - STD_ERROR_HANDLE_ID = ctypes.c_ulong(0xfffffff4) - GetStdHandle.restype = ctypes.c_ulong - handle = GetStdHandle(STD_ERROR_HANDLE_ID) - atexit.register(ctypes.windll.Kernel32.CloseHandle, handle) - return handle - - -def win_colorprint(printer, s, color, bold=False): - if bold and color <= 7: - color += 8 - handle = _stderr_handle() - SetConsoleTextAttribute = ctypes.windll.Kernel32.SetConsoleTextAttribute - SetConsoleTextAttribute(handle, color) - try: - printer(s) - finally: - SetConsoleTextAttribute(handle, DEFAULT_COLOR) - - -class ColouredResult(TextTestResult): - - def _color_print(self, s, color, bold=False): - if os.name == 'posix': - self.stream.writeln(hilite(s, color, bold=bold)) - else: - win_colorprint(self.stream.writeln, s, color, bold=bold) - - def addSuccess(self, test): - TestResult.addSuccess(self, test) - self._color_print("OK", GREEN) - - def addError(self, test, err): - TestResult.addError(self, test, err) - self._color_print("ERROR", RED, bold=True) - - def addFailure(self, test, err): - TestResult.addFailure(self, test, err) - self._color_print("FAIL", RED) - - def addSkip(self, test, reason): - TestResult.addSkip(self, test, reason) - self._color_print("skipped: %s" % reason, BROWN) - - def printErrorList(self, flavour, errors): - flavour = hilite(flavour, RED, bold=flavour == 'ERROR') - TextTestResult.printErrorList(self, flavour, errors) - - -class ColouredRunner(TextTestRunner): - resultclass = ColouredResult if term_supports_colors() else TextTestResult - - def _makeResult(self): - # Store result instance so that it can be accessed on - # KeyboardInterrupt. - self.result = TextTestRunner._makeResult(self) - return self.result - - -def setup_tests(): - if 'PSUTIL_TESTING' not in os.environ: - # This won't work on Windows but set_testing() below will do it. - os.environ['PSUTIL_TESTING'] = '1' - psutil._psplatform.cext.set_testing() - - -def get_suite(name=None): - suite = unittest.TestSuite() - if name is None: - testmods = [os.path.splitext(x)[0] for x in os.listdir(HERE) - if x.endswith('.py') and x.startswith('test_') and not - x.startswith('test_memory_leaks')] - if "WHEELHOUSE_UPLOADER_USERNAME" in os.environ: - testmods = [x for x in testmods if not x.endswith(( - "osx", "posix", "linux"))] - for tm in testmods: - # ...so that the full test paths are printed on screen - tm = "psutil.tests.%s" % tm - suite.addTest(unittest.defaultTestLoader.loadTestsFromName(tm)) - else: - name = os.path.splitext(os.path.basename(name))[0] - suite.addTest(unittest.defaultTestLoader.loadTestsFromName(name)) - return suite - - -def get_suite_from_failed(): - # ...from previously failed test run - suite = unittest.TestSuite() - if not os.path.isfile(FAILED_TESTS_FNAME): - return suite - with open(FAILED_TESTS_FNAME, 'rt') as f: - names = f.read().split() - for n in names: - suite.addTest(unittest.defaultTestLoader.loadTestsFromName(n)) - return suite - - -def save_failed_tests(result): - if result.wasSuccessful(): - return safe_rmpath(FAILED_TESTS_FNAME) - with open(FAILED_TESTS_FNAME, 'wt') as f: - for t in result.errors + result.failures: - tname = str(t[0]) - f.write(tname + '\n') - - -def run(name=None, last_failed=False): - setup_tests() - runner = ColouredRunner(verbosity=VERBOSITY) - suite = get_suite_from_failed() if last_failed else get_suite(name) - try: - result = runner.run(suite) - except (KeyboardInterrupt, SystemExit) as err: - print("received %s" % err.__class__.__name__, file=sys.stderr) - runner.result.printErrors() - sys.exit(1) - else: - save_failed_tests(result) - success = result.wasSuccessful() - sys.exit(0 if success else 1) +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Unit test runner, providing new features on top of unittest module: +- colourized output +- parallel run (UNIX only) +- print failures/tracebacks on CTRL+C +- re-run failed tests only (make test-failed). + +Invocation examples: +- make test +- make test-failed + +Parallel: +- make test-parallel +- make test-process ARGS=--parallel +""" + +from __future__ import print_function + +import atexit +import optparse +import os +import sys +import textwrap +import time +import unittest + + +try: + import ctypes +except ImportError: + ctypes = None + +try: + import concurrencytest # pip install concurrencytest +except ImportError: + concurrencytest = None + +import psutil +from psutil._common import hilite +from psutil._common import print_color +from psutil._common import term_supports_colors +from psutil._compat import super +from psutil.tests import CI_TESTING +from psutil.tests import import_module_by_path +from psutil.tests import print_sysinfo +from psutil.tests import reap_children +from psutil.tests import safe_rmpath + + +VERBOSITY = 2 +FAILED_TESTS_FNAME = '.failed-tests.txt' +NWORKERS = psutil.cpu_count() or 1 +USE_COLORS = not CI_TESTING and term_supports_colors() + +HERE = os.path.abspath(os.path.dirname(__file__)) +loadTestsFromTestCase = ( # noqa: N816 + unittest.defaultTestLoader.loadTestsFromTestCase +) + + +def cprint(msg, color, bold=False, file=None): + if file is None: + file = sys.stderr if color == 'red' else sys.stdout + if USE_COLORS: + print_color(msg, color, bold=bold, file=file) + else: + print(msg, file=file) + + +class TestLoader: + + testdir = HERE + skip_files = ['test_memleaks.py'] + if "WHEELHOUSE_UPLOADER_USERNAME" in os.environ: + skip_files.extend(['test_osx.py', 'test_linux.py', 'test_posix.py']) + + def _get_testmods(self): + return [ + os.path.join(self.testdir, x) + for x in os.listdir(self.testdir) + if x.startswith('test_') + and x.endswith('.py') + and x not in self.skip_files + ] + + def _iter_testmod_classes(self): + """Iterate over all test files in this directory and return + all TestCase classes in them. + """ + for path in self._get_testmods(): + mod = import_module_by_path(path) + for name in dir(mod): + obj = getattr(mod, name) + if isinstance(obj, type) and issubclass( + obj, unittest.TestCase + ): + yield obj + + def all(self): + suite = unittest.TestSuite() + for obj in self._iter_testmod_classes(): + test = loadTestsFromTestCase(obj) + suite.addTest(test) + return suite + + def last_failed(self): + # ...from previously failed test run + suite = unittest.TestSuite() + if not os.path.isfile(FAILED_TESTS_FNAME): + return suite + with open(FAILED_TESTS_FNAME) as f: + names = f.read().split() + for n in names: + test = unittest.defaultTestLoader.loadTestsFromName(n) + suite.addTest(test) + return suite + + def from_name(self, name): + if name.endswith('.py'): + name = os.path.splitext(os.path.basename(name))[0] + return unittest.defaultTestLoader.loadTestsFromName(name) + + +class ColouredResult(unittest.TextTestResult): + def addSuccess(self, test): + unittest.TestResult.addSuccess(self, test) + cprint("OK", "green") + + def addError(self, test, err): + unittest.TestResult.addError(self, test, err) + cprint("ERROR", "red", bold=True) + + def addFailure(self, test, err): + unittest.TestResult.addFailure(self, test, err) + cprint("FAIL", "red") + + def addSkip(self, test, reason): + unittest.TestResult.addSkip(self, test, reason) + cprint("skipped: %s" % reason.strip(), "brown") + + def printErrorList(self, flavour, errors): + flavour = hilite(flavour, "red", bold=flavour == 'ERROR') + super().printErrorList(flavour, errors) + + +class ColouredTextRunner(unittest.TextTestRunner): + """A coloured text runner which also prints failed tests on + KeyboardInterrupt and save failed tests in a file so that they can + be re-run. + """ + + resultclass = ColouredResult if USE_COLORS else unittest.TextTestResult + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.failed_tnames = set() + + def _makeResult(self): + # Store result instance so that it can be accessed on + # KeyboardInterrupt. + self.result = super()._makeResult() + return self.result + + def _write_last_failed(self): + if self.failed_tnames: + with open(FAILED_TESTS_FNAME, "w") as f: + for tname in self.failed_tnames: + f.write(tname + '\n') + + def _save_result(self, result): + if not result.wasSuccessful(): + for t in result.errors + result.failures: + tname = t[0].id() + self.failed_tnames.add(tname) + + def _run(self, suite): + try: + result = super().run(suite) + except (KeyboardInterrupt, SystemExit): + result = self.runner.result + result.printErrors() + raise sys.exit(1) + else: + self._save_result(result) + return result + + def _exit(self, success): + if success: + cprint("SUCCESS", "green", bold=True) + safe_rmpath(FAILED_TESTS_FNAME) + sys.exit(0) + else: + cprint("FAILED", "red", bold=True) + self._write_last_failed() + sys.exit(1) + + def run(self, suite): + result = self._run(suite) + self._exit(result.wasSuccessful()) + + +class ParallelRunner(ColouredTextRunner): + @staticmethod + def _parallelize(suite): + def fdopen(fd, mode, *kwds): + stream = orig_fdopen(fd, mode) + atexit.register(stream.close) + return stream + + # Monkey patch concurrencytest lib bug (fdopen() stream not closed). + # https://github.com/cgoldberg/concurrencytest/issues/11 + orig_fdopen = os.fdopen + concurrencytest.os.fdopen = fdopen + forker = concurrencytest.fork_for_tests(NWORKERS) + return concurrencytest.ConcurrentTestSuite(suite, forker) + + @staticmethod + def _split_suite(suite): + serial = unittest.TestSuite() + parallel = unittest.TestSuite() + for test in suite: + if test.countTestCases() == 0: + continue + if isinstance(test, unittest.TestSuite): + test_class = test._tests[0].__class__ + elif isinstance(test, unittest.TestCase): + test_class = test + else: + raise TypeError("can't recognize type %r" % test) + + if getattr(test_class, '_serialrun', False): + serial.addTest(test) + else: + parallel.addTest(test) + return (serial, parallel) + + def run(self, suite): + ser_suite, par_suite = self._split_suite(suite) + par_suite = self._parallelize(par_suite) + + # run parallel + cprint( + "starting parallel tests using %s workers" % NWORKERS, + "green", + bold=True, + ) + t = time.time() + par = self._run(par_suite) + par_elapsed = time.time() - t + + # At this point we should have N zombies (the workers), which + # will disappear with wait(). + orphans = psutil.Process().children() + gone, alive = psutil.wait_procs(orphans, timeout=1) + if alive: + cprint("alive processes %s" % alive, "red") + reap_children() + + # run serial + t = time.time() + ser = self._run(ser_suite) + ser_elapsed = time.time() - t + + # print + if not par.wasSuccessful() and ser_suite.countTestCases() > 0: + par.printErrors() # print them again at the bottom + par_fails, par_errs, par_skips = map( + len, (par.failures, par.errors, par.skipped) + ) + ser_fails, ser_errs, ser_skips = map( + len, (ser.failures, ser.errors, ser.skipped) + ) + print( + textwrap.dedent( + """ + +----------+----------+----------+----------+----------+----------+ + | | total | failures | errors | skipped | time | + +----------+----------+----------+----------+----------+----------+ + | parallel | %3s | %3s | %3s | %3s | %.2fs | + +----------+----------+----------+----------+----------+----------+ + | serial | %3s | %3s | %3s | %3s | %.2fs | + +----------+----------+----------+----------+----------+----------+ + """ + % ( + par.testsRun, + par_fails, + par_errs, + par_skips, + par_elapsed, + ser.testsRun, + ser_fails, + ser_errs, + ser_skips, + ser_elapsed, + ) + ) + ) + print( + "Ran %s tests in %.3fs using %s workers" + % ( + par.testsRun + ser.testsRun, + par_elapsed + ser_elapsed, + NWORKERS, + ) + ) + ok = par.wasSuccessful() and ser.wasSuccessful() + self._exit(ok) + + +def get_runner(parallel=False): + def warn(msg): + cprint(msg + " Running serial tests instead.", "red") + + if parallel: + if psutil.WINDOWS: + warn("Can't run parallel tests on Windows.") + elif concurrencytest is None: + warn("concurrencytest module is not installed.") + elif NWORKERS == 1: + warn("Only 1 CPU available.") + else: + return ParallelRunner(verbosity=VERBOSITY) + return ColouredTextRunner(verbosity=VERBOSITY) + + +# Used by test_*,py modules. +def run_from_name(name): + if CI_TESTING: + print_sysinfo() + suite = TestLoader().from_name(name) + runner = get_runner() + runner.run(suite) + + +def setup(): + psutil._set_debug(True) + + +def main(): + setup() + usage = "python3 -m psutil.tests [opts] [test-name]" + parser = optparse.OptionParser(usage=usage, description="run unit tests") + parser.add_option( + "--last-failed", + action="store_true", + default=False, + help="only run last failed tests", + ) + parser.add_option( + "--parallel", + action="store_true", + default=False, + help="run tests in parallel", + ) + opts, args = parser.parse_args() + + if not opts.last_failed: + safe_rmpath(FAILED_TESTS_FNAME) + + # loader + loader = TestLoader() + if args: + if len(args) > 1: + parser.print_usage() + return sys.exit(1) + else: + suite = loader.from_name(args[0]) + elif opts.last_failed: + suite = loader.last_failed() + else: + suite = loader.all() + + if CI_TESTING: + print_sysinfo() + runner = get_runner(opts.parallel) + runner.run(suite) + + +if __name__ == '__main__': + main() diff --git a/addon/globalPlugins/soundmanager/psutil/tests/runner.pyc b/addon/globalPlugins/soundmanager/psutil/tests/runner.pyc deleted file mode 100644 index 44738c1..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/tests/runner.pyc and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_aix.py b/addon/globalPlugins/soundmanager/psutil/tests/test_aix.py index 1757e3e..970e18a 100644 --- a/addon/globalPlugins/soundmanager/psutil/tests/test_aix.py +++ b/addon/globalPlugins/soundmanager/psutil/tests/test_aix.py @@ -1,121 +1,137 @@ -#!/usr/bin/env python - -# Copyright (c) 2009, Giampaolo Rodola' -# Copyright (c) 2017, Arnon Yaari -# All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -"""AIX specific tests.""" - -import re - -from psutil import AIX -from psutil.tests import sh -from psutil.tests import unittest -import psutil - - -@unittest.skipIf(not AIX, "AIX only") -class AIXSpecificTestCase(unittest.TestCase): - - def test_virtual_memory(self): - out = sh('/usr/bin/svmon -O unit=KB') - re_pattern = r"memory\s*" - for field in ("size inuse free pin virtual available mmode").split(): - re_pattern += r"(?P<%s>\S+)\s+" % (field,) - matchobj = re.search(re_pattern, out) - - self.assertIsNotNone( - matchobj, "svmon command returned unexpected output") - - KB = 1024 - total = int(matchobj.group("size")) * KB - available = int(matchobj.group("available")) * KB - used = int(matchobj.group("inuse")) * KB - free = int(matchobj.group("free")) * KB - - psutil_result = psutil.virtual_memory() - - # MEMORY_TOLERANCE from psutil.tests is not enough. For some reason - # we're seeing differences of ~1.2 MB. 2 MB is still a good tolerance - # when compared to GBs. - MEMORY_TOLERANCE = 2 * KB * KB # 2 MB - self.assertEqual(psutil_result.total, total) - self.assertAlmostEqual( - psutil_result.used, used, delta=MEMORY_TOLERANCE) - self.assertAlmostEqual( - psutil_result.available, available, delta=MEMORY_TOLERANCE) - self.assertAlmostEqual( - psutil_result.free, free, delta=MEMORY_TOLERANCE) - - def test_swap_memory(self): - out = sh('/usr/sbin/lsps -a') - # From the man page, "The size is given in megabytes" so we assume - # we'll always have 'MB' in the result - # TODO maybe try to use "swap -l" to check "used" too, but its units - # are not guaranteed to be "MB" so parsing may not be consistent - matchobj = re.search(r"(?P\S+)\s+" - r"(?P\S+)\s+" - r"(?P\S+)\s+" - r"(?P\d+)MB", out) - - self.assertIsNotNone( - matchobj, "lsps command returned unexpected output") - - total_mb = int(matchobj.group("size")) - MB = 1024 ** 2 - psutil_result = psutil.swap_memory() - # we divide our result by MB instead of multiplying the lsps value by - # MB because lsps may round down, so we round down too - self.assertEqual(int(psutil_result.total / MB), total_mb) - - def test_cpu_stats(self): - out = sh('/usr/bin/mpstat -a') - - re_pattern = r"ALL\s*" - for field in ("min maj mpcs mpcr dev soft dec ph cs ics bound rq " - "push S3pull S3grd S0rd S1rd S2rd S3rd S4rd S5rd " - "sysc").split(): - re_pattern += r"(?P<%s>\S+)\s+" % (field,) - matchobj = re.search(re_pattern, out) - - self.assertIsNotNone( - matchobj, "mpstat command returned unexpected output") - - # numbers are usually in the millions so 1000 is ok for tolerance - CPU_STATS_TOLERANCE = 1000 - psutil_result = psutil.cpu_stats() - self.assertAlmostEqual( - psutil_result.ctx_switches, - int(matchobj.group("cs")), - delta=CPU_STATS_TOLERANCE) - self.assertAlmostEqual( - psutil_result.syscalls, - int(matchobj.group("sysc")), - delta=CPU_STATS_TOLERANCE) - self.assertAlmostEqual( - psutil_result.interrupts, - int(matchobj.group("dev")), - delta=CPU_STATS_TOLERANCE) - self.assertAlmostEqual( - psutil_result.soft_interrupts, - int(matchobj.group("soft")), - delta=CPU_STATS_TOLERANCE) - - def test_cpu_count_logical(self): - out = sh('/usr/bin/mpstat -a') - mpstat_lcpu = int(re.search(r"lcpu=(\d+)", out).group(1)) - psutil_lcpu = psutil.cpu_count(logical=True) - self.assertEqual(mpstat_lcpu, psutil_lcpu) - - def test_net_if_addrs_names(self): - out = sh('/etc/ifconfig -l') - ifconfig_names = set(out.split()) - psutil_names = set(psutil.net_if_addrs().keys()) - self.assertSetEqual(ifconfig_names, psutil_names) - - -if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola' +# Copyright (c) 2017, Arnon Yaari +# All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""AIX specific tests.""" + +import re +import unittest + +import psutil +from psutil import AIX +from psutil.tests import PsutilTestCase +from psutil.tests import sh + + +@unittest.skipIf(not AIX, "AIX only") +class AIXSpecificTestCase(PsutilTestCase): + def test_virtual_memory(self): + out = sh('/usr/bin/svmon -O unit=KB') + re_pattern = r"memory\s*" + for field in ("size inuse free pin virtual available mmode").split(): + re_pattern += r"(?P<%s>\S+)\s+" % (field,) + matchobj = re.search(re_pattern, out) + + self.assertIsNotNone( + matchobj, "svmon command returned unexpected output" + ) + + KB = 1024 + total = int(matchobj.group("size")) * KB + available = int(matchobj.group("available")) * KB + used = int(matchobj.group("inuse")) * KB + free = int(matchobj.group("free")) * KB + + psutil_result = psutil.virtual_memory() + + # TOLERANCE_SYS_MEM from psutil.tests is not enough. For some reason + # we're seeing differences of ~1.2 MB. 2 MB is still a good tolerance + # when compared to GBs. + TOLERANCE_SYS_MEM = 2 * KB * KB # 2 MB + self.assertEqual(psutil_result.total, total) + self.assertAlmostEqual( + psutil_result.used, used, delta=TOLERANCE_SYS_MEM + ) + self.assertAlmostEqual( + psutil_result.available, available, delta=TOLERANCE_SYS_MEM + ) + self.assertAlmostEqual( + psutil_result.free, free, delta=TOLERANCE_SYS_MEM + ) + + def test_swap_memory(self): + out = sh('/usr/sbin/lsps -a') + # From the man page, "The size is given in megabytes" so we assume + # we'll always have 'MB' in the result + # TODO maybe try to use "swap -l" to check "used" too, but its units + # are not guaranteed to be "MB" so parsing may not be consistent + matchobj = re.search( + r"(?P\S+)\s+" + r"(?P\S+)\s+" + r"(?P\S+)\s+" + r"(?P\d+)MB", + out, + ) + + self.assertIsNotNone( + matchobj, "lsps command returned unexpected output" + ) + + total_mb = int(matchobj.group("size")) + MB = 1024**2 + psutil_result = psutil.swap_memory() + # we divide our result by MB instead of multiplying the lsps value by + # MB because lsps may round down, so we round down too + self.assertEqual(int(psutil_result.total / MB), total_mb) + + def test_cpu_stats(self): + out = sh('/usr/bin/mpstat -a') + + re_pattern = r"ALL\s*" + for field in ( + "min maj mpcs mpcr dev soft dec ph cs ics bound rq " + "push S3pull S3grd S0rd S1rd S2rd S3rd S4rd S5rd " + "sysc" + ).split(): + re_pattern += r"(?P<%s>\S+)\s+" % (field,) + matchobj = re.search(re_pattern, out) + + self.assertIsNotNone( + matchobj, "mpstat command returned unexpected output" + ) + + # numbers are usually in the millions so 1000 is ok for tolerance + CPU_STATS_TOLERANCE = 1000 + psutil_result = psutil.cpu_stats() + self.assertAlmostEqual( + psutil_result.ctx_switches, + int(matchobj.group("cs")), + delta=CPU_STATS_TOLERANCE, + ) + self.assertAlmostEqual( + psutil_result.syscalls, + int(matchobj.group("sysc")), + delta=CPU_STATS_TOLERANCE, + ) + self.assertAlmostEqual( + psutil_result.interrupts, + int(matchobj.group("dev")), + delta=CPU_STATS_TOLERANCE, + ) + self.assertAlmostEqual( + psutil_result.soft_interrupts, + int(matchobj.group("soft")), + delta=CPU_STATS_TOLERANCE, + ) + + def test_cpu_count_logical(self): + out = sh('/usr/bin/mpstat -a') + mpstat_lcpu = int(re.search(r"lcpu=(\d+)", out).group(1)) + psutil_lcpu = psutil.cpu_count(logical=True) + self.assertEqual(mpstat_lcpu, psutil_lcpu) + + def test_net_if_addrs_names(self): + out = sh('/etc/ifconfig -l') + ifconfig_names = set(out.split()) + psutil_names = set(psutil.net_if_addrs().keys()) + self.assertSetEqual(ifconfig_names, psutil_names) + + +if __name__ == '__main__': + from psutil.tests.runner import run_from_name + + run_from_name(__file__) diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_aix.pyc b/addon/globalPlugins/soundmanager/psutil/tests/test_aix.pyc deleted file mode 100644 index 943915b..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/tests/test_aix.pyc and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_bsd.py b/addon/globalPlugins/soundmanager/psutil/tests/test_bsd.py index 5df8ad2..5c23768 100644 --- a/addon/globalPlugins/soundmanager/psutil/tests/test_bsd.py +++ b/addon/globalPlugins/soundmanager/psutil/tests/test_bsd.py @@ -1,553 +1,632 @@ -#!/usr/bin/env python - -# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -# TODO: (FreeBSD) add test for comparing connections with 'sockstat' cmd. - - -"""Tests specific to all BSD platforms.""" - - -import datetime -import os -import re -import time - -import psutil -from psutil import BSD -from psutil import FREEBSD -from psutil import NETBSD -from psutil import OPENBSD -from psutil.tests import get_test_subprocess -from psutil.tests import HAS_BATTERY -from psutil.tests import MEMORY_TOLERANCE -from psutil.tests import reap_children -from psutil.tests import retry_on_failure -from psutil.tests import sh -from psutil.tests import unittest -from psutil.tests import which - - -if BSD: - PAGESIZE = os.sysconf("SC_PAGE_SIZE") - if os.getuid() == 0: # muse requires root privileges - MUSE_AVAILABLE = which('muse') - else: - MUSE_AVAILABLE = False -else: - MUSE_AVAILABLE = False - - -def sysctl(cmdline): - """Expects a sysctl command with an argument and parse the result - returning only the value of interest. - """ - result = sh("sysctl " + cmdline) - if FREEBSD: - result = result[result.find(": ") + 2:] - elif OPENBSD or NETBSD: - result = result[result.find("=") + 1:] - try: - return int(result) - except ValueError: - return result - - -def muse(field): - """Thin wrapper around 'muse' cmdline utility.""" - out = sh('muse') - for line in out.split('\n'): - if line.startswith(field): - break - else: - raise ValueError("line not found") - return int(line.split()[1]) - - -# ===================================================================== -# --- All BSD* -# ===================================================================== - - -@unittest.skipIf(not BSD, "BSD only") -class BSDSpecificTestCase(unittest.TestCase): - """Generic tests common to all BSD variants.""" - - @classmethod - def setUpClass(cls): - cls.pid = get_test_subprocess().pid - - @classmethod - def tearDownClass(cls): - reap_children() - - @unittest.skipIf(NETBSD, "-o lstart doesn't work on NETBSD") - def test_process_create_time(self): - output = sh("ps -o lstart -p %s" % self.pid) - start_ps = output.replace('STARTED', '').strip() - start_psutil = psutil.Process(self.pid).create_time() - start_psutil = time.strftime("%a %b %e %H:%M:%S %Y", - time.localtime(start_psutil)) - self.assertEqual(start_ps, start_psutil) - - def test_disks(self): - # test psutil.disk_usage() and psutil.disk_partitions() - # against "df -a" - def df(path): - out = sh('df -k "%s"' % path).strip() - lines = out.split('\n') - lines.pop(0) - line = lines.pop(0) - dev, total, used, free = line.split()[:4] - if dev == 'none': - dev = '' - total = int(total) * 1024 - used = int(used) * 1024 - free = int(free) * 1024 - return dev, total, used, free - - for part in psutil.disk_partitions(all=False): - usage = psutil.disk_usage(part.mountpoint) - dev, total, used, free = df(part.mountpoint) - self.assertEqual(part.device, dev) - self.assertEqual(usage.total, total) - # 10 MB tollerance - if abs(usage.free - free) > 10 * 1024 * 1024: - self.fail("psutil=%s, df=%s" % (usage.free, free)) - if abs(usage.used - used) > 10 * 1024 * 1024: - self.fail("psutil=%s, df=%s" % (usage.used, used)) - - @unittest.skipIf(not which('sysctl'), "sysctl cmd not available") - def test_cpu_count_logical(self): - syst = sysctl("hw.ncpu") - self.assertEqual(psutil.cpu_count(logical=True), syst) - - @unittest.skipIf(not which('sysctl'), "sysctl cmd not available") - def test_virtual_memory_total(self): - num = sysctl('hw.physmem') - self.assertEqual(num, psutil.virtual_memory().total) - - def test_net_if_stats(self): - for name, stats in psutil.net_if_stats().items(): - try: - out = sh("ifconfig %s" % name) - except RuntimeError: - pass - else: - self.assertEqual(stats.isup, 'RUNNING' in out, msg=out) - if "mtu" in out: - self.assertEqual(stats.mtu, - int(re.findall(r'mtu (\d+)', out)[0])) - - -# ===================================================================== -# --- FreeBSD -# ===================================================================== - - -@unittest.skipIf(not FREEBSD, "FREEBSD only") -class FreeBSDSpecificTestCase(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.pid = get_test_subprocess().pid - - @classmethod - def tearDownClass(cls): - reap_children() - - @staticmethod - def parse_swapinfo(): - # the last line is always the total - output = sh("swapinfo -k").splitlines()[-1] - parts = re.split(r'\s+', output) - - if not parts: - raise ValueError("Can't parse swapinfo: %s" % output) - - # the size is in 1k units, so multiply by 1024 - total, used, free = (int(p) * 1024 for p in parts[1:4]) - return total, used, free - - @retry_on_failure() - def test_proc_memory_maps(self): - out = sh('procstat -v %s' % self.pid) - maps = psutil.Process(self.pid).memory_maps(grouped=False) - lines = out.split('\n')[1:] - while lines: - line = lines.pop() - fields = line.split() - _, start, stop, perms, res = fields[:5] - map = maps.pop() - self.assertEqual("%s-%s" % (start, stop), map.addr) - self.assertEqual(int(res), map.rss) - if not map.path.startswith('['): - self.assertEqual(fields[10], map.path) - - def test_proc_exe(self): - out = sh('procstat -b %s' % self.pid) - self.assertEqual(psutil.Process(self.pid).exe(), - out.split('\n')[1].split()[-1]) - - def test_proc_cmdline(self): - out = sh('procstat -c %s' % self.pid) - self.assertEqual(' '.join(psutil.Process(self.pid).cmdline()), - ' '.join(out.split('\n')[1].split()[2:])) - - def test_proc_uids_gids(self): - out = sh('procstat -s %s' % self.pid) - euid, ruid, suid, egid, rgid, sgid = out.split('\n')[1].split()[2:8] - p = psutil.Process(self.pid) - uids = p.uids() - gids = p.gids() - self.assertEqual(uids.real, int(ruid)) - self.assertEqual(uids.effective, int(euid)) - self.assertEqual(uids.saved, int(suid)) - self.assertEqual(gids.real, int(rgid)) - self.assertEqual(gids.effective, int(egid)) - self.assertEqual(gids.saved, int(sgid)) - - @retry_on_failure() - def test_proc_ctx_switches(self): - tested = [] - out = sh('procstat -r %s' % self.pid) - p = psutil.Process(self.pid) - for line in out.split('\n'): - line = line.lower().strip() - if ' voluntary context' in line: - pstat_value = int(line.split()[-1]) - psutil_value = p.num_ctx_switches().voluntary - self.assertEqual(pstat_value, psutil_value) - tested.append(None) - elif ' involuntary context' in line: - pstat_value = int(line.split()[-1]) - psutil_value = p.num_ctx_switches().involuntary - self.assertEqual(pstat_value, psutil_value) - tested.append(None) - if len(tested) != 2: - raise RuntimeError("couldn't find lines match in procstat out") - - @retry_on_failure() - def test_proc_cpu_times(self): - tested = [] - out = sh('procstat -r %s' % self.pid) - p = psutil.Process(self.pid) - for line in out.split('\n'): - line = line.lower().strip() - if 'user time' in line: - pstat_value = float('0.' + line.split()[-1].split('.')[-1]) - psutil_value = p.cpu_times().user - self.assertEqual(pstat_value, psutil_value) - tested.append(None) - elif 'system time' in line: - pstat_value = float('0.' + line.split()[-1].split('.')[-1]) - psutil_value = p.cpu_times().system - self.assertEqual(pstat_value, psutil_value) - tested.append(None) - if len(tested) != 2: - raise RuntimeError("couldn't find lines match in procstat out") - - def test_cpu_frequency_against_sysctl(self): - # Currently only cpu 0 is frequency is supported in FreeBSD - # All other cores use the same frequency. - sensor = "dev.cpu.0.freq" - sysctl_result = int(sysctl(sensor)) - self.assertEqual(psutil.cpu_freq().current, sysctl_result) - - sensor = "dev.cpu.0.freq_levels" - sysctl_result = sysctl(sensor) - # sysctl returns a string of the format: - # / /... - # Ordered highest available to lowest available. - max_freq = int(sysctl_result.split()[0].split("/")[0]) - min_freq = int(sysctl_result.split()[-1].split("/")[0]) - self.assertEqual(psutil.cpu_freq().max, max_freq) - self.assertEqual(psutil.cpu_freq().min, min_freq) - - # --- virtual_memory(); tests against sysctl - - @retry_on_failure() - def test_vmem_active(self): - syst = sysctl("vm.stats.vm.v_active_count") * PAGESIZE - self.assertAlmostEqual(psutil.virtual_memory().active, syst, - delta=MEMORY_TOLERANCE) - - @retry_on_failure() - def test_vmem_inactive(self): - syst = sysctl("vm.stats.vm.v_inactive_count") * PAGESIZE - self.assertAlmostEqual(psutil.virtual_memory().inactive, syst, - delta=MEMORY_TOLERANCE) - - @retry_on_failure() - def test_vmem_wired(self): - syst = sysctl("vm.stats.vm.v_wire_count") * PAGESIZE - self.assertAlmostEqual(psutil.virtual_memory().wired, syst, - delta=MEMORY_TOLERANCE) - - @retry_on_failure() - def test_vmem_cached(self): - syst = sysctl("vm.stats.vm.v_cache_count") * PAGESIZE - self.assertAlmostEqual(psutil.virtual_memory().cached, syst, - delta=MEMORY_TOLERANCE) - - @retry_on_failure() - def test_vmem_free(self): - syst = sysctl("vm.stats.vm.v_free_count") * PAGESIZE - self.assertAlmostEqual(psutil.virtual_memory().free, syst, - delta=MEMORY_TOLERANCE) - - @retry_on_failure() - def test_vmem_buffers(self): - syst = sysctl("vfs.bufspace") - self.assertAlmostEqual(psutil.virtual_memory().buffers, syst, - delta=MEMORY_TOLERANCE) - - # --- virtual_memory(); tests against muse - - @unittest.skipIf(not MUSE_AVAILABLE, "muse not installed") - def test_muse_vmem_total(self): - num = muse('Total') - self.assertEqual(psutil.virtual_memory().total, num) - - @unittest.skipIf(not MUSE_AVAILABLE, "muse not installed") - @retry_on_failure() - def test_muse_vmem_active(self): - num = muse('Active') - self.assertAlmostEqual(psutil.virtual_memory().active, num, - delta=MEMORY_TOLERANCE) - - @unittest.skipIf(not MUSE_AVAILABLE, "muse not installed") - @retry_on_failure() - def test_muse_vmem_inactive(self): - num = muse('Inactive') - self.assertAlmostEqual(psutil.virtual_memory().inactive, num, - delta=MEMORY_TOLERANCE) - - @unittest.skipIf(not MUSE_AVAILABLE, "muse not installed") - @retry_on_failure() - def test_muse_vmem_wired(self): - num = muse('Wired') - self.assertAlmostEqual(psutil.virtual_memory().wired, num, - delta=MEMORY_TOLERANCE) - - @unittest.skipIf(not MUSE_AVAILABLE, "muse not installed") - @retry_on_failure() - def test_muse_vmem_cached(self): - num = muse('Cache') - self.assertAlmostEqual(psutil.virtual_memory().cached, num, - delta=MEMORY_TOLERANCE) - - @unittest.skipIf(not MUSE_AVAILABLE, "muse not installed") - @retry_on_failure() - def test_muse_vmem_free(self): - num = muse('Free') - self.assertAlmostEqual(psutil.virtual_memory().free, num, - delta=MEMORY_TOLERANCE) - - @unittest.skipIf(not MUSE_AVAILABLE, "muse not installed") - @retry_on_failure() - def test_muse_vmem_buffers(self): - num = muse('Buffer') - self.assertAlmostEqual(psutil.virtual_memory().buffers, num, - delta=MEMORY_TOLERANCE) - - def test_cpu_stats_ctx_switches(self): - self.assertAlmostEqual(psutil.cpu_stats().ctx_switches, - sysctl('vm.stats.sys.v_swtch'), delta=1000) - - def test_cpu_stats_interrupts(self): - self.assertAlmostEqual(psutil.cpu_stats().interrupts, - sysctl('vm.stats.sys.v_intr'), delta=1000) - - def test_cpu_stats_soft_interrupts(self): - self.assertAlmostEqual(psutil.cpu_stats().soft_interrupts, - sysctl('vm.stats.sys.v_soft'), delta=1000) - - def test_cpu_stats_syscalls(self): - self.assertAlmostEqual(psutil.cpu_stats().syscalls, - sysctl('vm.stats.sys.v_syscall'), delta=1000) - - # def test_cpu_stats_traps(self): - # self.assertAlmostEqual(psutil.cpu_stats().traps, - # sysctl('vm.stats.sys.v_trap'), delta=1000) - - # --- swap memory - - def test_swapmem_free(self): - total, used, free = self.parse_swapinfo() - self.assertAlmostEqual( - psutil.swap_memory().free, free, delta=MEMORY_TOLERANCE) - - def test_swapmem_used(self): - total, used, free = self.parse_swapinfo() - self.assertAlmostEqual( - psutil.swap_memory().used, used, delta=MEMORY_TOLERANCE) - - def test_swapmem_total(self): - total, used, free = self.parse_swapinfo() - self.assertAlmostEqual( - psutil.swap_memory().total, total, delta=MEMORY_TOLERANCE) - - # --- others - - def test_boot_time(self): - s = sysctl('sysctl kern.boottime') - s = s[s.find(" sec = ") + 7:] - s = s[:s.find(',')] - btime = int(s) - self.assertEqual(btime, psutil.boot_time()) - - # --- sensors_battery - - @unittest.skipIf(not HAS_BATTERY, "no battery") - def test_sensors_battery(self): - def secs2hours(secs): - m, s = divmod(secs, 60) - h, m = divmod(m, 60) - return "%d:%02d" % (h, m) - - out = sh("acpiconf -i 0") - fields = dict([(x.split('\t')[0], x.split('\t')[-1]) - for x in out.split("\n")]) - metrics = psutil.sensors_battery() - percent = int(fields['Remaining capacity:'].replace('%', '')) - remaining_time = fields['Remaining time:'] - self.assertEqual(metrics.percent, percent) - if remaining_time == 'unknown': - self.assertEqual(metrics.secsleft, psutil.POWER_TIME_UNLIMITED) - else: - self.assertEqual(secs2hours(metrics.secsleft), remaining_time) - - @unittest.skipIf(not HAS_BATTERY, "no battery") - def test_sensors_battery_against_sysctl(self): - self.assertEqual(psutil.sensors_battery().percent, - sysctl("hw.acpi.battery.life")) - self.assertEqual(psutil.sensors_battery().power_plugged, - sysctl("hw.acpi.acline") == 1) - secsleft = psutil.sensors_battery().secsleft - if secsleft < 0: - self.assertEqual(sysctl("hw.acpi.battery.time"), -1) - else: - self.assertEqual(secsleft, sysctl("hw.acpi.battery.time") * 60) - - @unittest.skipIf(HAS_BATTERY, "has battery") - def test_sensors_battery_no_battery(self): - # If no battery is present one of these calls is supposed - # to fail, see: - # https://github.com/giampaolo/psutil/issues/1074 - with self.assertRaises(RuntimeError): - sysctl("hw.acpi.battery.life") - sysctl("hw.acpi.battery.time") - sysctl("hw.acpi.acline") - self.assertIsNone(psutil.sensors_battery()) - - # --- sensors_temperatures - - def test_sensors_temperatures_against_sysctl(self): - num_cpus = psutil.cpu_count(True) - for cpu in range(num_cpus): - sensor = "dev.cpu.%s.temperature" % cpu - # sysctl returns a string in the format 46.0C - sysctl_result = int(float(sysctl(sensor)[:-1])) - self.assertAlmostEqual( - psutil.sensors_temperatures()["coretemp"][cpu].current, - sysctl_result, delta=10) - - sensor = "dev.cpu.%s.coretemp.tjmax" % cpu - sysctl_result = int(float(sysctl(sensor)[:-1])) - self.assertEqual( - psutil.sensors_temperatures()["coretemp"][cpu].high, - sysctl_result) - -# ===================================================================== -# --- OpenBSD -# ===================================================================== - - -@unittest.skipIf(not OPENBSD, "OPENBSD only") -class OpenBSDSpecificTestCase(unittest.TestCase): - - def test_boot_time(self): - s = sysctl('kern.boottime') - sys_bt = datetime.datetime.strptime(s, "%a %b %d %H:%M:%S %Y") - psutil_bt = datetime.datetime.fromtimestamp(psutil.boot_time()) - self.assertEqual(sys_bt, psutil_bt) - - -# ===================================================================== -# --- NetBSD -# ===================================================================== - - -@unittest.skipIf(not NETBSD, "NETBSD only") -class NetBSDSpecificTestCase(unittest.TestCase): - - @staticmethod - def parse_meminfo(look_for): - with open('/proc/meminfo', 'rb') as f: - for line in f: - if line.startswith(look_for): - return int(line.split()[1]) * 1024 - raise ValueError("can't find %s" % look_for) - - def test_vmem_total(self): - self.assertEqual( - psutil.virtual_memory().total, self.parse_meminfo("MemTotal:")) - - def test_vmem_free(self): - self.assertAlmostEqual( - psutil.virtual_memory().free, self.parse_meminfo("MemFree:"), - delta=MEMORY_TOLERANCE) - - def test_vmem_buffers(self): - self.assertAlmostEqual( - psutil.virtual_memory().buffers, self.parse_meminfo("Buffers:"), - delta=MEMORY_TOLERANCE) - - def test_vmem_shared(self): - self.assertAlmostEqual( - psutil.virtual_memory().shared, self.parse_meminfo("MemShared:"), - delta=MEMORY_TOLERANCE) - - def test_swapmem_total(self): - self.assertAlmostEqual( - psutil.swap_memory().total, self.parse_meminfo("SwapTotal:"), - delta=MEMORY_TOLERANCE) - - def test_swapmem_free(self): - self.assertAlmostEqual( - psutil.swap_memory().free, self.parse_meminfo("SwapFree:"), - delta=MEMORY_TOLERANCE) - - def test_swapmem_used(self): - smem = psutil.swap_memory() - self.assertEqual(smem.used, smem.total - smem.free) - - def test_cpu_stats_interrupts(self): - with open('/proc/stat', 'rb') as f: - for line in f: - if line.startswith(b'intr'): - interrupts = int(line.split()[1]) - break - else: - raise ValueError("couldn't find line") - self.assertAlmostEqual( - psutil.cpu_stats().interrupts, interrupts, delta=1000) - - def test_cpu_stats_ctx_switches(self): - with open('/proc/stat', 'rb') as f: - for line in f: - if line.startswith(b'ctxt'): - ctx_switches = int(line.split()[1]) - break - else: - raise ValueError("couldn't find line") - self.assertAlmostEqual( - psutil.cpu_stats().ctx_switches, ctx_switches, delta=1000) - - -if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# TODO: (FreeBSD) add test for comparing connections with 'sockstat' cmd. + + +"""Tests specific to all BSD platforms.""" + + +import datetime +import os +import re +import time +import unittest + +import psutil +from psutil import BSD +from psutil import FREEBSD +from psutil import NETBSD +from psutil import OPENBSD +from psutil.tests import HAS_BATTERY +from psutil.tests import TOLERANCE_SYS_MEM +from psutil.tests import PsutilTestCase +from psutil.tests import retry_on_failure +from psutil.tests import sh +from psutil.tests import spawn_testproc +from psutil.tests import terminate +from psutil.tests import which + + +if BSD: + from psutil._psutil_posix import getpagesize + + PAGESIZE = getpagesize() + # muse requires root privileges + MUSE_AVAILABLE = os.getuid() == 0 and which('muse') +else: + PAGESIZE = None + MUSE_AVAILABLE = False + + +def sysctl(cmdline): + """Expects a sysctl command with an argument and parse the result + returning only the value of interest. + """ + result = sh("sysctl " + cmdline) + if FREEBSD: + result = result[result.find(": ") + 2 :] + elif OPENBSD or NETBSD: + result = result[result.find("=") + 1 :] + try: + return int(result) + except ValueError: + return result + + +def muse(field): + """Thin wrapper around 'muse' cmdline utility.""" + out = sh('muse') + for line in out.split('\n'): + if line.startswith(field): + break + else: + raise ValueError("line not found") + return int(line.split()[1]) + + +# ===================================================================== +# --- All BSD* +# ===================================================================== + + +@unittest.skipIf(not BSD, "BSD only") +class BSDTestCase(PsutilTestCase): + """Generic tests common to all BSD variants.""" + + @classmethod + def setUpClass(cls): + cls.pid = spawn_testproc().pid + + @classmethod + def tearDownClass(cls): + terminate(cls.pid) + + @unittest.skipIf(NETBSD, "-o lstart doesn't work on NETBSD") + def test_process_create_time(self): + output = sh("ps -o lstart -p %s" % self.pid) + start_ps = output.replace('STARTED', '').strip() + start_psutil = psutil.Process(self.pid).create_time() + start_psutil = time.strftime( + "%a %b %e %H:%M:%S %Y", time.localtime(start_psutil) + ) + self.assertEqual(start_ps, start_psutil) + + def test_disks(self): + # test psutil.disk_usage() and psutil.disk_partitions() + # against "df -a" + def df(path): + out = sh('df -k "%s"' % path).strip() + lines = out.split('\n') + lines.pop(0) + line = lines.pop(0) + dev, total, used, free = line.split()[:4] + if dev == 'none': + dev = '' + total = int(total) * 1024 + used = int(used) * 1024 + free = int(free) * 1024 + return dev, total, used, free + + for part in psutil.disk_partitions(all=False): + usage = psutil.disk_usage(part.mountpoint) + dev, total, used, free = df(part.mountpoint) + self.assertEqual(part.device, dev) + self.assertEqual(usage.total, total) + # 10 MB tolerance + if abs(usage.free - free) > 10 * 1024 * 1024: + raise self.fail("psutil=%s, df=%s" % (usage.free, free)) + if abs(usage.used - used) > 10 * 1024 * 1024: + raise self.fail("psutil=%s, df=%s" % (usage.used, used)) + + @unittest.skipIf(not which('sysctl'), "sysctl cmd not available") + def test_cpu_count_logical(self): + syst = sysctl("hw.ncpu") + self.assertEqual(psutil.cpu_count(logical=True), syst) + + @unittest.skipIf(not which('sysctl'), "sysctl cmd not available") + @unittest.skipIf(NETBSD, "skipped on NETBSD") # we check /proc/meminfo + def test_virtual_memory_total(self): + num = sysctl('hw.physmem') + self.assertEqual(num, psutil.virtual_memory().total) + + @unittest.skipIf(not which('ifconfig'), "ifconfig cmd not available") + def test_net_if_stats(self): + for name, stats in psutil.net_if_stats().items(): + try: + out = sh("ifconfig %s" % name) + except RuntimeError: + pass + else: + self.assertEqual(stats.isup, 'RUNNING' in out, msg=out) + if "mtu" in out: + self.assertEqual( + stats.mtu, int(re.findall(r'mtu (\d+)', out)[0]) + ) + + +# ===================================================================== +# --- FreeBSD +# ===================================================================== + + +@unittest.skipIf(not FREEBSD, "FREEBSD only") +class FreeBSDPsutilTestCase(PsutilTestCase): + @classmethod + def setUpClass(cls): + cls.pid = spawn_testproc().pid + + @classmethod + def tearDownClass(cls): + terminate(cls.pid) + + @retry_on_failure() + def test_memory_maps(self): + out = sh('procstat -v %s' % self.pid) + maps = psutil.Process(self.pid).memory_maps(grouped=False) + lines = out.split('\n')[1:] + while lines: + line = lines.pop() + fields = line.split() + _, start, stop, perms, res = fields[:5] + map = maps.pop() + self.assertEqual("%s-%s" % (start, stop), map.addr) + self.assertEqual(int(res), map.rss) + if not map.path.startswith('['): + self.assertEqual(fields[10], map.path) + + def test_exe(self): + out = sh('procstat -b %s' % self.pid) + self.assertEqual( + psutil.Process(self.pid).exe(), out.split('\n')[1].split()[-1] + ) + + def test_cmdline(self): + out = sh('procstat -c %s' % self.pid) + self.assertEqual( + ' '.join(psutil.Process(self.pid).cmdline()), + ' '.join(out.split('\n')[1].split()[2:]), + ) + + def test_uids_gids(self): + out = sh('procstat -s %s' % self.pid) + euid, ruid, suid, egid, rgid, sgid = out.split('\n')[1].split()[2:8] + p = psutil.Process(self.pid) + uids = p.uids() + gids = p.gids() + self.assertEqual(uids.real, int(ruid)) + self.assertEqual(uids.effective, int(euid)) + self.assertEqual(uids.saved, int(suid)) + self.assertEqual(gids.real, int(rgid)) + self.assertEqual(gids.effective, int(egid)) + self.assertEqual(gids.saved, int(sgid)) + + @retry_on_failure() + def test_ctx_switches(self): + tested = [] + out = sh('procstat -r %s' % self.pid) + p = psutil.Process(self.pid) + for line in out.split('\n'): + line = line.lower().strip() + if ' voluntary context' in line: + pstat_value = int(line.split()[-1]) + psutil_value = p.num_ctx_switches().voluntary + self.assertEqual(pstat_value, psutil_value) + tested.append(None) + elif ' involuntary context' in line: + pstat_value = int(line.split()[-1]) + psutil_value = p.num_ctx_switches().involuntary + self.assertEqual(pstat_value, psutil_value) + tested.append(None) + if len(tested) != 2: + raise RuntimeError("couldn't find lines match in procstat out") + + @retry_on_failure() + def test_cpu_times(self): + tested = [] + out = sh('procstat -r %s' % self.pid) + p = psutil.Process(self.pid) + for line in out.split('\n'): + line = line.lower().strip() + if 'user time' in line: + pstat_value = float('0.' + line.split()[-1].split('.')[-1]) + psutil_value = p.cpu_times().user + self.assertEqual(pstat_value, psutil_value) + tested.append(None) + elif 'system time' in line: + pstat_value = float('0.' + line.split()[-1].split('.')[-1]) + psutil_value = p.cpu_times().system + self.assertEqual(pstat_value, psutil_value) + tested.append(None) + if len(tested) != 2: + raise RuntimeError("couldn't find lines match in procstat out") + + +@unittest.skipIf(not FREEBSD, "FREEBSD only") +class FreeBSDSystemTestCase(PsutilTestCase): + @staticmethod + def parse_swapinfo(): + # the last line is always the total + output = sh("swapinfo -k").splitlines()[-1] + parts = re.split(r'\s+', output) + + if not parts: + raise ValueError("Can't parse swapinfo: %s" % output) + + # the size is in 1k units, so multiply by 1024 + total, used, free = (int(p) * 1024 for p in parts[1:4]) + return total, used, free + + def test_cpu_frequency_against_sysctl(self): + # Currently only cpu 0 is frequency is supported in FreeBSD + # All other cores use the same frequency. + sensor = "dev.cpu.0.freq" + try: + sysctl_result = int(sysctl(sensor)) + except RuntimeError: + self.skipTest("frequencies not supported by kernel") + self.assertEqual(psutil.cpu_freq().current, sysctl_result) + + sensor = "dev.cpu.0.freq_levels" + sysctl_result = sysctl(sensor) + # sysctl returns a string of the format: + # / /... + # Ordered highest available to lowest available. + max_freq = int(sysctl_result.split()[0].split("/")[0]) + min_freq = int(sysctl_result.split()[-1].split("/")[0]) + self.assertEqual(psutil.cpu_freq().max, max_freq) + self.assertEqual(psutil.cpu_freq().min, min_freq) + + # --- virtual_memory(); tests against sysctl + + @retry_on_failure() + def test_vmem_active(self): + syst = sysctl("vm.stats.vm.v_active_count") * PAGESIZE + self.assertAlmostEqual( + psutil.virtual_memory().active, syst, delta=TOLERANCE_SYS_MEM + ) + + @retry_on_failure() + def test_vmem_inactive(self): + syst = sysctl("vm.stats.vm.v_inactive_count") * PAGESIZE + self.assertAlmostEqual( + psutil.virtual_memory().inactive, syst, delta=TOLERANCE_SYS_MEM + ) + + @retry_on_failure() + def test_vmem_wired(self): + syst = sysctl("vm.stats.vm.v_wire_count") * PAGESIZE + self.assertAlmostEqual( + psutil.virtual_memory().wired, syst, delta=TOLERANCE_SYS_MEM + ) + + @retry_on_failure() + def test_vmem_cached(self): + syst = sysctl("vm.stats.vm.v_cache_count") * PAGESIZE + self.assertAlmostEqual( + psutil.virtual_memory().cached, syst, delta=TOLERANCE_SYS_MEM + ) + + @retry_on_failure() + def test_vmem_free(self): + syst = sysctl("vm.stats.vm.v_free_count") * PAGESIZE + self.assertAlmostEqual( + psutil.virtual_memory().free, syst, delta=TOLERANCE_SYS_MEM + ) + + @retry_on_failure() + def test_vmem_buffers(self): + syst = sysctl("vfs.bufspace") + self.assertAlmostEqual( + psutil.virtual_memory().buffers, syst, delta=TOLERANCE_SYS_MEM + ) + + # --- virtual_memory(); tests against muse + + @unittest.skipIf(not MUSE_AVAILABLE, "muse not installed") + def test_muse_vmem_total(self): + num = muse('Total') + self.assertEqual(psutil.virtual_memory().total, num) + + @unittest.skipIf(not MUSE_AVAILABLE, "muse not installed") + @retry_on_failure() + def test_muse_vmem_active(self): + num = muse('Active') + self.assertAlmostEqual( + psutil.virtual_memory().active, num, delta=TOLERANCE_SYS_MEM + ) + + @unittest.skipIf(not MUSE_AVAILABLE, "muse not installed") + @retry_on_failure() + def test_muse_vmem_inactive(self): + num = muse('Inactive') + self.assertAlmostEqual( + psutil.virtual_memory().inactive, num, delta=TOLERANCE_SYS_MEM + ) + + @unittest.skipIf(not MUSE_AVAILABLE, "muse not installed") + @retry_on_failure() + def test_muse_vmem_wired(self): + num = muse('Wired') + self.assertAlmostEqual( + psutil.virtual_memory().wired, num, delta=TOLERANCE_SYS_MEM + ) + + @unittest.skipIf(not MUSE_AVAILABLE, "muse not installed") + @retry_on_failure() + def test_muse_vmem_cached(self): + num = muse('Cache') + self.assertAlmostEqual( + psutil.virtual_memory().cached, num, delta=TOLERANCE_SYS_MEM + ) + + @unittest.skipIf(not MUSE_AVAILABLE, "muse not installed") + @retry_on_failure() + def test_muse_vmem_free(self): + num = muse('Free') + self.assertAlmostEqual( + psutil.virtual_memory().free, num, delta=TOLERANCE_SYS_MEM + ) + + @unittest.skipIf(not MUSE_AVAILABLE, "muse not installed") + @retry_on_failure() + def test_muse_vmem_buffers(self): + num = muse('Buffer') + self.assertAlmostEqual( + psutil.virtual_memory().buffers, num, delta=TOLERANCE_SYS_MEM + ) + + def test_cpu_stats_ctx_switches(self): + self.assertAlmostEqual( + psutil.cpu_stats().ctx_switches, + sysctl('vm.stats.sys.v_swtch'), + delta=1000, + ) + + def test_cpu_stats_interrupts(self): + self.assertAlmostEqual( + psutil.cpu_stats().interrupts, + sysctl('vm.stats.sys.v_intr'), + delta=1000, + ) + + def test_cpu_stats_soft_interrupts(self): + self.assertAlmostEqual( + psutil.cpu_stats().soft_interrupts, + sysctl('vm.stats.sys.v_soft'), + delta=1000, + ) + + @retry_on_failure() + def test_cpu_stats_syscalls(self): + # pretty high tolerance but it looks like it's OK. + self.assertAlmostEqual( + psutil.cpu_stats().syscalls, + sysctl('vm.stats.sys.v_syscall'), + delta=200000, + ) + + # def test_cpu_stats_traps(self): + # self.assertAlmostEqual(psutil.cpu_stats().traps, + # sysctl('vm.stats.sys.v_trap'), delta=1000) + + # --- swap memory + + def test_swapmem_free(self): + total, used, free = self.parse_swapinfo() + self.assertAlmostEqual( + psutil.swap_memory().free, free, delta=TOLERANCE_SYS_MEM + ) + + def test_swapmem_used(self): + total, used, free = self.parse_swapinfo() + self.assertAlmostEqual( + psutil.swap_memory().used, used, delta=TOLERANCE_SYS_MEM + ) + + def test_swapmem_total(self): + total, used, free = self.parse_swapinfo() + self.assertAlmostEqual( + psutil.swap_memory().total, total, delta=TOLERANCE_SYS_MEM + ) + + # --- others + + def test_boot_time(self): + s = sysctl('sysctl kern.boottime') + s = s[s.find(" sec = ") + 7 :] + s = s[: s.find(',')] + btime = int(s) + self.assertEqual(btime, psutil.boot_time()) + + # --- sensors_battery + + @unittest.skipIf(not HAS_BATTERY, "no battery") + def test_sensors_battery(self): + def secs2hours(secs): + m, s = divmod(secs, 60) + h, m = divmod(m, 60) + return "%d:%02d" % (h, m) + + out = sh("acpiconf -i 0") + fields = dict( + [(x.split('\t')[0], x.split('\t')[-1]) for x in out.split("\n")] + ) + metrics = psutil.sensors_battery() + percent = int(fields['Remaining capacity:'].replace('%', '')) + remaining_time = fields['Remaining time:'] + self.assertEqual(metrics.percent, percent) + if remaining_time == 'unknown': + self.assertEqual(metrics.secsleft, psutil.POWER_TIME_UNLIMITED) + else: + self.assertEqual(secs2hours(metrics.secsleft), remaining_time) + + @unittest.skipIf(not HAS_BATTERY, "no battery") + def test_sensors_battery_against_sysctl(self): + self.assertEqual( + psutil.sensors_battery().percent, sysctl("hw.acpi.battery.life") + ) + self.assertEqual( + psutil.sensors_battery().power_plugged, + sysctl("hw.acpi.acline") == 1, + ) + secsleft = psutil.sensors_battery().secsleft + if secsleft < 0: + self.assertEqual(sysctl("hw.acpi.battery.time"), -1) + else: + self.assertEqual(secsleft, sysctl("hw.acpi.battery.time") * 60) + + @unittest.skipIf(HAS_BATTERY, "has battery") + def test_sensors_battery_no_battery(self): + # If no battery is present one of these calls is supposed + # to fail, see: + # https://github.com/giampaolo/psutil/issues/1074 + with self.assertRaises(RuntimeError): + sysctl("hw.acpi.battery.life") + sysctl("hw.acpi.battery.time") + sysctl("hw.acpi.acline") + self.assertIsNone(psutil.sensors_battery()) + + # --- sensors_temperatures + + def test_sensors_temperatures_against_sysctl(self): + num_cpus = psutil.cpu_count(True) + for cpu in range(num_cpus): + sensor = "dev.cpu.%s.temperature" % cpu + # sysctl returns a string in the format 46.0C + try: + sysctl_result = int(float(sysctl(sensor)[:-1])) + except RuntimeError: + self.skipTest("temperatures not supported by kernel") + self.assertAlmostEqual( + psutil.sensors_temperatures()["coretemp"][cpu].current, + sysctl_result, + delta=10, + ) + + sensor = "dev.cpu.%s.coretemp.tjmax" % cpu + sysctl_result = int(float(sysctl(sensor)[:-1])) + self.assertEqual( + psutil.sensors_temperatures()["coretemp"][cpu].high, + sysctl_result, + ) + + +# ===================================================================== +# --- OpenBSD +# ===================================================================== + + +@unittest.skipIf(not OPENBSD, "OPENBSD only") +class OpenBSDTestCase(PsutilTestCase): + def test_boot_time(self): + s = sysctl('kern.boottime') + sys_bt = datetime.datetime.strptime(s, "%a %b %d %H:%M:%S %Y") + psutil_bt = datetime.datetime.fromtimestamp(psutil.boot_time()) + self.assertEqual(sys_bt, psutil_bt) + + +# ===================================================================== +# --- NetBSD +# ===================================================================== + + +@unittest.skipIf(not NETBSD, "NETBSD only") +class NetBSDTestCase(PsutilTestCase): + @staticmethod + def parse_meminfo(look_for): + with open('/proc/meminfo') as f: + for line in f: + if line.startswith(look_for): + return int(line.split()[1]) * 1024 + raise ValueError("can't find %s" % look_for) + + # --- virtual mem + + def test_vmem_total(self): + self.assertEqual( + psutil.virtual_memory().total, self.parse_meminfo("MemTotal:") + ) + + def test_vmem_free(self): + self.assertAlmostEqual( + psutil.virtual_memory().free, + self.parse_meminfo("MemFree:"), + delta=TOLERANCE_SYS_MEM, + ) + + def test_vmem_buffers(self): + self.assertAlmostEqual( + psutil.virtual_memory().buffers, + self.parse_meminfo("Buffers:"), + delta=TOLERANCE_SYS_MEM, + ) + + def test_vmem_shared(self): + self.assertAlmostEqual( + psutil.virtual_memory().shared, + self.parse_meminfo("MemShared:"), + delta=TOLERANCE_SYS_MEM, + ) + + def test_vmem_cached(self): + self.assertAlmostEqual( + psutil.virtual_memory().cached, + self.parse_meminfo("Cached:"), + delta=TOLERANCE_SYS_MEM, + ) + + # --- swap mem + + def test_swapmem_total(self): + self.assertAlmostEqual( + psutil.swap_memory().total, + self.parse_meminfo("SwapTotal:"), + delta=TOLERANCE_SYS_MEM, + ) + + def test_swapmem_free(self): + self.assertAlmostEqual( + psutil.swap_memory().free, + self.parse_meminfo("SwapFree:"), + delta=TOLERANCE_SYS_MEM, + ) + + def test_swapmem_used(self): + smem = psutil.swap_memory() + self.assertEqual(smem.used, smem.total - smem.free) + + # --- others + + def test_cpu_stats_interrupts(self): + with open('/proc/stat', 'rb') as f: + for line in f: + if line.startswith(b'intr'): + interrupts = int(line.split()[1]) + break + else: + raise ValueError("couldn't find line") + self.assertAlmostEqual( + psutil.cpu_stats().interrupts, interrupts, delta=1000 + ) + + def test_cpu_stats_ctx_switches(self): + with open('/proc/stat', 'rb') as f: + for line in f: + if line.startswith(b'ctxt'): + ctx_switches = int(line.split()[1]) + break + else: + raise ValueError("couldn't find line") + self.assertAlmostEqual( + psutil.cpu_stats().ctx_switches, ctx_switches, delta=1000 + ) + + +if __name__ == '__main__': + from psutil.tests.runner import run_from_name + + run_from_name(__file__) diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_bsd.pyc b/addon/globalPlugins/soundmanager/psutil/tests/test_bsd.pyc deleted file mode 100644 index 4a213a0..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/tests/test_bsd.pyc and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_connections.py b/addon/globalPlugins/soundmanager/psutil/tests/test_connections.py index 68eea78..876467a 100644 --- a/addon/globalPlugins/soundmanager/psutil/tests/test_connections.py +++ b/addon/globalPlugins/soundmanager/psutil/tests/test_connections.py @@ -1,526 +1,576 @@ -#!/usr/bin/env python - -# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -"""Tests for net_connections() and Process.connections() APIs.""" - -import os -import socket -import textwrap -from contextlib import closing -from socket import AF_INET -from socket import AF_INET6 -from socket import SOCK_DGRAM -from socket import SOCK_STREAM - -import psutil -from psutil import FREEBSD -from psutil import LINUX -from psutil import MACOS -from psutil import NETBSD -from psutil import OPENBSD -from psutil import POSIX -from psutil import SUNOS -from psutil import WINDOWS -from psutil._common import supports_ipv6 -from psutil._compat import PY3 -from psutil.tests import AF_UNIX -from psutil.tests import bind_socket -from psutil.tests import bind_unix_socket -from psutil.tests import check_connection_ntuple -from psutil.tests import create_sockets -from psutil.tests import get_free_port -from psutil.tests import HAS_CONNECTIONS_UNIX -from psutil.tests import pyrun -from psutil.tests import reap_children -from psutil.tests import safe_rmpath -from psutil.tests import skip_on_access_denied -from psutil.tests import tcp_socketpair -from psutil.tests import TESTFN -from psutil.tests import TRAVIS -from psutil.tests import unittest -from psutil.tests import unix_socket_path -from psutil.tests import unix_socketpair -from psutil.tests import wait_for_file - - -thisproc = psutil.Process() - - -class Base(object): - - def setUp(self): - safe_rmpath(TESTFN) - if not NETBSD: - # NetBSD opens a UNIX socket to /var/log/run. - cons = thisproc.connections(kind='all') - assert not cons, cons - - def tearDown(self): - safe_rmpath(TESTFN) - reap_children() - if not NETBSD: - # Make sure we closed all resources. - # NetBSD opens a UNIX socket to /var/log/run. - cons = thisproc.connections(kind='all') - assert not cons, cons - - def get_conn_from_sock(self, sock): - cons = thisproc.connections(kind='all') - smap = dict([(c.fd, c) for c in cons]) - if NETBSD: - # NetBSD opens a UNIX socket to /var/log/run - # so there may be more connections. - return smap[sock.fileno()] - else: - self.assertEqual(len(cons), 1) - if cons[0].fd != -1: - self.assertEqual(smap[sock.fileno()].fd, sock.fileno()) - return cons[0] - - def check_socket(self, sock, conn=None): - """Given a socket, makes sure it matches the one obtained - via psutil. It assumes this process created one connection - only (the one supposed to be checked). - """ - if conn is None: - conn = self.get_conn_from_sock(sock) - check_connection_ntuple(conn) - - # fd, family, type - if conn.fd != -1: - self.assertEqual(conn.fd, sock.fileno()) - self.assertEqual(conn.family, sock.family) - # see: http://bugs.python.org/issue30204 - self.assertEqual( - conn.type, sock.getsockopt(socket.SOL_SOCKET, socket.SO_TYPE)) - - # local address - laddr = sock.getsockname() - if not laddr and PY3 and isinstance(laddr, bytes): - # See: http://bugs.python.org/issue30205 - laddr = laddr.decode() - if sock.family == AF_INET6: - laddr = laddr[:2] - if sock.family == AF_UNIX and OPENBSD: - # No addresses are set for UNIX sockets on OpenBSD. - pass - else: - self.assertEqual(conn.laddr, laddr) - - # XXX Solaris can't retrieve system-wide UNIX sockets - if sock.family == AF_UNIX and HAS_CONNECTIONS_UNIX: - cons = thisproc.connections(kind='all') - self.compare_procsys_connections(os.getpid(), cons) - return conn - - def compare_procsys_connections(self, pid, proc_cons, kind='all'): - """Given a process PID and its list of connections compare - those against system-wide connections retrieved via - psutil.net_connections. - """ - try: - sys_cons = psutil.net_connections(kind=kind) - except psutil.AccessDenied: - # On MACOS, system-wide connections are retrieved by iterating - # over all processes - if MACOS: - return - else: - raise - # Filter for this proc PID and exlucde PIDs from the tuple. - sys_cons = [c[:-1] for c in sys_cons if c.pid == pid] - sys_cons.sort() - proc_cons.sort() - self.assertEqual(proc_cons, sys_cons) - - -# ===================================================================== -# --- Test unconnected sockets -# ===================================================================== - - -class TestUnconnectedSockets(Base, unittest.TestCase): - """Tests sockets which are open but not connected to anything.""" - - def test_tcp_v4(self): - addr = ("127.0.0.1", get_free_port()) - with closing(bind_socket(AF_INET, SOCK_STREAM, addr=addr)) as sock: - conn = self.check_socket(sock) - assert not conn.raddr - self.assertEqual(conn.status, psutil.CONN_LISTEN) - - @unittest.skipIf(not supports_ipv6(), "IPv6 not supported") - def test_tcp_v6(self): - addr = ("::1", get_free_port()) - with closing(bind_socket(AF_INET6, SOCK_STREAM, addr=addr)) as sock: - conn = self.check_socket(sock) - assert not conn.raddr - self.assertEqual(conn.status, psutil.CONN_LISTEN) - - def test_udp_v4(self): - addr = ("127.0.0.1", get_free_port()) - with closing(bind_socket(AF_INET, SOCK_DGRAM, addr=addr)) as sock: - conn = self.check_socket(sock) - assert not conn.raddr - self.assertEqual(conn.status, psutil.CONN_NONE) - - @unittest.skipIf(not supports_ipv6(), "IPv6 not supported") - def test_udp_v6(self): - addr = ("::1", get_free_port()) - with closing(bind_socket(AF_INET6, SOCK_DGRAM, addr=addr)) as sock: - conn = self.check_socket(sock) - assert not conn.raddr - self.assertEqual(conn.status, psutil.CONN_NONE) - - @unittest.skipIf(not POSIX, 'POSIX only') - def test_unix_tcp(self): - with unix_socket_path() as name: - with closing(bind_unix_socket(name, type=SOCK_STREAM)) as sock: - conn = self.check_socket(sock) - assert not conn.raddr - self.assertEqual(conn.status, psutil.CONN_NONE) - - @unittest.skipIf(not POSIX, 'POSIX only') - def test_unix_udp(self): - with unix_socket_path() as name: - with closing(bind_unix_socket(name, type=SOCK_STREAM)) as sock: - conn = self.check_socket(sock) - assert not conn.raddr - self.assertEqual(conn.status, psutil.CONN_NONE) - - -# ===================================================================== -# --- Test connected sockets -# ===================================================================== - - -class TestConnectedSocketPairs(Base, unittest.TestCase): - """Test socket pairs which are are actually connected to - each other. - """ - - # On SunOS, even after we close() it, the server socket stays around - # in TIME_WAIT state. - @unittest.skipIf(SUNOS, "unreliable on SUONS") - def test_tcp(self): - addr = ("127.0.0.1", get_free_port()) - assert not thisproc.connections(kind='tcp4') - server, client = tcp_socketpair(AF_INET, addr=addr) - try: - cons = thisproc.connections(kind='tcp4') - self.assertEqual(len(cons), 2) - self.assertEqual(cons[0].status, psutil.CONN_ESTABLISHED) - self.assertEqual(cons[1].status, psutil.CONN_ESTABLISHED) - # May not be fast enough to change state so it stays - # commenteed. - # client.close() - # cons = thisproc.connections(kind='all') - # self.assertEqual(len(cons), 1) - # self.assertEqual(cons[0].status, psutil.CONN_CLOSE_WAIT) - finally: - server.close() - client.close() - - @unittest.skipIf(not POSIX, 'POSIX only') - def test_unix(self): - with unix_socket_path() as name: - server, client = unix_socketpair(name) - try: - cons = thisproc.connections(kind='unix') - assert not (cons[0].laddr and cons[0].raddr) - assert not (cons[1].laddr and cons[1].raddr) - if NETBSD: - # On NetBSD creating a UNIX socket will cause - # a UNIX connection to /var/run/log. - cons = [c for c in cons if c.raddr != '/var/run/log'] - self.assertEqual(len(cons), 2) - if LINUX or FREEBSD or SUNOS: - # remote path is never set - self.assertEqual(cons[0].raddr, "") - self.assertEqual(cons[1].raddr, "") - # one local address should though - self.assertEqual(name, cons[0].laddr or cons[1].laddr) - elif OPENBSD: - # No addresses whatsoever here. - for addr in (cons[0].laddr, cons[0].raddr, - cons[1].laddr, cons[1].raddr): - self.assertEqual(addr, "") - else: - # On other systems either the laddr or raddr - # of both peers are set. - self.assertEqual(cons[0].laddr or cons[1].laddr, name) - self.assertEqual(cons[0].raddr or cons[1].raddr, name) - finally: - server.close() - client.close() - - @skip_on_access_denied(only_if=MACOS) - def test_combos(self): - def check_conn(proc, conn, family, type, laddr, raddr, status, kinds): - all_kinds = ("all", "inet", "inet4", "inet6", "tcp", "tcp4", - "tcp6", "udp", "udp4", "udp6") - check_connection_ntuple(conn) - self.assertEqual(conn.family, family) - self.assertEqual(conn.type, type) - self.assertEqual(conn.laddr, laddr) - self.assertEqual(conn.raddr, raddr) - self.assertEqual(conn.status, status) - for kind in all_kinds: - cons = proc.connections(kind=kind) - if kind in kinds: - assert cons - else: - assert not cons, cons - # compare against system-wide connections - # XXX Solaris can't retrieve system-wide UNIX - # sockets. - if HAS_CONNECTIONS_UNIX: - self.compare_procsys_connections(proc.pid, [conn]) - - tcp_template = textwrap.dedent(""" - import socket, time - s = socket.socket($family, socket.SOCK_STREAM) - s.bind(('$addr', 0)) - s.listen(1) - with open('$testfn', 'w') as f: - f.write(str(s.getsockname()[:2])) - time.sleep(60) - """) - - udp_template = textwrap.dedent(""" - import socket, time - s = socket.socket($family, socket.SOCK_DGRAM) - s.bind(('$addr', 0)) - with open('$testfn', 'w') as f: - f.write(str(s.getsockname()[:2])) - time.sleep(60) - """) - - from string import Template - testfile = os.path.basename(TESTFN) - tcp4_template = Template(tcp_template).substitute( - family=int(AF_INET), addr="127.0.0.1", testfn=testfile) - udp4_template = Template(udp_template).substitute( - family=int(AF_INET), addr="127.0.0.1", testfn=testfile) - tcp6_template = Template(tcp_template).substitute( - family=int(AF_INET6), addr="::1", testfn=testfile) - udp6_template = Template(udp_template).substitute( - family=int(AF_INET6), addr="::1", testfn=testfile) - - # launch various subprocess instantiating a socket of various - # families and types to enrich psutil results - tcp4_proc = pyrun(tcp4_template) - tcp4_addr = eval(wait_for_file(testfile)) - udp4_proc = pyrun(udp4_template) - udp4_addr = eval(wait_for_file(testfile)) - if supports_ipv6(): - tcp6_proc = pyrun(tcp6_template) - tcp6_addr = eval(wait_for_file(testfile)) - udp6_proc = pyrun(udp6_template) - udp6_addr = eval(wait_for_file(testfile)) - else: - tcp6_proc = None - udp6_proc = None - tcp6_addr = None - udp6_addr = None - - for p in thisproc.children(): - cons = p.connections() - self.assertEqual(len(cons), 1) - for conn in cons: - # TCP v4 - if p.pid == tcp4_proc.pid: - check_conn(p, conn, AF_INET, SOCK_STREAM, tcp4_addr, (), - psutil.CONN_LISTEN, - ("all", "inet", "inet4", "tcp", "tcp4")) - # UDP v4 - elif p.pid == udp4_proc.pid: - check_conn(p, conn, AF_INET, SOCK_DGRAM, udp4_addr, (), - psutil.CONN_NONE, - ("all", "inet", "inet4", "udp", "udp4")) - # TCP v6 - elif p.pid == getattr(tcp6_proc, "pid", None): - check_conn(p, conn, AF_INET6, SOCK_STREAM, tcp6_addr, (), - psutil.CONN_LISTEN, - ("all", "inet", "inet6", "tcp", "tcp6")) - # UDP v6 - elif p.pid == getattr(udp6_proc, "pid", None): - check_conn(p, conn, AF_INET6, SOCK_DGRAM, udp6_addr, (), - psutil.CONN_NONE, - ("all", "inet", "inet6", "udp", "udp6")) - - # err - self.assertRaises(ValueError, p.connections, kind='???') - - def test_multi_sockets_filtering(self): - with create_sockets() as socks: - cons = thisproc.connections(kind='all') - self.assertEqual(len(cons), len(socks)) - # tcp - cons = thisproc.connections(kind='tcp') - self.assertEqual(len(cons), 2 if supports_ipv6() else 1) - for conn in cons: - self.assertIn(conn.family, (AF_INET, AF_INET6)) - self.assertEqual(conn.type, SOCK_STREAM) - # tcp4 - cons = thisproc.connections(kind='tcp4') - self.assertEqual(len(cons), 1) - self.assertEqual(cons[0].family, AF_INET) - self.assertEqual(cons[0].type, SOCK_STREAM) - # tcp6 - if supports_ipv6(): - cons = thisproc.connections(kind='tcp6') - self.assertEqual(len(cons), 1) - self.assertEqual(cons[0].family, AF_INET6) - self.assertEqual(cons[0].type, SOCK_STREAM) - # udp - cons = thisproc.connections(kind='udp') - self.assertEqual(len(cons), 2 if supports_ipv6() else 1) - for conn in cons: - self.assertIn(conn.family, (AF_INET, AF_INET6)) - self.assertEqual(conn.type, SOCK_DGRAM) - # udp4 - cons = thisproc.connections(kind='udp4') - self.assertEqual(len(cons), 1) - self.assertEqual(cons[0].family, AF_INET) - self.assertEqual(cons[0].type, SOCK_DGRAM) - # udp6 - if supports_ipv6(): - cons = thisproc.connections(kind='udp6') - self.assertEqual(len(cons), 1) - self.assertEqual(cons[0].family, AF_INET6) - self.assertEqual(cons[0].type, SOCK_DGRAM) - # inet - cons = thisproc.connections(kind='inet') - self.assertEqual(len(cons), 4 if supports_ipv6() else 2) - for conn in cons: - self.assertIn(conn.family, (AF_INET, AF_INET6)) - self.assertIn(conn.type, (SOCK_STREAM, SOCK_DGRAM)) - # inet6 - if supports_ipv6(): - cons = thisproc.connections(kind='inet6') - self.assertEqual(len(cons), 2) - for conn in cons: - self.assertEqual(conn.family, AF_INET6) - self.assertIn(conn.type, (SOCK_STREAM, SOCK_DGRAM)) - # unix - if HAS_CONNECTIONS_UNIX: - cons = thisproc.connections(kind='unix') - self.assertEqual(len(cons), 3) - for conn in cons: - self.assertEqual(conn.family, AF_UNIX) - self.assertIn(conn.type, (SOCK_STREAM, SOCK_DGRAM)) - - -# ===================================================================== -# --- Miscellaneous tests -# ===================================================================== - - -class TestSystemWideConnections(Base, unittest.TestCase): - """Tests for net_connections().""" - - @skip_on_access_denied() - def test_it(self): - def check(cons, families, types_): - AF_UNIX = getattr(socket, 'AF_UNIX', object()) - for conn in cons: - self.assertIn(conn.family, families, msg=conn) - if conn.family != AF_UNIX: - self.assertIn(conn.type, types_, msg=conn) - check_connection_ntuple(conn) - - with create_sockets(): - from psutil._common import conn_tmap - for kind, groups in conn_tmap.items(): - # XXX: SunOS does not retrieve UNIX sockets. - if kind == 'unix' and not HAS_CONNECTIONS_UNIX: - continue - families, types_ = groups - cons = psutil.net_connections(kind) - self.assertEqual(len(cons), len(set(cons))) - check(cons, families, types_) - - self.assertRaises(ValueError, psutil.net_connections, kind='???') - - @skip_on_access_denied() - def test_multi_socks(self): - with create_sockets() as socks: - cons = [x for x in psutil.net_connections(kind='all') - if x.pid == os.getpid()] - self.assertEqual(len(cons), len(socks)) - - @skip_on_access_denied() - # See: https://travis-ci.org/giampaolo/psutil/jobs/237566297 - @unittest.skipIf(MACOS and TRAVIS, "unreliable on MACOS + TRAVIS") - def test_multi_sockets_procs(self): - # Creates multiple sub processes, each creating different - # sockets. For each process check that proc.connections() - # and net_connections() return the same results. - # This is done mainly to check whether net_connections()'s - # pid is properly set, see: - # https://github.com/giampaolo/psutil/issues/1013 - with create_sockets() as socks: - expected = len(socks) - pids = [] - times = 10 - for i in range(times): - fname = os.path.realpath(TESTFN) + str(i) - src = textwrap.dedent("""\ - import time, os - from psutil.tests import create_sockets - with create_sockets(): - with open(r'%s', 'w') as f: - f.write(str(os.getpid())) - time.sleep(60) - """ % fname) - sproc = pyrun(src) - pids.append(sproc.pid) - self.addCleanup(safe_rmpath, fname) - - # sync - for i in range(times): - fname = TESTFN + str(i) - wait_for_file(fname) - - syscons = [x for x in psutil.net_connections(kind='all') if x.pid - in pids] - for pid in pids: - self.assertEqual(len([x for x in syscons if x.pid == pid]), - expected) - p = psutil.Process(pid) - self.assertEqual(len(p.connections('all')), expected) - - -# ===================================================================== -# --- Miscellaneous tests -# ===================================================================== - - -class TestMisc(unittest.TestCase): - - def test_connection_constants(self): - ints = [] - strs = [] - for name in dir(psutil): - if name.startswith('CONN_'): - num = getattr(psutil, name) - str_ = str(num) - assert str_.isupper(), str_ - self.assertNotIn(str, strs) - self.assertNotIn(num, ints) - ints.append(num) - strs.append(str_) - if SUNOS: - psutil.CONN_IDLE - psutil.CONN_BOUND - if WINDOWS: - psutil.CONN_DELETE_TCB - - -if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Tests for net_connections() and Process.connections() APIs.""" + +import os +import socket +import textwrap +import unittest +from contextlib import closing +from socket import AF_INET +from socket import AF_INET6 +from socket import SOCK_DGRAM +from socket import SOCK_STREAM + +import psutil +from psutil import FREEBSD +from psutil import LINUX +from psutil import MACOS +from psutil import NETBSD +from psutil import OPENBSD +from psutil import POSIX +from psutil import SUNOS +from psutil import WINDOWS +from psutil._common import supports_ipv6 +from psutil._compat import PY3 +from psutil.tests import AF_UNIX +from psutil.tests import HAS_CONNECTIONS_UNIX +from psutil.tests import SKIP_SYSCONS +from psutil.tests import PsutilTestCase +from psutil.tests import bind_socket +from psutil.tests import bind_unix_socket +from psutil.tests import check_connection_ntuple +from psutil.tests import create_sockets +from psutil.tests import filter_proc_connections +from psutil.tests import reap_children +from psutil.tests import retry_on_failure +from psutil.tests import serialrun +from psutil.tests import skip_on_access_denied +from psutil.tests import tcp_socketpair +from psutil.tests import unix_socketpair +from psutil.tests import wait_for_file + + +SOCK_SEQPACKET = getattr(socket, "SOCK_SEQPACKET", object()) + + +def this_proc_connections(kind): + cons = psutil.Process().connections(kind=kind) + if kind in ("all", "unix"): + return filter_proc_connections(cons) + return cons + + +@serialrun +class ConnectionTestCase(PsutilTestCase): + def setUp(self): + self.assertEqual(this_proc_connections(kind='all'), []) + + def tearDown(self): + # Make sure we closed all resources. + self.assertEqual(this_proc_connections(kind='all'), []) + + def compare_procsys_connections(self, pid, proc_cons, kind='all'): + """Given a process PID and its list of connections compare + those against system-wide connections retrieved via + psutil.net_connections. + """ + try: + sys_cons = psutil.net_connections(kind=kind) + except psutil.AccessDenied: + # On MACOS, system-wide connections are retrieved by iterating + # over all processes + if MACOS: + return + else: + raise + # Filter for this proc PID and exlucde PIDs from the tuple. + sys_cons = [c[:-1] for c in sys_cons if c.pid == pid] + sys_cons.sort() + proc_cons.sort() + self.assertEqual(proc_cons, sys_cons) + + +class TestBasicOperations(ConnectionTestCase): + @unittest.skipIf(SKIP_SYSCONS, "requires root") + def test_system(self): + with create_sockets(): + for conn in psutil.net_connections(kind='all'): + check_connection_ntuple(conn) + + def test_process(self): + with create_sockets(): + for conn in this_proc_connections(kind='all'): + check_connection_ntuple(conn) + + def test_invalid_kind(self): + self.assertRaises(ValueError, this_proc_connections, kind='???') + self.assertRaises(ValueError, psutil.net_connections, kind='???') + + +@serialrun +class TestUnconnectedSockets(ConnectionTestCase): + """Tests sockets which are open but not connected to anything.""" + + def get_conn_from_sock(self, sock): + cons = this_proc_connections(kind='all') + smap = dict([(c.fd, c) for c in cons]) + if NETBSD or FREEBSD: + # NetBSD opens a UNIX socket to /var/log/run + # so there may be more connections. + return smap[sock.fileno()] + else: + self.assertEqual(len(cons), 1) + if cons[0].fd != -1: + self.assertEqual(smap[sock.fileno()].fd, sock.fileno()) + return cons[0] + + def check_socket(self, sock): + """Given a socket, makes sure it matches the one obtained + via psutil. It assumes this process created one connection + only (the one supposed to be checked). + """ + conn = self.get_conn_from_sock(sock) + check_connection_ntuple(conn) + + # fd, family, type + if conn.fd != -1: + self.assertEqual(conn.fd, sock.fileno()) + self.assertEqual(conn.family, sock.family) + # see: http://bugs.python.org/issue30204 + self.assertEqual( + conn.type, sock.getsockopt(socket.SOL_SOCKET, socket.SO_TYPE) + ) + + # local address + laddr = sock.getsockname() + if not laddr and PY3 and isinstance(laddr, bytes): + # See: http://bugs.python.org/issue30205 + laddr = laddr.decode() + if sock.family == AF_INET6: + laddr = laddr[:2] + self.assertEqual(conn.laddr, laddr) + + # XXX Solaris can't retrieve system-wide UNIX sockets + if sock.family == AF_UNIX and HAS_CONNECTIONS_UNIX: + cons = this_proc_connections(kind='all') + self.compare_procsys_connections(os.getpid(), cons, kind='all') + return conn + + def test_tcp_v4(self): + addr = ("127.0.0.1", 0) + with closing(bind_socket(AF_INET, SOCK_STREAM, addr=addr)) as sock: + conn = self.check_socket(sock) + self.assertEqual(conn.raddr, ()) + self.assertEqual(conn.status, psutil.CONN_LISTEN) + + @unittest.skipIf(not supports_ipv6(), "IPv6 not supported") + def test_tcp_v6(self): + addr = ("::1", 0) + with closing(bind_socket(AF_INET6, SOCK_STREAM, addr=addr)) as sock: + conn = self.check_socket(sock) + self.assertEqual(conn.raddr, ()) + self.assertEqual(conn.status, psutil.CONN_LISTEN) + + def test_udp_v4(self): + addr = ("127.0.0.1", 0) + with closing(bind_socket(AF_INET, SOCK_DGRAM, addr=addr)) as sock: + conn = self.check_socket(sock) + self.assertEqual(conn.raddr, ()) + self.assertEqual(conn.status, psutil.CONN_NONE) + + @unittest.skipIf(not supports_ipv6(), "IPv6 not supported") + def test_udp_v6(self): + addr = ("::1", 0) + with closing(bind_socket(AF_INET6, SOCK_DGRAM, addr=addr)) as sock: + conn = self.check_socket(sock) + self.assertEqual(conn.raddr, ()) + self.assertEqual(conn.status, psutil.CONN_NONE) + + @unittest.skipIf(not POSIX, 'POSIX only') + def test_unix_tcp(self): + testfn = self.get_testfn() + with closing(bind_unix_socket(testfn, type=SOCK_STREAM)) as sock: + conn = self.check_socket(sock) + self.assertEqual(conn.raddr, "") + self.assertEqual(conn.status, psutil.CONN_NONE) + + @unittest.skipIf(not POSIX, 'POSIX only') + def test_unix_udp(self): + testfn = self.get_testfn() + with closing(bind_unix_socket(testfn, type=SOCK_STREAM)) as sock: + conn = self.check_socket(sock) + self.assertEqual(conn.raddr, "") + self.assertEqual(conn.status, psutil.CONN_NONE) + + +@serialrun +class TestConnectedSocket(ConnectionTestCase): + """Test socket pairs which are actually connected to + each other. + """ + + # On SunOS, even after we close() it, the server socket stays around + # in TIME_WAIT state. + @unittest.skipIf(SUNOS, "unreliable on SUONS") + def test_tcp(self): + addr = ("127.0.0.1", 0) + self.assertEqual(this_proc_connections(kind='tcp4'), []) + server, client = tcp_socketpair(AF_INET, addr=addr) + try: + cons = this_proc_connections(kind='tcp4') + self.assertEqual(len(cons), 2) + self.assertEqual(cons[0].status, psutil.CONN_ESTABLISHED) + self.assertEqual(cons[1].status, psutil.CONN_ESTABLISHED) + # May not be fast enough to change state so it stays + # commenteed. + # client.close() + # cons = this_proc_connections(kind='all') + # self.assertEqual(len(cons), 1) + # self.assertEqual(cons[0].status, psutil.CONN_CLOSE_WAIT) + finally: + server.close() + client.close() + + @unittest.skipIf(not POSIX, 'POSIX only') + def test_unix(self): + testfn = self.get_testfn() + server, client = unix_socketpair(testfn) + try: + cons = this_proc_connections(kind='unix') + assert not (cons[0].laddr and cons[0].raddr), cons + assert not (cons[1].laddr and cons[1].raddr), cons + if NETBSD or FREEBSD: + # On NetBSD creating a UNIX socket will cause + # a UNIX connection to /var/run/log. + cons = [c for c in cons if c.raddr != '/var/run/log'] + self.assertEqual(len(cons), 2, msg=cons) + if LINUX or FREEBSD or SUNOS or OPENBSD: + # remote path is never set + self.assertEqual(cons[0].raddr, "") + self.assertEqual(cons[1].raddr, "") + # one local address should though + self.assertEqual(testfn, cons[0].laddr or cons[1].laddr) + else: + # On other systems either the laddr or raddr + # of both peers are set. + self.assertEqual(cons[0].laddr or cons[1].laddr, testfn) + finally: + server.close() + client.close() + + +class TestFilters(ConnectionTestCase): + def test_filters(self): + def check(kind, families, types): + for conn in this_proc_connections(kind=kind): + self.assertIn(conn.family, families) + self.assertIn(conn.type, types) + if not SKIP_SYSCONS: + for conn in psutil.net_connections(kind=kind): + self.assertIn(conn.family, families) + self.assertIn(conn.type, types) + + with create_sockets(): + check( + 'all', + [AF_INET, AF_INET6, AF_UNIX], + [SOCK_STREAM, SOCK_DGRAM, SOCK_SEQPACKET], + ) + check('inet', [AF_INET, AF_INET6], [SOCK_STREAM, SOCK_DGRAM]) + check('inet4', [AF_INET], [SOCK_STREAM, SOCK_DGRAM]) + check('tcp', [AF_INET, AF_INET6], [SOCK_STREAM]) + check('tcp4', [AF_INET], [SOCK_STREAM]) + check('tcp6', [AF_INET6], [SOCK_STREAM]) + check('udp', [AF_INET, AF_INET6], [SOCK_DGRAM]) + check('udp4', [AF_INET], [SOCK_DGRAM]) + check('udp6', [AF_INET6], [SOCK_DGRAM]) + if HAS_CONNECTIONS_UNIX: + check( + 'unix', + [AF_UNIX], + [SOCK_STREAM, SOCK_DGRAM, SOCK_SEQPACKET], + ) + + @skip_on_access_denied(only_if=MACOS) + def test_combos(self): + reap_children() + + def check_conn(proc, conn, family, type, laddr, raddr, status, kinds): + all_kinds = ( + "all", + "inet", + "inet4", + "inet6", + "tcp", + "tcp4", + "tcp6", + "udp", + "udp4", + "udp6", + ) + check_connection_ntuple(conn) + self.assertEqual(conn.family, family) + self.assertEqual(conn.type, type) + self.assertEqual(conn.laddr, laddr) + self.assertEqual(conn.raddr, raddr) + self.assertEqual(conn.status, status) + for kind in all_kinds: + cons = proc.connections(kind=kind) + if kind in kinds: + self.assertNotEqual(cons, []) + else: + self.assertEqual(cons, []) + # compare against system-wide connections + # XXX Solaris can't retrieve system-wide UNIX + # sockets. + if HAS_CONNECTIONS_UNIX: + self.compare_procsys_connections(proc.pid, [conn]) + + tcp_template = textwrap.dedent(""" + import socket, time + s = socket.socket({family}, socket.SOCK_STREAM) + s.bind(('{addr}', 0)) + s.listen(5) + with open('{testfn}', 'w') as f: + f.write(str(s.getsockname()[:2])) + time.sleep(60) + """) + + udp_template = textwrap.dedent(""" + import socket, time + s = socket.socket({family}, socket.SOCK_DGRAM) + s.bind(('{addr}', 0)) + with open('{testfn}', 'w') as f: + f.write(str(s.getsockname()[:2])) + time.sleep(60) + """) + + # must be relative on Windows + testfile = os.path.basename(self.get_testfn(dir=os.getcwd())) + tcp4_template = tcp_template.format( + family=int(AF_INET), addr="127.0.0.1", testfn=testfile + ) + udp4_template = udp_template.format( + family=int(AF_INET), addr="127.0.0.1", testfn=testfile + ) + tcp6_template = tcp_template.format( + family=int(AF_INET6), addr="::1", testfn=testfile + ) + udp6_template = udp_template.format( + family=int(AF_INET6), addr="::1", testfn=testfile + ) + + # launch various subprocess instantiating a socket of various + # families and types to enrich psutil results + tcp4_proc = self.pyrun(tcp4_template) + tcp4_addr = eval(wait_for_file(testfile, delete=True)) # noqa + udp4_proc = self.pyrun(udp4_template) + udp4_addr = eval(wait_for_file(testfile, delete=True)) # noqa + if supports_ipv6(): + tcp6_proc = self.pyrun(tcp6_template) + tcp6_addr = eval(wait_for_file(testfile, delete=True)) # noqa + udp6_proc = self.pyrun(udp6_template) + udp6_addr = eval(wait_for_file(testfile, delete=True)) # noqa + else: + tcp6_proc = None + udp6_proc = None + tcp6_addr = None + udp6_addr = None + + for p in psutil.Process().children(): + cons = p.connections() + self.assertEqual(len(cons), 1) + for conn in cons: + # TCP v4 + if p.pid == tcp4_proc.pid: + check_conn( + p, + conn, + AF_INET, + SOCK_STREAM, + tcp4_addr, + (), + psutil.CONN_LISTEN, + ("all", "inet", "inet4", "tcp", "tcp4"), + ) + # UDP v4 + elif p.pid == udp4_proc.pid: + check_conn( + p, + conn, + AF_INET, + SOCK_DGRAM, + udp4_addr, + (), + psutil.CONN_NONE, + ("all", "inet", "inet4", "udp", "udp4"), + ) + # TCP v6 + elif p.pid == getattr(tcp6_proc, "pid", None): + check_conn( + p, + conn, + AF_INET6, + SOCK_STREAM, + tcp6_addr, + (), + psutil.CONN_LISTEN, + ("all", "inet", "inet6", "tcp", "tcp6"), + ) + # UDP v6 + elif p.pid == getattr(udp6_proc, "pid", None): + check_conn( + p, + conn, + AF_INET6, + SOCK_DGRAM, + udp6_addr, + (), + psutil.CONN_NONE, + ("all", "inet", "inet6", "udp", "udp6"), + ) + + def test_count(self): + with create_sockets(): + # tcp + cons = this_proc_connections(kind='tcp') + self.assertEqual(len(cons), 2 if supports_ipv6() else 1) + for conn in cons: + self.assertIn(conn.family, (AF_INET, AF_INET6)) + self.assertEqual(conn.type, SOCK_STREAM) + # tcp4 + cons = this_proc_connections(kind='tcp4') + self.assertEqual(len(cons), 1) + self.assertEqual(cons[0].family, AF_INET) + self.assertEqual(cons[0].type, SOCK_STREAM) + # tcp6 + if supports_ipv6(): + cons = this_proc_connections(kind='tcp6') + self.assertEqual(len(cons), 1) + self.assertEqual(cons[0].family, AF_INET6) + self.assertEqual(cons[0].type, SOCK_STREAM) + # udp + cons = this_proc_connections(kind='udp') + self.assertEqual(len(cons), 2 if supports_ipv6() else 1) + for conn in cons: + self.assertIn(conn.family, (AF_INET, AF_INET6)) + self.assertEqual(conn.type, SOCK_DGRAM) + # udp4 + cons = this_proc_connections(kind='udp4') + self.assertEqual(len(cons), 1) + self.assertEqual(cons[0].family, AF_INET) + self.assertEqual(cons[0].type, SOCK_DGRAM) + # udp6 + if supports_ipv6(): + cons = this_proc_connections(kind='udp6') + self.assertEqual(len(cons), 1) + self.assertEqual(cons[0].family, AF_INET6) + self.assertEqual(cons[0].type, SOCK_DGRAM) + # inet + cons = this_proc_connections(kind='inet') + self.assertEqual(len(cons), 4 if supports_ipv6() else 2) + for conn in cons: + self.assertIn(conn.family, (AF_INET, AF_INET6)) + self.assertIn(conn.type, (SOCK_STREAM, SOCK_DGRAM)) + # inet6 + if supports_ipv6(): + cons = this_proc_connections(kind='inet6') + self.assertEqual(len(cons), 2) + for conn in cons: + self.assertEqual(conn.family, AF_INET6) + self.assertIn(conn.type, (SOCK_STREAM, SOCK_DGRAM)) + # Skipped on BSD becayse by default the Python process + # creates a UNIX socket to '/var/run/log'. + if HAS_CONNECTIONS_UNIX and not (FREEBSD or NETBSD): + cons = this_proc_connections(kind='unix') + self.assertEqual(len(cons), 3) + for conn in cons: + self.assertEqual(conn.family, AF_UNIX) + self.assertIn(conn.type, (SOCK_STREAM, SOCK_DGRAM)) + + +@unittest.skipIf(SKIP_SYSCONS, "requires root") +class TestSystemWideConnections(ConnectionTestCase): + """Tests for net_connections().""" + + def test_it(self): + def check(cons, families, types_): + for conn in cons: + self.assertIn(conn.family, families, msg=conn) + if conn.family != AF_UNIX: + self.assertIn(conn.type, types_, msg=conn) + check_connection_ntuple(conn) + + with create_sockets(): + from psutil._common import conn_tmap + + for kind, groups in conn_tmap.items(): + # XXX: SunOS does not retrieve UNIX sockets. + if kind == 'unix' and not HAS_CONNECTIONS_UNIX: + continue + families, types_ = groups + cons = psutil.net_connections(kind) + self.assertEqual(len(cons), len(set(cons))) + check(cons, families, types_) + + @retry_on_failure() + def test_multi_sockets_procs(self): + # Creates multiple sub processes, each creating different + # sockets. For each process check that proc.connections() + # and net_connections() return the same results. + # This is done mainly to check whether net_connections()'s + # pid is properly set, see: + # https://github.com/giampaolo/psutil/issues/1013 + with create_sockets() as socks: + expected = len(socks) + pids = [] + times = 10 + fnames = [] + for _ in range(times): + fname = self.get_testfn() + fnames.append(fname) + src = textwrap.dedent("""\ + import time, os + from psutil.tests import create_sockets + with create_sockets(): + with open(r'%s', 'w') as f: + f.write("hello") + time.sleep(60) + """ % fname) + sproc = self.pyrun(src) + pids.append(sproc.pid) + + # sync + for fname in fnames: + wait_for_file(fname) + + syscons = [ + x for x in psutil.net_connections(kind='all') if x.pid in pids + ] + for pid in pids: + self.assertEqual( + len([x for x in syscons if x.pid == pid]), expected + ) + p = psutil.Process(pid) + self.assertEqual(len(p.connections('all')), expected) + + +class TestMisc(PsutilTestCase): + def test_connection_constants(self): + ints = [] + strs = [] + for name in dir(psutil): + if name.startswith('CONN_'): + num = getattr(psutil, name) + str_ = str(num) + assert str_.isupper(), str_ + self.assertNotIn(str, strs) + self.assertNotIn(num, ints) + ints.append(num) + strs.append(str_) + if SUNOS: + psutil.CONN_IDLE # noqa + psutil.CONN_BOUND # noqa + if WINDOWS: + psutil.CONN_DELETE_TCB # noqa + + +if __name__ == '__main__': + from psutil.tests.runner import run_from_name + + run_from_name(__file__) diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_connections.pyc b/addon/globalPlugins/soundmanager/psutil/tests/test_connections.pyc deleted file mode 100644 index 5d1cb2d..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/tests/test_connections.pyc and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_contracts.py b/addon/globalPlugins/soundmanager/psutil/tests/test_contracts.py index adf7b68..5d4a15b 100644 --- a/addon/globalPlugins/soundmanager/psutil/tests/test_contracts.py +++ b/addon/globalPlugins/soundmanager/psutil/tests/test_contracts.py @@ -1,660 +1,349 @@ -#!/usr/bin/env python - -# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -"""Contracts tests. These tests mainly check API sanity in terms of -returned types and APIs availability. -Some of these are duplicates of tests test_system.py and test_process.py -""" - -import errno -import os -import stat -import time -import traceback -import warnings -from contextlib import closing - -from psutil import AIX -from psutil import BSD -from psutil import FREEBSD -from psutil import LINUX -from psutil import MACOS -from psutil import NETBSD -from psutil import OPENBSD -from psutil import OSX -from psutil import POSIX -from psutil import SUNOS -from psutil import WINDOWS -from psutil._compat import long -from psutil.tests import bind_unix_socket -from psutil.tests import check_connection_ntuple -from psutil.tests import get_kernel_version -from psutil.tests import HAS_CONNECTIONS_UNIX -from psutil.tests import HAS_NET_IO_COUNTERS -from psutil.tests import HAS_RLIMIT -from psutil.tests import HAS_SENSORS_FANS -from psutil.tests import HAS_SENSORS_TEMPERATURES -from psutil.tests import is_namedtuple -from psutil.tests import safe_rmpath -from psutil.tests import skip_on_access_denied -from psutil.tests import TESTFN -from psutil.tests import unittest -from psutil.tests import unix_socket_path -from psutil.tests import VALID_PROC_STATUSES -from psutil.tests import warn -import psutil - - -# =================================================================== -# --- APIs availability -# =================================================================== - - -class TestAvailability(unittest.TestCase): - """Make sure code reflects what doc promises in terms of APIs - availability. - """ - - def test_cpu_affinity(self): - hasit = LINUX or WINDOWS or FREEBSD - self.assertEqual(hasattr(psutil.Process, "cpu_affinity"), hasit) - - def test_win_service(self): - self.assertEqual(hasattr(psutil, "win_service_iter"), WINDOWS) - self.assertEqual(hasattr(psutil, "win_service_get"), WINDOWS) - - def test_PROCFS_PATH(self): - self.assertEqual(hasattr(psutil, "PROCFS_PATH"), - LINUX or SUNOS or AIX) - - def test_win_priority(self): - ae = self.assertEqual - ae(hasattr(psutil, "ABOVE_NORMAL_PRIORITY_CLASS"), WINDOWS) - ae(hasattr(psutil, "BELOW_NORMAL_PRIORITY_CLASS"), WINDOWS) - ae(hasattr(psutil, "HIGH_PRIORITY_CLASS"), WINDOWS) - ae(hasattr(psutil, "IDLE_PRIORITY_CLASS"), WINDOWS) - ae(hasattr(psutil, "NORMAL_PRIORITY_CLASS"), WINDOWS) - ae(hasattr(psutil, "REALTIME_PRIORITY_CLASS"), WINDOWS) - - def test_linux_ioprio(self): - ae = self.assertEqual - ae(hasattr(psutil, "IOPRIO_CLASS_NONE"), LINUX) - ae(hasattr(psutil, "IOPRIO_CLASS_RT"), LINUX) - ae(hasattr(psutil, "IOPRIO_CLASS_BE"), LINUX) - ae(hasattr(psutil, "IOPRIO_CLASS_IDLE"), LINUX) - - def test_linux_rlimit(self): - ae = self.assertEqual - hasit = LINUX and get_kernel_version() >= (2, 6, 36) - ae(hasattr(psutil.Process, "rlimit"), hasit) - ae(hasattr(psutil, "RLIM_INFINITY"), hasit) - ae(hasattr(psutil, "RLIMIT_AS"), hasit) - ae(hasattr(psutil, "RLIMIT_CORE"), hasit) - ae(hasattr(psutil, "RLIMIT_CPU"), hasit) - ae(hasattr(psutil, "RLIMIT_DATA"), hasit) - ae(hasattr(psutil, "RLIMIT_FSIZE"), hasit) - ae(hasattr(psutil, "RLIMIT_LOCKS"), hasit) - ae(hasattr(psutil, "RLIMIT_MEMLOCK"), hasit) - ae(hasattr(psutil, "RLIMIT_NOFILE"), hasit) - ae(hasattr(psutil, "RLIMIT_NPROC"), hasit) - ae(hasattr(psutil, "RLIMIT_RSS"), hasit) - ae(hasattr(psutil, "RLIMIT_STACK"), hasit) - - hasit = LINUX and get_kernel_version() >= (3, 0) - ae(hasattr(psutil, "RLIMIT_MSGQUEUE"), hasit) - ae(hasattr(psutil, "RLIMIT_NICE"), hasit) - ae(hasattr(psutil, "RLIMIT_RTPRIO"), hasit) - ae(hasattr(psutil, "RLIMIT_RTTIME"), hasit) - ae(hasattr(psutil, "RLIMIT_SIGPENDING"), hasit) - - def test_cpu_freq(self): - linux = (LINUX and - (os.path.exists("/sys/devices/system/cpu/cpufreq") or - os.path.exists("/sys/devices/system/cpu/cpu0/cpufreq"))) - self.assertEqual(hasattr(psutil, "cpu_freq"), - linux or MACOS or WINDOWS or FREEBSD) - - def test_sensors_temperatures(self): - self.assertEqual( - hasattr(psutil, "sensors_temperatures"), LINUX or FREEBSD) - - def test_sensors_fans(self): - self.assertEqual(hasattr(psutil, "sensors_fans"), LINUX) - - def test_battery(self): - self.assertEqual(hasattr(psutil, "sensors_battery"), - LINUX or WINDOWS or FREEBSD or MACOS) - - def test_proc_environ(self): - self.assertEqual(hasattr(psutil.Process, "environ"), - LINUX or MACOS or WINDOWS) - - def test_proc_uids(self): - self.assertEqual(hasattr(psutil.Process, "uids"), POSIX) - - def test_proc_gids(self): - self.assertEqual(hasattr(psutil.Process, "uids"), POSIX) - - def test_proc_terminal(self): - self.assertEqual(hasattr(psutil.Process, "terminal"), POSIX) - - def test_proc_ionice(self): - self.assertEqual(hasattr(psutil.Process, "ionice"), LINUX or WINDOWS) - - def test_proc_rlimit(self): - self.assertEqual(hasattr(psutil.Process, "rlimit"), LINUX) - - def test_proc_io_counters(self): - hasit = hasattr(psutil.Process, "io_counters") - self.assertEqual(hasit, False if MACOS or SUNOS else True) - - def test_proc_num_fds(self): - self.assertEqual(hasattr(psutil.Process, "num_fds"), POSIX) - - def test_proc_num_handles(self): - self.assertEqual(hasattr(psutil.Process, "num_handles"), WINDOWS) - - def test_proc_cpu_affinity(self): - self.assertEqual(hasattr(psutil.Process, "cpu_affinity"), - LINUX or WINDOWS or FREEBSD) - - def test_proc_cpu_num(self): - self.assertEqual(hasattr(psutil.Process, "cpu_num"), - LINUX or FREEBSD or SUNOS) - - def test_proc_memory_maps(self): - hasit = hasattr(psutil.Process, "memory_maps") - self.assertEqual( - hasit, False if OPENBSD or NETBSD or AIX or MACOS else True) - - -# =================================================================== -# --- Test deprecations -# =================================================================== - - -class TestDeprecations(unittest.TestCase): - - def test_memory_info_ex(self): - with warnings.catch_warnings(record=True) as ws: - psutil.Process().memory_info_ex() - w = ws[0] - self.assertIsInstance(w.category(), DeprecationWarning) - self.assertIn("memory_info_ex() is deprecated", str(w.message)) - self.assertIn("use memory_info() instead", str(w.message)) - - -# =================================================================== -# --- System API types -# =================================================================== - - -class TestSystem(unittest.TestCase): - """Check the return types of system related APIs. - Mainly we want to test we never return unicode on Python 2, see: - https://github.com/giampaolo/psutil/issues/1039 - """ - - @classmethod - def setUpClass(cls): - cls.proc = psutil.Process() - - def tearDown(self): - safe_rmpath(TESTFN) - - def test_cpu_times(self): - # Duplicate of test_system.py. Keep it anyway. - ret = psutil.cpu_times() - assert is_namedtuple(ret) - for n in ret: - self.assertIsInstance(n, float) - self.assertGreaterEqual(n, 0) - - def test_io_counters(self): - # Duplicate of test_system.py. Keep it anyway. - for k in psutil.disk_io_counters(perdisk=True): - self.assertIsInstance(k, str) - - def test_disk_partitions(self): - # Duplicate of test_system.py. Keep it anyway. - for disk in psutil.disk_partitions(): - self.assertIsInstance(disk.device, str) - self.assertIsInstance(disk.mountpoint, str) - self.assertIsInstance(disk.fstype, str) - self.assertIsInstance(disk.opts, str) - - @unittest.skipIf(not POSIX, 'POSIX only') - @unittest.skipIf(not HAS_CONNECTIONS_UNIX, "can't list UNIX sockets") - @skip_on_access_denied(only_if=MACOS) - def test_net_connections(self): - with unix_socket_path() as name: - with closing(bind_unix_socket(name)): - cons = psutil.net_connections(kind='unix') - assert cons - for conn in cons: - self.assertIsInstance(conn.laddr, str) - - def test_net_if_addrs(self): - # Duplicate of test_system.py. Keep it anyway. - for ifname, addrs in psutil.net_if_addrs().items(): - self.assertIsInstance(ifname, str) - for addr in addrs: - self.assertIsInstance(addr.address, str) - self.assertIsInstance(addr.netmask, (str, type(None))) - self.assertIsInstance(addr.broadcast, (str, type(None))) - - def test_net_if_stats(self): - # Duplicate of test_system.py. Keep it anyway. - for ifname, _ in psutil.net_if_stats().items(): - self.assertIsInstance(ifname, str) - - @unittest.skipIf(not HAS_NET_IO_COUNTERS, 'not supported') - def test_net_io_counters(self): - # Duplicate of test_system.py. Keep it anyway. - for ifname, _ in psutil.net_io_counters(pernic=True).items(): - self.assertIsInstance(ifname, str) - - @unittest.skipIf(not HAS_SENSORS_FANS, "not supported") - def test_sensors_fans(self): - # Duplicate of test_system.py. Keep it anyway. - for name, units in psutil.sensors_fans().items(): - self.assertIsInstance(name, str) - for unit in units: - self.assertIsInstance(unit.label, str) - - @unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported") - def test_sensors_temperatures(self): - # Duplicate of test_system.py. Keep it anyway. - for name, units in psutil.sensors_temperatures().items(): - self.assertIsInstance(name, str) - for unit in units: - self.assertIsInstance(unit.label, str) - - def test_users(self): - # Duplicate of test_system.py. Keep it anyway. - for user in psutil.users(): - self.assertIsInstance(user.name, str) - self.assertIsInstance(user.terminal, (str, type(None))) - self.assertIsInstance(user.host, (str, type(None))) - self.assertIsInstance(user.pid, (int, type(None))) - - -# =================================================================== -# --- Featch all processes test -# =================================================================== - - -class TestFetchAllProcesses(unittest.TestCase): - """Test which iterates over all running processes and performs - some sanity checks against Process API's returned values. - """ - - def get_attr_names(self): - excluded_names = set([ - 'send_signal', 'suspend', 'resume', 'terminate', 'kill', 'wait', - 'as_dict', 'parent', 'parents', 'children', 'memory_info_ex', - 'oneshot', - ]) - if LINUX and not HAS_RLIMIT: - excluded_names.add('rlimit') - attrs = [] - for name in dir(psutil.Process): - if name.startswith("_"): - continue - if name in excluded_names: - continue - attrs.append(name) - return attrs - - def iter_procs(self): - attrs = self.get_attr_names() - for p in psutil.process_iter(): - with p.oneshot(): - for name in attrs: - yield (p, name) - - def call_meth(self, p, name): - args = () - kwargs = {} - attr = getattr(p, name, None) - if attr is not None and callable(attr): - if name == 'rlimit': - args = (psutil.RLIMIT_NOFILE,) - elif name == 'memory_maps': - kwargs = {'grouped': False} - return attr(*args, **kwargs) - else: - return attr - - def test_fetch_all(self): - valid_procs = 0 - default = object() - failures = [] - for p, name in self.iter_procs(): - ret = default - try: - ret = self.call_meth(p, name) - except NotImplementedError: - msg = "%r was skipped because not implemented" % ( - self.__class__.__name__ + '.test_' + name) - warn(msg) - except (psutil.NoSuchProcess, psutil.AccessDenied) as err: - self.assertEqual(err.pid, p.pid) - if err.name: - # make sure exception's name attr is set - # with the actual process name - self.assertEqual(err.name, p.name()) - assert str(err) - assert err.msg - except Exception: - s = '\n' + '=' * 70 + '\n' - s += "FAIL: test_%s (proc=%s" % (name, p) - if ret != default: - s += ", ret=%s)" % repr(ret) - s += ')\n' - s += '-' * 70 - s += "\n%s" % traceback.format_exc() - s = "\n".join((" " * 4) + i for i in s.splitlines()) - s += '\n' - failures.append(s) - break - else: - valid_procs += 1 - if ret not in (0, 0.0, [], None, '', {}): - assert ret, ret - meth = getattr(self, name) - meth(ret, p) - - if failures: - self.fail(''.join(failures)) - - # we should always have a non-empty list, not including PID 0 etc. - # special cases. - assert valid_procs - - def cmdline(self, ret, proc): - self.assertIsInstance(ret, list) - for part in ret: - self.assertIsInstance(part, str) - - def exe(self, ret, proc): - self.assertIsInstance(ret, (str, type(None))) - if not ret: - self.assertEqual(ret, '') - else: - assert os.path.isabs(ret), ret - # Note: os.stat() may return False even if the file is there - # hence we skip the test, see: - # http://stackoverflow.com/questions/3112546/os-path-exists-lies - if POSIX and os.path.isfile(ret): - if hasattr(os, 'access') and hasattr(os, "X_OK"): - # XXX may fail on MACOS - assert os.access(ret, os.X_OK) - - def pid(self, ret, proc): - self.assertIsInstance(ret, int) - self.assertGreaterEqual(ret, 0) - - def ppid(self, ret, proc): - self.assertIsInstance(ret, (int, long)) - self.assertGreaterEqual(ret, 0) - - def name(self, ret, proc): - self.assertIsInstance(ret, str) - # on AIX, "" processes don't have names - if not AIX: - assert ret - - def create_time(self, ret, proc): - self.assertIsInstance(ret, float) - try: - self.assertGreaterEqual(ret, 0) - except AssertionError: - # XXX - if OPENBSD and proc.status() == psutil.STATUS_ZOMBIE: - pass - else: - raise - # this can't be taken for granted on all platforms - # self.assertGreaterEqual(ret, psutil.boot_time()) - # make sure returned value can be pretty printed - # with strftime - time.strftime("%Y %m %d %H:%M:%S", time.localtime(ret)) - - def uids(self, ret, proc): - assert is_namedtuple(ret) - for uid in ret: - self.assertIsInstance(uid, int) - self.assertGreaterEqual(uid, 0) - - def gids(self, ret, proc): - assert is_namedtuple(ret) - # note: testing all gids as above seems not to be reliable for - # gid == 30 (nodoby); not sure why. - for gid in ret: - self.assertIsInstance(gid, int) - if not MACOS and not NETBSD: - self.assertGreaterEqual(gid, 0) - - def username(self, ret, proc): - self.assertIsInstance(ret, str) - assert ret - - def status(self, ret, proc): - self.assertIsInstance(ret, str) - assert ret - self.assertNotEqual(ret, '?') # XXX - self.assertIn(ret, VALID_PROC_STATUSES) - - def io_counters(self, ret, proc): - assert is_namedtuple(ret) - for field in ret: - self.assertIsInstance(field, (int, long)) - if field != -1: - self.assertGreaterEqual(field, 0) - - def ionice(self, ret, proc): - if POSIX: - assert is_namedtuple(ret) - for field in ret: - self.assertIsInstance(field, int) - if LINUX: - self.assertGreaterEqual(ret.ioclass, 0) - self.assertGreaterEqual(ret.value, 0) - else: - self.assertGreaterEqual(ret, 0) - self.assertIn(ret, (0, 1, 2)) - - def num_threads(self, ret, proc): - self.assertIsInstance(ret, int) - self.assertGreaterEqual(ret, 1) - - def threads(self, ret, proc): - self.assertIsInstance(ret, list) - for t in ret: - assert is_namedtuple(t) - self.assertGreaterEqual(t.id, 0) - self.assertGreaterEqual(t.user_time, 0) - self.assertGreaterEqual(t.system_time, 0) - for field in t: - self.assertIsInstance(field, (int, float)) - - def cpu_times(self, ret, proc): - assert is_namedtuple(ret) - for n in ret: - self.assertIsInstance(n, float) - self.assertGreaterEqual(n, 0) - # TODO: check ntuple fields - - def cpu_percent(self, ret, proc): - self.assertIsInstance(ret, float) - assert 0.0 <= ret <= 100.0, ret - - def cpu_num(self, ret, proc): - self.assertIsInstance(ret, int) - if FREEBSD and ret == -1: - return - self.assertGreaterEqual(ret, 0) - if psutil.cpu_count() == 1: - self.assertEqual(ret, 0) - self.assertIn(ret, list(range(psutil.cpu_count()))) - - def memory_info(self, ret, proc): - assert is_namedtuple(ret) - for value in ret: - self.assertIsInstance(value, (int, long)) - self.assertGreaterEqual(value, 0) - if POSIX and not AIX and ret.vms != 0: - # VMS is always supposed to be the highest - for name in ret._fields: - if name != 'vms': - value = getattr(ret, name) - self.assertGreater(ret.vms, value, msg=ret) - elif WINDOWS: - self.assertGreaterEqual(ret.peak_wset, ret.wset) - self.assertGreaterEqual(ret.peak_paged_pool, ret.paged_pool) - self.assertGreaterEqual(ret.peak_nonpaged_pool, ret.nonpaged_pool) - self.assertGreaterEqual(ret.peak_pagefile, ret.pagefile) - - def memory_full_info(self, ret, proc): - assert is_namedtuple(ret) - total = psutil.virtual_memory().total - for name in ret._fields: - value = getattr(ret, name) - self.assertIsInstance(value, (int, long)) - self.assertGreaterEqual(value, 0, msg=(name, value)) - if LINUX or OSX and name in ('vms', 'data'): - # On Linux there are processes (e.g. 'goa-daemon') whose - # VMS is incredibly high for some reason. - continue - self.assertLessEqual(value, total, msg=(name, value, total)) - - if LINUX: - self.assertGreaterEqual(ret.pss, ret.uss) - - def open_files(self, ret, proc): - self.assertIsInstance(ret, list) - for f in ret: - self.assertIsInstance(f.fd, int) - self.assertIsInstance(f.path, str) - if WINDOWS: - self.assertEqual(f.fd, -1) - elif LINUX: - self.assertIsInstance(f.position, int) - self.assertIsInstance(f.mode, str) - self.assertIsInstance(f.flags, int) - self.assertGreaterEqual(f.position, 0) - self.assertIn(f.mode, ('r', 'w', 'a', 'r+', 'a+')) - self.assertGreater(f.flags, 0) - elif BSD and not f.path: - # XXX see: https://github.com/giampaolo/psutil/issues/595 - continue - assert os.path.isabs(f.path), f - assert os.path.isfile(f.path), f - - def num_fds(self, ret, proc): - self.assertIsInstance(ret, int) - self.assertGreaterEqual(ret, 0) - - def connections(self, ret, proc): - self.assertEqual(len(ret), len(set(ret))) - for conn in ret: - check_connection_ntuple(conn) - - def cwd(self, ret, proc): - if ret: # 'ret' can be None or empty - self.assertIsInstance(ret, str) - assert os.path.isabs(ret), ret - try: - st = os.stat(ret) - except OSError as err: - if WINDOWS and err.errno in \ - psutil._psplatform.ACCESS_DENIED_SET: - pass - # directory has been removed in mean time - elif err.errno != errno.ENOENT: - raise - else: - assert stat.S_ISDIR(st.st_mode) - - def memory_percent(self, ret, proc): - self.assertIsInstance(ret, float) - assert 0 <= ret <= 100, ret - - def is_running(self, ret, proc): - self.assertIsInstance(ret, bool) - - def cpu_affinity(self, ret, proc): - self.assertIsInstance(ret, list) - assert ret != [], ret - cpus = range(psutil.cpu_count()) - for n in ret: - self.assertIsInstance(n, int) - self.assertIn(n, cpus) - - def terminal(self, ret, proc): - self.assertIsInstance(ret, (str, type(None))) - if ret is not None: - assert os.path.isabs(ret), ret - assert os.path.exists(ret), ret - - def memory_maps(self, ret, proc): - for nt in ret: - self.assertIsInstance(nt.addr, str) - self.assertIsInstance(nt.perms, str) - self.assertIsInstance(nt.path, str) - for fname in nt._fields: - value = getattr(nt, fname) - if fname == 'path': - if not value.startswith('['): - assert os.path.isabs(nt.path), nt.path - # commented as on Linux we might get - # '/foo/bar (deleted)' - # assert os.path.exists(nt.path), nt.path - elif fname == 'addr': - assert value, repr(value) - elif fname == 'perms': - if not WINDOWS: - assert value, repr(value) - else: - self.assertIsInstance(value, (int, long)) - self.assertGreaterEqual(value, 0) - - def num_handles(self, ret, proc): - self.assertIsInstance(ret, int) - self.assertGreaterEqual(ret, 0) - - def nice(self, ret, proc): - self.assertIsInstance(ret, int) - if POSIX: - assert -20 <= ret <= 20, ret - else: - priorities = [getattr(psutil, x) for x in dir(psutil) - if x.endswith('_PRIORITY_CLASS')] - self.assertIn(ret, priorities) - - def num_ctx_switches(self, ret, proc): - assert is_namedtuple(ret) - for value in ret: - self.assertIsInstance(value, (int, long)) - self.assertGreaterEqual(value, 0) - - def rlimit(self, ret, proc): - self.assertIsInstance(ret, tuple) - self.assertEqual(len(ret), 2) - self.assertGreaterEqual(ret[0], -1) - self.assertGreaterEqual(ret[1], -1) - - def environ(self, ret, proc): - self.assertIsInstance(ret, dict) - for k, v in ret.items(): - self.assertIsInstance(k, str) - self.assertIsInstance(v, str) - - -if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Contracts tests. These tests mainly check API sanity in terms of +returned types and APIs availability. +Some of these are duplicates of tests test_system.py and test_process.py. +""" + +import platform +import signal +import unittest + +import psutil +from psutil import AIX +from psutil import FREEBSD +from psutil import LINUX +from psutil import MACOS +from psutil import NETBSD +from psutil import OPENBSD +from psutil import POSIX +from psutil import SUNOS +from psutil import WINDOWS +from psutil._compat import long +from psutil.tests import GITHUB_ACTIONS +from psutil.tests import HAS_CPU_FREQ +from psutil.tests import HAS_NET_IO_COUNTERS +from psutil.tests import HAS_SENSORS_FANS +from psutil.tests import HAS_SENSORS_TEMPERATURES +from psutil.tests import PYPY +from psutil.tests import SKIP_SYSCONS +from psutil.tests import PsutilTestCase +from psutil.tests import create_sockets +from psutil.tests import enum +from psutil.tests import is_namedtuple +from psutil.tests import kernel_version + + +# =================================================================== +# --- APIs availability +# =================================================================== + +# Make sure code reflects what doc promises in terms of APIs +# availability. + + +class TestAvailConstantsAPIs(PsutilTestCase): + def test_PROCFS_PATH(self): + self.assertEqual(hasattr(psutil, "PROCFS_PATH"), LINUX or SUNOS or AIX) + + def test_win_priority(self): + ae = self.assertEqual + ae(hasattr(psutil, "ABOVE_NORMAL_PRIORITY_CLASS"), WINDOWS) + ae(hasattr(psutil, "BELOW_NORMAL_PRIORITY_CLASS"), WINDOWS) + ae(hasattr(psutil, "HIGH_PRIORITY_CLASS"), WINDOWS) + ae(hasattr(psutil, "IDLE_PRIORITY_CLASS"), WINDOWS) + ae(hasattr(psutil, "NORMAL_PRIORITY_CLASS"), WINDOWS) + ae(hasattr(psutil, "REALTIME_PRIORITY_CLASS"), WINDOWS) + + def test_linux_ioprio_linux(self): + ae = self.assertEqual + ae(hasattr(psutil, "IOPRIO_CLASS_NONE"), LINUX) + ae(hasattr(psutil, "IOPRIO_CLASS_RT"), LINUX) + ae(hasattr(psutil, "IOPRIO_CLASS_BE"), LINUX) + ae(hasattr(psutil, "IOPRIO_CLASS_IDLE"), LINUX) + + def test_linux_ioprio_windows(self): + ae = self.assertEqual + ae(hasattr(psutil, "IOPRIO_HIGH"), WINDOWS) + ae(hasattr(psutil, "IOPRIO_NORMAL"), WINDOWS) + ae(hasattr(psutil, "IOPRIO_LOW"), WINDOWS) + ae(hasattr(psutil, "IOPRIO_VERYLOW"), WINDOWS) + + @unittest.skipIf( + GITHUB_ACTIONS and LINUX, "unsupported on GITHUB_ACTIONS + LINUX" + ) + def test_rlimit(self): + ae = self.assertEqual + ae(hasattr(psutil, "RLIM_INFINITY"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_AS"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_CORE"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_CPU"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_DATA"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_FSIZE"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_MEMLOCK"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_NOFILE"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_NPROC"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_RSS"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_STACK"), LINUX or FREEBSD) + + ae(hasattr(psutil, "RLIMIT_LOCKS"), LINUX) + if POSIX: + if kernel_version() >= (2, 6, 8): + ae(hasattr(psutil, "RLIMIT_MSGQUEUE"), LINUX) + if kernel_version() >= (2, 6, 12): + ae(hasattr(psutil, "RLIMIT_NICE"), LINUX) + if kernel_version() >= (2, 6, 12): + ae(hasattr(psutil, "RLIMIT_RTPRIO"), LINUX) + if kernel_version() >= (2, 6, 25): + ae(hasattr(psutil, "RLIMIT_RTTIME"), LINUX) + if kernel_version() >= (2, 6, 8): + ae(hasattr(psutil, "RLIMIT_SIGPENDING"), LINUX) + + ae(hasattr(psutil, "RLIMIT_SWAP"), FREEBSD) + ae(hasattr(psutil, "RLIMIT_SBSIZE"), FREEBSD) + ae(hasattr(psutil, "RLIMIT_NPTS"), FREEBSD) + + +class TestAvailSystemAPIs(PsutilTestCase): + def test_win_service_iter(self): + self.assertEqual(hasattr(psutil, "win_service_iter"), WINDOWS) + + def test_win_service_get(self): + self.assertEqual(hasattr(psutil, "win_service_get"), WINDOWS) + + def test_cpu_freq(self): + self.assertEqual( + hasattr(psutil, "cpu_freq"), + LINUX or MACOS or WINDOWS or FREEBSD or OPENBSD, + ) + + def test_sensors_temperatures(self): + self.assertEqual( + hasattr(psutil, "sensors_temperatures"), LINUX or FREEBSD + ) + + def test_sensors_fans(self): + self.assertEqual(hasattr(psutil, "sensors_fans"), LINUX) + + def test_battery(self): + self.assertEqual( + hasattr(psutil, "sensors_battery"), + LINUX or WINDOWS or FREEBSD or MACOS, + ) + + +class TestAvailProcessAPIs(PsutilTestCase): + def test_environ(self): + self.assertEqual( + hasattr(psutil.Process, "environ"), + LINUX + or MACOS + or WINDOWS + or AIX + or SUNOS + or FREEBSD + or OPENBSD + or NETBSD, + ) + + def test_uids(self): + self.assertEqual(hasattr(psutil.Process, "uids"), POSIX) + + def test_gids(self): + self.assertEqual(hasattr(psutil.Process, "uids"), POSIX) + + def test_terminal(self): + self.assertEqual(hasattr(psutil.Process, "terminal"), POSIX) + + def test_ionice(self): + self.assertEqual(hasattr(psutil.Process, "ionice"), LINUX or WINDOWS) + + @unittest.skipIf( + GITHUB_ACTIONS and LINUX, "unsupported on GITHUB_ACTIONS + LINUX" + ) + def test_rlimit(self): + self.assertEqual(hasattr(psutil.Process, "rlimit"), LINUX or FREEBSD) + + def test_io_counters(self): + hasit = hasattr(psutil.Process, "io_counters") + self.assertEqual(hasit, not (MACOS or SUNOS)) + + def test_num_fds(self): + self.assertEqual(hasattr(psutil.Process, "num_fds"), POSIX) + + def test_num_handles(self): + self.assertEqual(hasattr(psutil.Process, "num_handles"), WINDOWS) + + def test_cpu_affinity(self): + self.assertEqual( + hasattr(psutil.Process, "cpu_affinity"), + LINUX or WINDOWS or FREEBSD, + ) + + def test_cpu_num(self): + self.assertEqual( + hasattr(psutil.Process, "cpu_num"), LINUX or FREEBSD or SUNOS + ) + + def test_memory_maps(self): + hasit = hasattr(psutil.Process, "memory_maps") + self.assertEqual(hasit, not (OPENBSD or NETBSD or AIX or MACOS)) + + +# =================================================================== +# --- API types +# =================================================================== + + +class TestSystemAPITypes(PsutilTestCase): + """Check the return types of system related APIs. + Mainly we want to test we never return unicode on Python 2, see: + https://github.com/giampaolo/psutil/issues/1039. + """ + + @classmethod + def setUpClass(cls): + cls.proc = psutil.Process() + + def assert_ntuple_of_nums(self, nt, type_=float, gezero=True): + assert is_namedtuple(nt) + for n in nt: + self.assertIsInstance(n, type_) + if gezero: + self.assertGreaterEqual(n, 0) + + def test_cpu_times(self): + self.assert_ntuple_of_nums(psutil.cpu_times()) + for nt in psutil.cpu_times(percpu=True): + self.assert_ntuple_of_nums(nt) + + def test_cpu_percent(self): + self.assertIsInstance(psutil.cpu_percent(interval=None), float) + self.assertIsInstance(psutil.cpu_percent(interval=0.00001), float) + + def test_cpu_times_percent(self): + self.assert_ntuple_of_nums(psutil.cpu_times_percent(interval=None)) + self.assert_ntuple_of_nums(psutil.cpu_times_percent(interval=0.0001)) + + def test_cpu_count(self): + self.assertIsInstance(psutil.cpu_count(), int) + + # TODO: remove this once 1892 is fixed + @unittest.skipIf( + MACOS and platform.machine() == 'arm64', "skipped due to #1892" + ) + @unittest.skipIf(not HAS_CPU_FREQ, "not supported") + def test_cpu_freq(self): + if psutil.cpu_freq() is None: + raise self.skipTest("cpu_freq() returns None") + self.assert_ntuple_of_nums(psutil.cpu_freq(), type_=(float, int, long)) + + def test_disk_io_counters(self): + # Duplicate of test_system.py. Keep it anyway. + for k, v in psutil.disk_io_counters(perdisk=True).items(): + self.assertIsInstance(k, str) + self.assert_ntuple_of_nums(v, type_=(int, long)) + + def test_disk_partitions(self): + # Duplicate of test_system.py. Keep it anyway. + for disk in psutil.disk_partitions(): + self.assertIsInstance(disk.device, str) + self.assertIsInstance(disk.mountpoint, str) + self.assertIsInstance(disk.fstype, str) + self.assertIsInstance(disk.opts, str) + self.assertIsInstance(disk.maxfile, (int, type(None))) + self.assertIsInstance(disk.maxpath, (int, type(None))) + + @unittest.skipIf(SKIP_SYSCONS, "requires root") + def test_net_connections(self): + with create_sockets(): + ret = psutil.net_connections('all') + self.assertEqual(len(ret), len(set(ret))) + for conn in ret: + assert is_namedtuple(conn) + + def test_net_if_addrs(self): + # Duplicate of test_system.py. Keep it anyway. + for ifname, addrs in psutil.net_if_addrs().items(): + self.assertIsInstance(ifname, str) + for addr in addrs: + if enum is not None and not PYPY: + self.assertIsInstance(addr.family, enum.IntEnum) + else: + self.assertIsInstance(addr.family, int) + self.assertIsInstance(addr.address, str) + self.assertIsInstance(addr.netmask, (str, type(None))) + self.assertIsInstance(addr.broadcast, (str, type(None))) + + def test_net_if_stats(self): + # Duplicate of test_system.py. Keep it anyway. + for ifname, info in psutil.net_if_stats().items(): + self.assertIsInstance(ifname, str) + self.assertIsInstance(info.isup, bool) + if enum is not None: + self.assertIsInstance(info.duplex, enum.IntEnum) + else: + self.assertIsInstance(info.duplex, int) + self.assertIsInstance(info.speed, int) + self.assertIsInstance(info.mtu, int) + + @unittest.skipIf(not HAS_NET_IO_COUNTERS, 'not supported') + def test_net_io_counters(self): + # Duplicate of test_system.py. Keep it anyway. + for ifname in psutil.net_io_counters(pernic=True): + self.assertIsInstance(ifname, str) + + @unittest.skipIf(not HAS_SENSORS_FANS, "not supported") + def test_sensors_fans(self): + # Duplicate of test_system.py. Keep it anyway. + for name, units in psutil.sensors_fans().items(): + self.assertIsInstance(name, str) + for unit in units: + self.assertIsInstance(unit.label, str) + self.assertIsInstance(unit.current, (float, int, type(None))) + + @unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported") + def test_sensors_temperatures(self): + # Duplicate of test_system.py. Keep it anyway. + for name, units in psutil.sensors_temperatures().items(): + self.assertIsInstance(name, str) + for unit in units: + self.assertIsInstance(unit.label, str) + self.assertIsInstance(unit.current, (float, int, type(None))) + self.assertIsInstance(unit.high, (float, int, type(None))) + self.assertIsInstance(unit.critical, (float, int, type(None))) + + def test_boot_time(self): + # Duplicate of test_system.py. Keep it anyway. + self.assertIsInstance(psutil.boot_time(), float) + + def test_users(self): + # Duplicate of test_system.py. Keep it anyway. + for user in psutil.users(): + self.assertIsInstance(user.name, str) + self.assertIsInstance(user.terminal, (str, type(None))) + self.assertIsInstance(user.host, (str, type(None))) + self.assertIsInstance(user.pid, (int, type(None))) + + +class TestProcessWaitType(PsutilTestCase): + @unittest.skipIf(not POSIX, "not POSIX") + def test_negative_signal(self): + p = psutil.Process(self.spawn_testproc().pid) + p.terminate() + code = p.wait() + self.assertEqual(code, -signal.SIGTERM) + if enum is not None: + self.assertIsInstance(code, enum.IntEnum) + else: + self.assertIsInstance(code, int) + + +if __name__ == '__main__': + from psutil.tests.runner import run_from_name + + run_from_name(__file__) diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_contracts.pyc b/addon/globalPlugins/soundmanager/psutil/tests/test_contracts.pyc deleted file mode 100644 index fbac63a..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/tests/test_contracts.pyc and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_linux.py b/addon/globalPlugins/soundmanager/psutil/tests/test_linux.py index 4cdc9cb..8fdd3cc 100644 --- a/addon/globalPlugins/soundmanager/psutil/tests/test_linux.py +++ b/addon/globalPlugins/soundmanager/psutil/tests/test_linux.py @@ -1,2130 +1,2350 @@ -#!/usr/bin/env python - -# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -"""Linux specific tests.""" - -from __future__ import division -import collections -import contextlib -import errno -import io -import os -import re -import shutil -import socket -import struct -import tempfile -import textwrap -import time -import warnings - -import psutil -from psutil import LINUX -from psutil._compat import basestring -from psutil._compat import PY3 -from psutil._compat import u -from psutil.tests import call_until -from psutil.tests import HAS_BATTERY -from psutil.tests import HAS_CPU_FREQ -from psutil.tests import HAS_GETLOADAVG -from psutil.tests import HAS_RLIMIT -from psutil.tests import MEMORY_TOLERANCE -from psutil.tests import mock -from psutil.tests import PYPY -from psutil.tests import pyrun -from psutil.tests import reap_children -from psutil.tests import reload_module -from psutil.tests import retry_on_failure -from psutil.tests import safe_rmpath -from psutil.tests import sh -from psutil.tests import skip_on_not_implemented -from psutil.tests import TESTFN -from psutil.tests import ThreadTask -from psutil.tests import TRAVIS -from psutil.tests import unittest -from psutil.tests import which - - -HERE = os.path.abspath(os.path.dirname(__file__)) -SIOCGIFADDR = 0x8915 -SIOCGIFCONF = 0x8912 -SIOCGIFHWADDR = 0x8927 -if LINUX: - SECTOR_SIZE = 512 - - -# ===================================================================== -# --- utils -# ===================================================================== - - -def get_ipv4_address(ifname): - import fcntl - ifname = ifname[:15] - if PY3: - ifname = bytes(ifname, 'ascii') - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - with contextlib.closing(s): - return socket.inet_ntoa( - fcntl.ioctl(s.fileno(), - SIOCGIFADDR, - struct.pack('256s', ifname))[20:24]) - - -def get_mac_address(ifname): - import fcntl - ifname = ifname[:15] - if PY3: - ifname = bytes(ifname, 'ascii') - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - with contextlib.closing(s): - info = fcntl.ioctl( - s.fileno(), SIOCGIFHWADDR, struct.pack('256s', ifname)) - if PY3: - def ord(x): - return x - else: - import __builtin__ - ord = __builtin__.ord - return ''.join(['%02x:' % ord(char) for char in info[18:24]])[:-1] - - -def free_swap(): - """Parse 'free' cmd and return swap memory's s total, used and free - values. - """ - out = sh('free -b', env={"LANG": "C.UTF-8"}) - lines = out.split('\n') - for line in lines: - if line.startswith('Swap'): - _, total, used, free = line.split() - nt = collections.namedtuple('free', 'total used free') - return nt(int(total), int(used), int(free)) - raise ValueError( - "can't find 'Swap' in 'free' output:\n%s" % '\n'.join(lines)) - - -def free_physmem(): - """Parse 'free' cmd and return physical memory's total, used - and free values. - """ - # Note: free can have 2 different formats, invalidating 'shared' - # and 'cached' memory which may have different positions so we - # do not return them. - # https://github.com/giampaolo/psutil/issues/538#issuecomment-57059946 - out = sh('free -b', env={"LANG": "C.UTF-8"}) - lines = out.split('\n') - for line in lines: - if line.startswith('Mem'): - total, used, free, shared = \ - [int(x) for x in line.split()[1:5]] - nt = collections.namedtuple( - 'free', 'total used free shared output') - return nt(total, used, free, shared, out) - raise ValueError( - "can't find 'Mem' in 'free' output:\n%s" % '\n'.join(lines)) - - -def vmstat(stat): - out = sh("vmstat -s", env={"LANG": "C.UTF-8"}) - for line in out.split("\n"): - line = line.strip() - if stat in line: - return int(line.split(' ')[0]) - raise ValueError("can't find %r in 'vmstat' output" % stat) - - -def get_free_version_info(): - out = sh("free -V").strip() - return tuple(map(int, out.split()[-1].split('.'))) - - -@contextlib.contextmanager -def mock_open_content(for_path, content): - """Mock open() builtin and forces it to return a certain `content` - on read() if the path being opened matches `for_path`. - """ - def open_mock(name, *args, **kwargs): - if name == for_path: - if PY3: - if isinstance(content, basestring): - return io.StringIO(content) - else: - return io.BytesIO(content) - else: - return io.BytesIO(content) - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, create=True, side_effect=open_mock) as m: - yield m - - -@contextlib.contextmanager -def mock_open_exception(for_path, exc): - """Mock open() builtin and raises `exc` if the path being opened - matches `for_path`. - """ - def open_mock(name, *args, **kwargs): - if name == for_path: - raise exc - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, create=True, side_effect=open_mock) as m: - yield m - - -# ===================================================================== -# --- system virtual memory -# ===================================================================== - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestSystemVirtualMemory(unittest.TestCase): - - def test_total(self): - # free_value = free_physmem().total - # psutil_value = psutil.virtual_memory().total - # self.assertEqual(free_value, psutil_value) - vmstat_value = vmstat('total memory') * 1024 - psutil_value = psutil.virtual_memory().total - self.assertAlmostEqual(vmstat_value, psutil_value) - - # Older versions of procps used slab memory to calculate used memory. - # This got changed in: - # https://gitlab.com/procps-ng/procps/commit/ - # 05d751c4f076a2f0118b914c5e51cfbb4762ad8e - @unittest.skipIf(LINUX and get_free_version_info() < (3, 3, 12), - "old free version") - @retry_on_failure() - def test_used(self): - free = free_physmem() - free_value = free.used - psutil_value = psutil.virtual_memory().used - self.assertAlmostEqual( - free_value, psutil_value, delta=MEMORY_TOLERANCE, - msg='%s %s \n%s' % (free_value, psutil_value, free.output)) - - @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") - @retry_on_failure() - def test_free(self): - vmstat_value = vmstat('free memory') * 1024 - psutil_value = psutil.virtual_memory().free - self.assertAlmostEqual( - vmstat_value, psutil_value, delta=MEMORY_TOLERANCE) - - @retry_on_failure() - def test_buffers(self): - vmstat_value = vmstat('buffer memory') * 1024 - psutil_value = psutil.virtual_memory().buffers - self.assertAlmostEqual( - vmstat_value, psutil_value, delta=MEMORY_TOLERANCE) - - # https://travis-ci.org/giampaolo/psutil/jobs/226719664 - @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") - @retry_on_failure() - def test_active(self): - vmstat_value = vmstat('active memory') * 1024 - psutil_value = psutil.virtual_memory().active - self.assertAlmostEqual( - vmstat_value, psutil_value, delta=MEMORY_TOLERANCE) - - # https://travis-ci.org/giampaolo/psutil/jobs/227242952 - @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") - @retry_on_failure() - def test_inactive(self): - vmstat_value = vmstat('inactive memory') * 1024 - psutil_value = psutil.virtual_memory().inactive - self.assertAlmostEqual( - vmstat_value, psutil_value, delta=MEMORY_TOLERANCE) - - @retry_on_failure() - def test_shared(self): - free = free_physmem() - free_value = free.shared - if free_value == 0: - raise unittest.SkipTest("free does not support 'shared' column") - psutil_value = psutil.virtual_memory().shared - self.assertAlmostEqual( - free_value, psutil_value, delta=MEMORY_TOLERANCE, - msg='%s %s \n%s' % (free_value, psutil_value, free.output)) - - @retry_on_failure() - def test_available(self): - # "free" output format has changed at some point: - # https://github.com/giampaolo/psutil/issues/538#issuecomment-147192098 - out = sh("free -b") - lines = out.split('\n') - if 'available' not in lines[0]: - raise unittest.SkipTest("free does not support 'available' column") - else: - free_value = int(lines[1].split()[-1]) - psutil_value = psutil.virtual_memory().available - self.assertAlmostEqual( - free_value, psutil_value, delta=MEMORY_TOLERANCE, - msg='%s %s \n%s' % (free_value, psutil_value, out)) - - def test_warnings_on_misses(self): - # Emulate a case where /proc/meminfo provides few info. - # psutil is supposed to set the missing fields to 0 and - # raise a warning. - with mock_open_content( - '/proc/meminfo', - textwrap.dedent("""\ - Active(anon): 6145416 kB - Active(file): 2950064 kB - Inactive(anon): 574764 kB - Inactive(file): 1567648 kB - MemAvailable: -1 kB - MemFree: 2057400 kB - MemTotal: 16325648 kB - SReclaimable: 346648 kB - """).encode()) as m: - with warnings.catch_warnings(record=True) as ws: - warnings.simplefilter("always") - ret = psutil.virtual_memory() - assert m.called - self.assertEqual(len(ws), 1) - w = ws[0] - assert w.filename.endswith('psutil/_pslinux.py') - self.assertIn( - "memory stats couldn't be determined", str(w.message)) - self.assertIn("cached", str(w.message)) - self.assertIn("shared", str(w.message)) - self.assertIn("active", str(w.message)) - self.assertIn("inactive", str(w.message)) - self.assertIn("buffers", str(w.message)) - self.assertIn("available", str(w.message)) - self.assertEqual(ret.cached, 0) - self.assertEqual(ret.active, 0) - self.assertEqual(ret.inactive, 0) - self.assertEqual(ret.shared, 0) - self.assertEqual(ret.buffers, 0) - self.assertEqual(ret.available, 0) - self.assertEqual(ret.slab, 0) - - def test_avail_old_percent(self): - # Make sure that our calculation of avail mem for old kernels - # is off by max 10%. - from psutil._pslinux import calculate_avail_vmem - from psutil._pslinux import open_binary - - mems = {} - with open_binary('/proc/meminfo') as f: - for line in f: - fields = line.split() - mems[fields[0]] = int(fields[1]) * 1024 - - a = calculate_avail_vmem(mems) - if b'MemAvailable:' in mems: - b = mems[b'MemAvailable:'] - diff_percent = abs(a - b) / a * 100 - self.assertLess(diff_percent, 10) - - def test_avail_old_comes_from_kernel(self): - # Make sure "MemAvailable:" coluimn is used instead of relying - # on our internal algorithm to calculate avail mem. - with mock_open_content( - '/proc/meminfo', - textwrap.dedent("""\ - Active: 9444728 kB - Active(anon): 6145416 kB - Active(file): 2950064 kB - Buffers: 287952 kB - Cached: 4818144 kB - Inactive(file): 1578132 kB - Inactive(anon): 574764 kB - Inactive(file): 1567648 kB - MemAvailable: 6574984 kB - MemFree: 2057400 kB - MemTotal: 16325648 kB - Shmem: 577588 kB - SReclaimable: 346648 kB - """).encode()) as m: - with warnings.catch_warnings(record=True) as ws: - ret = psutil.virtual_memory() - assert m.called - self.assertEqual(ret.available, 6574984 * 1024) - w = ws[0] - self.assertIn( - "inactive memory stats couldn't be determined", str(w.message)) - - def test_avail_old_missing_fields(self): - # Remove Active(file), Inactive(file) and SReclaimable - # from /proc/meminfo and make sure the fallback is used - # (free + cached), - with mock_open_content( - "/proc/meminfo", - textwrap.dedent("""\ - Active: 9444728 kB - Active(anon): 6145416 kB - Buffers: 287952 kB - Cached: 4818144 kB - Inactive(file): 1578132 kB - Inactive(anon): 574764 kB - MemFree: 2057400 kB - MemTotal: 16325648 kB - Shmem: 577588 kB - """).encode()) as m: - with warnings.catch_warnings(record=True) as ws: - ret = psutil.virtual_memory() - assert m.called - self.assertEqual(ret.available, 2057400 * 1024 + 4818144 * 1024) - w = ws[0] - self.assertIn( - "inactive memory stats couldn't be determined", str(w.message)) - - def test_avail_old_missing_zoneinfo(self): - # Remove /proc/zoneinfo file. Make sure fallback is used - # (free + cached). - with mock_open_content( - "/proc/meminfo", - textwrap.dedent("""\ - Active: 9444728 kB - Active(anon): 6145416 kB - Active(file): 2950064 kB - Buffers: 287952 kB - Cached: 4818144 kB - Inactive(file): 1578132 kB - Inactive(anon): 574764 kB - Inactive(file): 1567648 kB - MemFree: 2057400 kB - MemTotal: 16325648 kB - Shmem: 577588 kB - SReclaimable: 346648 kB - """).encode()): - with mock_open_exception( - "/proc/zoneinfo", - IOError(errno.ENOENT, 'no such file or directory')): - with warnings.catch_warnings(record=True) as ws: - ret = psutil.virtual_memory() - self.assertEqual( - ret.available, 2057400 * 1024 + 4818144 * 1024) - w = ws[0] - self.assertIn( - "inactive memory stats couldn't be determined", - str(w.message)) - - def test_virtual_memory_mocked(self): - # Emulate /proc/meminfo because neither vmstat nor free return slab. - def open_mock(name, *args, **kwargs): - if name == '/proc/meminfo': - return io.BytesIO(textwrap.dedent("""\ - MemTotal: 100 kB - MemFree: 2 kB - MemAvailable: 3 kB - Buffers: 4 kB - Cached: 5 kB - SwapCached: 6 kB - Active: 7 kB - Inactive: 8 kB - Active(anon): 9 kB - Inactive(anon): 10 kB - Active(file): 11 kB - Inactive(file): 12 kB - Unevictable: 13 kB - Mlocked: 14 kB - SwapTotal: 15 kB - SwapFree: 16 kB - Dirty: 17 kB - Writeback: 18 kB - AnonPages: 19 kB - Mapped: 20 kB - Shmem: 21 kB - Slab: 22 kB - SReclaimable: 23 kB - SUnreclaim: 24 kB - KernelStack: 25 kB - PageTables: 26 kB - NFS_Unstable: 27 kB - Bounce: 28 kB - WritebackTmp: 29 kB - CommitLimit: 30 kB - Committed_AS: 31 kB - VmallocTotal: 32 kB - VmallocUsed: 33 kB - VmallocChunk: 34 kB - HardwareCorrupted: 35 kB - AnonHugePages: 36 kB - ShmemHugePages: 37 kB - ShmemPmdMapped: 38 kB - CmaTotal: 39 kB - CmaFree: 40 kB - HugePages_Total: 41 kB - HugePages_Free: 42 kB - HugePages_Rsvd: 43 kB - HugePages_Surp: 44 kB - Hugepagesize: 45 kB - DirectMap46k: 46 kB - DirectMap47M: 47 kB - DirectMap48G: 48 kB - """).encode()) - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, create=True, side_effect=open_mock) as m: - mem = psutil.virtual_memory() - assert m.called - self.assertEqual(mem.total, 100 * 1024) - self.assertEqual(mem.free, 2 * 1024) - self.assertEqual(mem.buffers, 4 * 1024) - # cached mem also includes reclaimable memory - self.assertEqual(mem.cached, (5 + 23) * 1024) - self.assertEqual(mem.shared, 21 * 1024) - self.assertEqual(mem.active, 7 * 1024) - self.assertEqual(mem.inactive, 8 * 1024) - self.assertEqual(mem.slab, 22 * 1024) - self.assertEqual(mem.available, 3 * 1024) - - -# ===================================================================== -# --- system swap memory -# ===================================================================== - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestSystemSwapMemory(unittest.TestCase): - - @staticmethod - def meminfo_has_swap_info(): - """Return True if /proc/meminfo provides swap metrics.""" - with open("/proc/meminfo") as f: - data = f.read() - return 'SwapTotal:' in data and 'SwapFree:' in data - - def test_total(self): - free_value = free_swap().total - psutil_value = psutil.swap_memory().total - return self.assertAlmostEqual( - free_value, psutil_value, delta=MEMORY_TOLERANCE) - - @retry_on_failure() - def test_used(self): - free_value = free_swap().used - psutil_value = psutil.swap_memory().used - return self.assertAlmostEqual( - free_value, psutil_value, delta=MEMORY_TOLERANCE) - - @retry_on_failure() - def test_free(self): - free_value = free_swap().free - psutil_value = psutil.swap_memory().free - return self.assertAlmostEqual( - free_value, psutil_value, delta=MEMORY_TOLERANCE) - - def test_missing_sin_sout(self): - with mock.patch('psutil._common.open', create=True) as m: - with warnings.catch_warnings(record=True) as ws: - warnings.simplefilter("always") - ret = psutil.swap_memory() - assert m.called - self.assertEqual(len(ws), 1) - w = ws[0] - assert w.filename.endswith('psutil/_pslinux.py') - self.assertIn( - "'sin' and 'sout' swap memory stats couldn't " - "be determined", str(w.message)) - self.assertEqual(ret.sin, 0) - self.assertEqual(ret.sout, 0) - - def test_no_vmstat_mocked(self): - # see https://github.com/giampaolo/psutil/issues/722 - with mock_open_exception( - "/proc/vmstat", - IOError(errno.ENOENT, 'no such file or directory')) as m: - with warnings.catch_warnings(record=True) as ws: - warnings.simplefilter("always") - ret = psutil.swap_memory() - assert m.called - self.assertEqual(len(ws), 1) - w = ws[0] - assert w.filename.endswith('psutil/_pslinux.py') - self.assertIn( - "'sin' and 'sout' swap memory stats couldn't " - "be determined and were set to 0", - str(w.message)) - self.assertEqual(ret.sin, 0) - self.assertEqual(ret.sout, 0) - - def test_meminfo_against_sysinfo(self): - # Make sure the content of /proc/meminfo about swap memory - # matches sysinfo() syscall, see: - # https://github.com/giampaolo/psutil/issues/1015 - if not self.meminfo_has_swap_info(): - return unittest.skip("/proc/meminfo has no swap metrics") - with mock.patch('psutil._pslinux.cext.linux_sysinfo') as m: - swap = psutil.swap_memory() - assert not m.called - import psutil._psutil_linux as cext - _, _, _, _, total, free, unit_multiplier = cext.linux_sysinfo() - total *= unit_multiplier - free *= unit_multiplier - self.assertEqual(swap.total, total) - self.assertAlmostEqual(swap.free, free, delta=MEMORY_TOLERANCE) - - def test_emulate_meminfo_has_no_metrics(self): - # Emulate a case where /proc/meminfo provides no swap metrics - # in which case sysinfo() syscall is supposed to be used - # as a fallback. - with mock_open_content("/proc/meminfo", b"") as m: - psutil.swap_memory() - assert m.called - - -# ===================================================================== -# --- system CPU -# ===================================================================== - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestSystemCPUTimes(unittest.TestCase): - - @unittest.skipIf(TRAVIS, "unknown failure on travis") - def test_fields(self): - fields = psutil.cpu_times()._fields - kernel_ver = re.findall(r'\d+\.\d+\.\d+', os.uname()[2])[0] - kernel_ver_info = tuple(map(int, kernel_ver.split('.'))) - if kernel_ver_info >= (2, 6, 11): - self.assertIn('steal', fields) - else: - self.assertNotIn('steal', fields) - if kernel_ver_info >= (2, 6, 24): - self.assertIn('guest', fields) - else: - self.assertNotIn('guest', fields) - if kernel_ver_info >= (3, 2, 0): - self.assertIn('guest_nice', fields) - else: - self.assertNotIn('guest_nice', fields) - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestSystemCPUCountLogical(unittest.TestCase): - - @unittest.skipIf(not os.path.exists("/sys/devices/system/cpu/online"), - "/sys/devices/system/cpu/online does not exist") - def test_against_sysdev_cpu_online(self): - with open("/sys/devices/system/cpu/online") as f: - value = f.read().strip() - if "-" in str(value): - value = int(value.split('-')[1]) + 1 - self.assertEqual(psutil.cpu_count(), value) - - @unittest.skipIf(not os.path.exists("/sys/devices/system/cpu"), - "/sys/devices/system/cpu does not exist") - def test_against_sysdev_cpu_num(self): - ls = os.listdir("/sys/devices/system/cpu") - count = len([x for x in ls if re.search(r"cpu\d+$", x) is not None]) - self.assertEqual(psutil.cpu_count(), count) - - @unittest.skipIf(not which("nproc"), "nproc utility not available") - def test_against_nproc(self): - num = int(sh("nproc --all")) - self.assertEqual(psutil.cpu_count(logical=True), num) - - @unittest.skipIf(not which("lscpu"), "lscpu utility not available") - def test_against_lscpu(self): - out = sh("lscpu -p") - num = len([x for x in out.split('\n') if not x.startswith('#')]) - self.assertEqual(psutil.cpu_count(logical=True), num) - - def test_emulate_fallbacks(self): - import psutil._pslinux - original = psutil._pslinux.cpu_count_logical() - # Here we want to mock os.sysconf("SC_NPROCESSORS_ONLN") in - # order to cause the parsing of /proc/cpuinfo and /proc/stat. - with mock.patch( - 'psutil._pslinux.os.sysconf', side_effect=ValueError) as m: - self.assertEqual(psutil._pslinux.cpu_count_logical(), original) - assert m.called - - # Let's have open() return emtpy data and make sure None is - # returned ('cause we mimick os.cpu_count()). - with mock.patch('psutil._common.open', create=True) as m: - self.assertIsNone(psutil._pslinux.cpu_count_logical()) - self.assertEqual(m.call_count, 2) - # /proc/stat should be the last one - self.assertEqual(m.call_args[0][0], '/proc/stat') - - # Let's push this a bit further and make sure /proc/cpuinfo - # parsing works as expected. - with open('/proc/cpuinfo', 'rb') as f: - cpuinfo_data = f.read() - fake_file = io.BytesIO(cpuinfo_data) - with mock.patch('psutil._common.open', - return_value=fake_file, create=True) as m: - self.assertEqual(psutil._pslinux.cpu_count_logical(), original) - - # Finally, let's make /proc/cpuinfo return meaningless data; - # this way we'll fall back on relying on /proc/stat - with mock_open_content('/proc/cpuinfo', b"") as m: - self.assertEqual(psutil._pslinux.cpu_count_logical(), original) - m.called - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestSystemCPUCountPhysical(unittest.TestCase): - - @unittest.skipIf(not which("lscpu"), "lscpu utility not available") - def test_against_lscpu(self): - out = sh("lscpu -p") - core_ids = set() - for line in out.split('\n'): - if not line.startswith('#'): - fields = line.split(',') - core_ids.add(fields[1]) - self.assertEqual(psutil.cpu_count(logical=False), len(core_ids)) - - def test_emulate_none(self): - with mock.patch('glob.glob', return_value=[]) as m1: - with mock.patch('psutil._common.open', create=True) as m2: - self.assertIsNone(psutil._pslinux.cpu_count_physical()) - assert m1.called - assert m2.called - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestSystemCPUFrequency(unittest.TestCase): - - @unittest.skipIf(not HAS_CPU_FREQ, "not supported") - def test_emulate_no_files(self): - with mock.patch("os.path.exists", return_value=False): - self.assertIsNone(psutil.cpu_freq()) - - @unittest.skipIf(TRAVIS, "fails on Travis") - @unittest.skipIf(not HAS_CPU_FREQ, "not supported") - def test_emulate_use_second_file(self): - # https://github.com/giampaolo/psutil/issues/981 - def path_exists_mock(path): - if path.startswith("/sys/devices/system/cpu/cpufreq/policy"): - return False - else: - flags.append(None) - return orig_exists(path) - - flags = [] - orig_exists = os.path.exists - with mock.patch("os.path.exists", side_effect=path_exists_mock, - create=True): - assert psutil.cpu_freq() - self.assertEqual(len(flags), psutil.cpu_count(logical=True)) - - @unittest.skipIf(not HAS_CPU_FREQ, "not supported") - def test_emulate_use_cpuinfo(self): - # Emulate a case where /sys/devices/system/cpu/cpufreq* does not - # exist and /proc/cpuinfo is used instead. - def path_exists_mock(path): - if path.startswith('/sys/devices/system/cpu/'): - return False - else: - if path == "/proc/cpuinfo": - flags.append(None) - return os_path_exists(path) - - flags = [] - os_path_exists = os.path.exists - try: - with mock.patch("os.path.exists", side_effect=path_exists_mock): - reload_module(psutil._pslinux) - ret = psutil.cpu_freq() - assert ret - assert flags - self.assertEqual(ret.max, 0.0) - self.assertEqual(ret.min, 0.0) - for freq in psutil.cpu_freq(percpu=True): - self.assertEqual(ret.max, 0.0) - self.assertEqual(ret.min, 0.0) - finally: - reload_module(psutil._pslinux) - reload_module(psutil) - - @unittest.skipIf(not HAS_CPU_FREQ, "not supported") - def test_emulate_data(self): - def open_mock(name, *args, **kwargs): - if (name.endswith('/scaling_cur_freq') and - name.startswith("/sys/devices/system/cpu/cpufreq/policy")): - return io.BytesIO(b"500000") - elif (name.endswith('/scaling_min_freq') and - name.startswith("/sys/devices/system/cpu/cpufreq/policy")): - return io.BytesIO(b"600000") - elif (name.endswith('/scaling_max_freq') and - name.startswith("/sys/devices/system/cpu/cpufreq/policy")): - return io.BytesIO(b"700000") - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock): - with mock.patch( - 'os.path.exists', return_value=True): - freq = psutil.cpu_freq() - self.assertEqual(freq.current, 500.0) - self.assertEqual(freq.min, 600.0) - self.assertEqual(freq.max, 700.0) - - @unittest.skipIf(not HAS_CPU_FREQ, "not supported") - def test_emulate_multi_cpu(self): - def open_mock(name, *args, **kwargs): - n = name - if (n.endswith('/scaling_cur_freq') and - n.startswith("/sys/devices/system/cpu/cpufreq/policy0")): - return io.BytesIO(b"100000") - elif (n.endswith('/scaling_min_freq') and - n.startswith("/sys/devices/system/cpu/cpufreq/policy0")): - return io.BytesIO(b"200000") - elif (n.endswith('/scaling_max_freq') and - n.startswith("/sys/devices/system/cpu/cpufreq/policy0")): - return io.BytesIO(b"300000") - elif (n.endswith('/scaling_cur_freq') and - n.startswith("/sys/devices/system/cpu/cpufreq/policy1")): - return io.BytesIO(b"400000") - elif (n.endswith('/scaling_min_freq') and - n.startswith("/sys/devices/system/cpu/cpufreq/policy1")): - return io.BytesIO(b"500000") - elif (n.endswith('/scaling_max_freq') and - n.startswith("/sys/devices/system/cpu/cpufreq/policy1")): - return io.BytesIO(b"600000") - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock): - with mock.patch('os.path.exists', return_value=True): - with mock.patch('psutil._pslinux.cpu_count_logical', - return_value=2): - freq = psutil.cpu_freq(percpu=True) - self.assertEqual(freq[0].current, 100.0) - self.assertEqual(freq[0].min, 200.0) - self.assertEqual(freq[0].max, 300.0) - self.assertEqual(freq[1].current, 400.0) - self.assertEqual(freq[1].min, 500.0) - self.assertEqual(freq[1].max, 600.0) - - @unittest.skipIf(TRAVIS, "fails on Travis") - @unittest.skipIf(not HAS_CPU_FREQ, "not supported") - def test_emulate_no_scaling_cur_freq_file(self): - # See: https://github.com/giampaolo/psutil/issues/1071 - def open_mock(name, *args, **kwargs): - if name.endswith('/scaling_cur_freq'): - raise IOError(errno.ENOENT, "") - elif name.endswith('/cpuinfo_cur_freq'): - return io.BytesIO(b"200000") - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock): - with mock.patch('os.path.exists', return_value=True): - with mock.patch('psutil._pslinux.cpu_count_logical', - return_value=1): - freq = psutil.cpu_freq() - self.assertEqual(freq.current, 200) - - # Also test that NotImplementedError is raised in case no - # current freq file is present. - - def open_mock(name, *args, **kwargs): - if name.endswith('/scaling_cur_freq'): - raise IOError(errno.ENOENT, "") - elif name.endswith('/cpuinfo_cur_freq'): - raise IOError(errno.ENOENT, "") - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock): - with mock.patch('os.path.exists', return_value=True): - self.assertRaises(NotImplementedError, psutil.cpu_freq) - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestSystemCPUStats(unittest.TestCase): - - @unittest.skipIf(TRAVIS, "fails on Travis") - def test_ctx_switches(self): - vmstat_value = vmstat("context switches") - psutil_value = psutil.cpu_stats().ctx_switches - self.assertAlmostEqual(vmstat_value, psutil_value, delta=500) - - @unittest.skipIf(TRAVIS, "fails on Travis") - def test_interrupts(self): - vmstat_value = vmstat("interrupts") - psutil_value = psutil.cpu_stats().interrupts - self.assertAlmostEqual(vmstat_value, psutil_value, delta=500) - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestLoadAvg(unittest.TestCase): - - @unittest.skipIf(not HAS_GETLOADAVG, "not supported") - def test_getloadavg(self): - psutil_value = psutil.getloadavg() - with open("/proc/loadavg", "r") as f: - proc_value = f.read().split() - - self.assertAlmostEqual(float(proc_value[0]), psutil_value[0], delta=1) - self.assertAlmostEqual(float(proc_value[1]), psutil_value[1], delta=1) - self.assertAlmostEqual(float(proc_value[2]), psutil_value[2], delta=1) - - -# ===================================================================== -# --- system network -# ===================================================================== - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestSystemNetIfAddrs(unittest.TestCase): - - def test_ips(self): - for name, addrs in psutil.net_if_addrs().items(): - for addr in addrs: - if addr.family == psutil.AF_LINK: - self.assertEqual(addr.address, get_mac_address(name)) - elif addr.family == socket.AF_INET: - self.assertEqual(addr.address, get_ipv4_address(name)) - # TODO: test for AF_INET6 family - - # XXX - not reliable when having virtual NICs installed by Docker. - # @unittest.skipIf(not which('ip'), "'ip' utility not available") - # @unittest.skipIf(TRAVIS, "skipped on Travis") - # def test_net_if_names(self): - # out = sh("ip addr").strip() - # nics = [x for x in psutil.net_if_addrs().keys() if ':' not in x] - # found = 0 - # for line in out.split('\n'): - # line = line.strip() - # if re.search(r"^\d+:", line): - # found += 1 - # name = line.split(':')[1].strip() - # self.assertIn(name, nics) - # self.assertEqual(len(nics), found, msg="%s\n---\n%s" % ( - # pprint.pformat(nics), out)) - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestSystemNetIfStats(unittest.TestCase): - - def test_against_ifconfig(self): - for name, stats in psutil.net_if_stats().items(): - try: - out = sh("ifconfig %s" % name) - except RuntimeError: - pass - else: - # Not always reliable. - # self.assertEqual(stats.isup, 'RUNNING' in out, msg=out) - self.assertEqual(stats.mtu, - int(re.findall(r'(?i)MTU[: ](\d+)', out)[0])) - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestSystemNetIOCounters(unittest.TestCase): - - @retry_on_failure() - def test_against_ifconfig(self): - def ifconfig(nic): - ret = {} - out = sh("ifconfig %s" % name) - ret['packets_recv'] = int( - re.findall(r'RX packets[: ](\d+)', out)[0]) - ret['packets_sent'] = int( - re.findall(r'TX packets[: ](\d+)', out)[0]) - ret['errin'] = int(re.findall(r'errors[: ](\d+)', out)[0]) - ret['errout'] = int(re.findall(r'errors[: ](\d+)', out)[1]) - ret['dropin'] = int(re.findall(r'dropped[: ](\d+)', out)[0]) - ret['dropout'] = int(re.findall(r'dropped[: ](\d+)', out)[1]) - ret['bytes_recv'] = int( - re.findall(r'RX (?:packets \d+ +)?bytes[: ](\d+)', out)[0]) - ret['bytes_sent'] = int( - re.findall(r'TX (?:packets \d+ +)?bytes[: ](\d+)', out)[0]) - return ret - - nio = psutil.net_io_counters(pernic=True, nowrap=False) - for name, stats in nio.items(): - try: - ifconfig_ret = ifconfig(name) - except RuntimeError: - continue - self.assertAlmostEqual( - stats.bytes_recv, ifconfig_ret['bytes_recv'], delta=1024 * 5) - self.assertAlmostEqual( - stats.bytes_sent, ifconfig_ret['bytes_sent'], delta=1024 * 5) - self.assertAlmostEqual( - stats.packets_recv, ifconfig_ret['packets_recv'], delta=1024) - self.assertAlmostEqual( - stats.packets_sent, ifconfig_ret['packets_sent'], delta=1024) - self.assertAlmostEqual( - stats.errin, ifconfig_ret['errin'], delta=10) - self.assertAlmostEqual( - stats.errout, ifconfig_ret['errout'], delta=10) - self.assertAlmostEqual( - stats.dropin, ifconfig_ret['dropin'], delta=10) - self.assertAlmostEqual( - stats.dropout, ifconfig_ret['dropout'], delta=10) - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestSystemNetConnections(unittest.TestCase): - - @mock.patch('psutil._pslinux.socket.inet_ntop', side_effect=ValueError) - @mock.patch('psutil._pslinux.supports_ipv6', return_value=False) - def test_emulate_ipv6_unsupported(self, supports_ipv6, inet_ntop): - # see: https://github.com/giampaolo/psutil/issues/623 - try: - s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) - self.addCleanup(s.close) - s.bind(("::1", 0)) - except socket.error: - pass - psutil.net_connections(kind='inet6') - - def test_emulate_unix(self): - with mock_open_content( - '/proc/net/unix', - textwrap.dedent("""\ - 0: 00000003 000 000 0001 03 462170 @/tmp/dbus-Qw2hMPIU3n - 0: 00000003 000 000 0001 03 35010 @/tmp/dbus-tB2X8h69BQ - 0: 00000003 000 000 0001 03 34424 @/tmp/dbus-cHy80Y8O - 000000000000000000000000000000000000000000000000000000 - """)) as m: - psutil.net_connections(kind='unix') - assert m.called - - -# ===================================================================== -# --- system disks -# ===================================================================== - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestSystemDiskPartitions(unittest.TestCase): - - @unittest.skipIf(not hasattr(os, 'statvfs'), "os.statvfs() not available") - @skip_on_not_implemented() - def test_against_df(self): - # test psutil.disk_usage() and psutil.disk_partitions() - # against "df -a" - def df(path): - out = sh('df -P -B 1 "%s"' % path).strip() - lines = out.split('\n') - lines.pop(0) - line = lines.pop(0) - dev, total, used, free = line.split()[:4] - if dev == 'none': - dev = '' - total, used, free = int(total), int(used), int(free) - return dev, total, used, free - - for part in psutil.disk_partitions(all=False): - usage = psutil.disk_usage(part.mountpoint) - dev, total, used, free = df(part.mountpoint) - self.assertEqual(usage.total, total) - # 10 MB tollerance - if abs(usage.free - free) > 10 * 1024 * 1024: - self.fail("psutil=%s, df=%s" % (usage.free, free)) - if abs(usage.used - used) > 10 * 1024 * 1024: - self.fail("psutil=%s, df=%s" % (usage.used, used)) - - def test_zfs_fs(self): - # Test that ZFS partitions are returned. - with open("/proc/filesystems", "r") as f: - data = f.read() - if 'zfs' in data: - for part in psutil.disk_partitions(): - if part.fstype == 'zfs': - break - else: - self.fail("couldn't find any ZFS partition") - else: - # No ZFS partitions on this system. Let's fake one. - fake_file = io.StringIO(u("nodev\tzfs\n")) - with mock.patch('psutil._common.open', - return_value=fake_file, create=True) as m1: - with mock.patch( - 'psutil._pslinux.cext.disk_partitions', - return_value=[('/dev/sdb3', '/', 'zfs', 'rw')]) as m2: - ret = psutil.disk_partitions() - assert m1.called - assert m2.called - assert ret - self.assertEqual(ret[0].fstype, 'zfs') - - def test_emulate_realpath_fail(self): - # See: https://github.com/giampaolo/psutil/issues/1307 - try: - with mock.patch('os.path.realpath', - return_value='/non/existent') as m: - with self.assertRaises(OSError) as cm: - psutil.disk_partitions() - assert m.called - self.assertEqual(cm.exception.errno, errno.ENOENT) - finally: - psutil.PROCFS_PATH = "/proc" - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestSystemDiskIoCounters(unittest.TestCase): - - def test_emulate_kernel_2_4(self): - # Tests /proc/diskstats parsing format for 2.4 kernels, see: - # https://github.com/giampaolo/psutil/issues/767 - with mock_open_content( - '/proc/diskstats', - " 3 0 1 hda 2 3 4 5 6 7 8 9 10 11 12"): - with mock.patch('psutil._pslinux.is_storage_device', - return_value=True): - ret = psutil.disk_io_counters(nowrap=False) - self.assertEqual(ret.read_count, 1) - self.assertEqual(ret.read_merged_count, 2) - self.assertEqual(ret.read_bytes, 3 * SECTOR_SIZE) - self.assertEqual(ret.read_time, 4) - self.assertEqual(ret.write_count, 5) - self.assertEqual(ret.write_merged_count, 6) - self.assertEqual(ret.write_bytes, 7 * SECTOR_SIZE) - self.assertEqual(ret.write_time, 8) - self.assertEqual(ret.busy_time, 10) - - def test_emulate_kernel_2_6_full(self): - # Tests /proc/diskstats parsing format for 2.6 kernels, - # lines reporting all metrics: - # https://github.com/giampaolo/psutil/issues/767 - with mock_open_content( - '/proc/diskstats', - " 3 0 hda 1 2 3 4 5 6 7 8 9 10 11"): - with mock.patch('psutil._pslinux.is_storage_device', - return_value=True): - ret = psutil.disk_io_counters(nowrap=False) - self.assertEqual(ret.read_count, 1) - self.assertEqual(ret.read_merged_count, 2) - self.assertEqual(ret.read_bytes, 3 * SECTOR_SIZE) - self.assertEqual(ret.read_time, 4) - self.assertEqual(ret.write_count, 5) - self.assertEqual(ret.write_merged_count, 6) - self.assertEqual(ret.write_bytes, 7 * SECTOR_SIZE) - self.assertEqual(ret.write_time, 8) - self.assertEqual(ret.busy_time, 10) - - def test_emulate_kernel_2_6_limited(self): - # Tests /proc/diskstats parsing format for 2.6 kernels, - # where one line of /proc/partitions return a limited - # amount of metrics when it bumps into a partition - # (instead of a disk). See: - # https://github.com/giampaolo/psutil/issues/767 - with mock_open_content( - '/proc/diskstats', - " 3 1 hda 1 2 3 4"): - with mock.patch('psutil._pslinux.is_storage_device', - return_value=True): - ret = psutil.disk_io_counters(nowrap=False) - self.assertEqual(ret.read_count, 1) - self.assertEqual(ret.read_bytes, 2 * SECTOR_SIZE) - self.assertEqual(ret.write_count, 3) - self.assertEqual(ret.write_bytes, 4 * SECTOR_SIZE) - - self.assertEqual(ret.read_merged_count, 0) - self.assertEqual(ret.read_time, 0) - self.assertEqual(ret.write_merged_count, 0) - self.assertEqual(ret.write_time, 0) - self.assertEqual(ret.busy_time, 0) - - def test_emulate_include_partitions(self): - # Make sure that when perdisk=True disk partitions are returned, - # see: - # https://github.com/giampaolo/psutil/pull/1313#issuecomment-408626842 - with mock_open_content( - '/proc/diskstats', - textwrap.dedent("""\ - 3 0 nvme0n1 1 2 3 4 5 6 7 8 9 10 11 - 3 0 nvme0n1p1 1 2 3 4 5 6 7 8 9 10 11 - """)): - with mock.patch('psutil._pslinux.is_storage_device', - return_value=False): - ret = psutil.disk_io_counters(perdisk=True, nowrap=False) - self.assertEqual(len(ret), 2) - self.assertEqual(ret['nvme0n1'].read_count, 1) - self.assertEqual(ret['nvme0n1p1'].read_count, 1) - self.assertEqual(ret['nvme0n1'].write_count, 5) - self.assertEqual(ret['nvme0n1p1'].write_count, 5) - - def test_emulate_exclude_partitions(self): - # Make sure that when perdisk=False partitions (e.g. 'sda1', - # 'nvme0n1p1') are skipped and not included in the total count. - # https://github.com/giampaolo/psutil/pull/1313#issuecomment-408626842 - with mock_open_content( - '/proc/diskstats', - textwrap.dedent("""\ - 3 0 nvme0n1 1 2 3 4 5 6 7 8 9 10 11 - 3 0 nvme0n1p1 1 2 3 4 5 6 7 8 9 10 11 - """)): - with mock.patch('psutil._pslinux.is_storage_device', - return_value=False): - ret = psutil.disk_io_counters(perdisk=False, nowrap=False) - self.assertIsNone(ret) - - # - def is_storage_device(name): - return name == 'nvme0n1' - - with mock_open_content( - '/proc/diskstats', - textwrap.dedent("""\ - 3 0 nvme0n1 1 2 3 4 5 6 7 8 9 10 11 - 3 0 nvme0n1p1 1 2 3 4 5 6 7 8 9 10 11 - """)): - with mock.patch('psutil._pslinux.is_storage_device', - create=True, side_effect=is_storage_device): - ret = psutil.disk_io_counters(perdisk=False, nowrap=False) - self.assertEqual(ret.read_count, 1) - self.assertEqual(ret.write_count, 5) - - def test_emulate_use_sysfs(self): - def exists(path): - if path == '/proc/diskstats': - return False - return True - - wprocfs = psutil.disk_io_counters(perdisk=True) - with mock.patch('psutil._pslinux.os.path.exists', - create=True, side_effect=exists): - wsysfs = psutil.disk_io_counters(perdisk=True) - self.assertEqual(len(wprocfs), len(wsysfs)) - - def test_emulate_not_impl(self): - def exists(path): - return False - - with mock.patch('psutil._pslinux.os.path.exists', - create=True, side_effect=exists): - self.assertRaises(NotImplementedError, psutil.disk_io_counters) - - -# ===================================================================== -# --- misc -# ===================================================================== - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestMisc(unittest.TestCase): - - def test_boot_time(self): - vmstat_value = vmstat('boot time') - psutil_value = psutil.boot_time() - self.assertEqual(int(vmstat_value), int(psutil_value)) - - def test_no_procfs_on_import(self): - my_procfs = tempfile.mkdtemp() - - with open(os.path.join(my_procfs, 'stat'), 'w') as f: - f.write('cpu 0 0 0 0 0 0 0 0 0 0\n') - f.write('cpu0 0 0 0 0 0 0 0 0 0 0\n') - f.write('cpu1 0 0 0 0 0 0 0 0 0 0\n') - - try: - orig_open = open - - def open_mock(name, *args, **kwargs): - if name.startswith('/proc'): - raise IOError(errno.ENOENT, 'rejecting access for test') - return orig_open(name, *args, **kwargs) - - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock): - reload_module(psutil) - - self.assertRaises(IOError, psutil.cpu_times) - self.assertRaises(IOError, psutil.cpu_times, percpu=True) - self.assertRaises(IOError, psutil.cpu_percent) - self.assertRaises(IOError, psutil.cpu_percent, percpu=True) - self.assertRaises(IOError, psutil.cpu_times_percent) - self.assertRaises( - IOError, psutil.cpu_times_percent, percpu=True) - - psutil.PROCFS_PATH = my_procfs - - self.assertEqual(psutil.cpu_percent(), 0) - self.assertEqual(sum(psutil.cpu_times_percent()), 0) - - # since we don't know the number of CPUs at import time, - # we awkwardly say there are none until the second call - per_cpu_percent = psutil.cpu_percent(percpu=True) - self.assertEqual(sum(per_cpu_percent), 0) - - # ditto awkward length - per_cpu_times_percent = psutil.cpu_times_percent(percpu=True) - self.assertEqual(sum(map(sum, per_cpu_times_percent)), 0) - - # much user, very busy - with open(os.path.join(my_procfs, 'stat'), 'w') as f: - f.write('cpu 1 0 0 0 0 0 0 0 0 0\n') - f.write('cpu0 1 0 0 0 0 0 0 0 0 0\n') - f.write('cpu1 1 0 0 0 0 0 0 0 0 0\n') - - self.assertNotEqual(psutil.cpu_percent(), 0) - self.assertNotEqual( - sum(psutil.cpu_percent(percpu=True)), 0) - self.assertNotEqual(sum(psutil.cpu_times_percent()), 0) - self.assertNotEqual( - sum(map(sum, psutil.cpu_times_percent(percpu=True))), 0) - finally: - shutil.rmtree(my_procfs) - reload_module(psutil) - - self.assertEqual(psutil.PROCFS_PATH, '/proc') - - def test_cpu_steal_decrease(self): - # Test cumulative cpu stats decrease. We should ignore this. - # See issue #1210. - with mock_open_content( - "/proc/stat", - textwrap.dedent("""\ - cpu 0 0 0 0 0 0 0 1 0 0 - cpu0 0 0 0 0 0 0 0 1 0 0 - cpu1 0 0 0 0 0 0 0 1 0 0 - """).encode()) as m: - # first call to "percent" functions should read the new stat file - # and compare to the "real" file read at import time - so the - # values are meaningless - psutil.cpu_percent() - assert m.called - psutil.cpu_percent(percpu=True) - psutil.cpu_times_percent() - psutil.cpu_times_percent(percpu=True) - - with mock_open_content( - "/proc/stat", - textwrap.dedent("""\ - cpu 1 0 0 0 0 0 0 0 0 0 - cpu0 1 0 0 0 0 0 0 0 0 0 - cpu1 1 0 0 0 0 0 0 0 0 0 - """).encode()) as m: - # Increase "user" while steal goes "backwards" to zero. - cpu_percent = psutil.cpu_percent() - assert m.called - cpu_percent_percpu = psutil.cpu_percent(percpu=True) - cpu_times_percent = psutil.cpu_times_percent() - cpu_times_percent_percpu = psutil.cpu_times_percent(percpu=True) - self.assertNotEqual(cpu_percent, 0) - self.assertNotEqual(sum(cpu_percent_percpu), 0) - self.assertNotEqual(sum(cpu_times_percent), 0) - self.assertNotEqual(sum(cpu_times_percent), 100.0) - self.assertNotEqual(sum(map(sum, cpu_times_percent_percpu)), 0) - self.assertNotEqual(sum(map(sum, cpu_times_percent_percpu)), 100.0) - self.assertEqual(cpu_times_percent.steal, 0) - self.assertNotEqual(cpu_times_percent.user, 0) - - def test_boot_time_mocked(self): - with mock.patch('psutil._common.open', create=True) as m: - self.assertRaises( - RuntimeError, - psutil._pslinux.boot_time) - assert m.called - - def test_users_mocked(self): - # Make sure ':0' and ':0.0' (returned by C ext) are converted - # to 'localhost'. - with mock.patch('psutil._pslinux.cext.users', - return_value=[('giampaolo', 'pts/2', ':0', - 1436573184.0, True, 2)]) as m: - self.assertEqual(psutil.users()[0].host, 'localhost') - assert m.called - with mock.patch('psutil._pslinux.cext.users', - return_value=[('giampaolo', 'pts/2', ':0.0', - 1436573184.0, True, 2)]) as m: - self.assertEqual(psutil.users()[0].host, 'localhost') - assert m.called - # ...otherwise it should be returned as-is - with mock.patch('psutil._pslinux.cext.users', - return_value=[('giampaolo', 'pts/2', 'foo', - 1436573184.0, True, 2)]) as m: - self.assertEqual(psutil.users()[0].host, 'foo') - assert m.called - - def test_procfs_path(self): - tdir = tempfile.mkdtemp() - try: - psutil.PROCFS_PATH = tdir - self.assertRaises(IOError, psutil.virtual_memory) - self.assertRaises(IOError, psutil.cpu_times) - self.assertRaises(IOError, psutil.cpu_times, percpu=True) - self.assertRaises(IOError, psutil.boot_time) - # self.assertRaises(IOError, psutil.pids) - self.assertRaises(IOError, psutil.net_connections) - self.assertRaises(IOError, psutil.net_io_counters) - self.assertRaises(IOError, psutil.net_if_stats) - # self.assertRaises(IOError, psutil.disk_io_counters) - self.assertRaises(IOError, psutil.disk_partitions) - self.assertRaises(psutil.NoSuchProcess, psutil.Process) - finally: - psutil.PROCFS_PATH = "/proc" - os.rmdir(tdir) - - def test_issue_687(self): - # In case of thread ID: - # - pid_exists() is supposed to return False - # - Process(tid) is supposed to work - # - pids() should not return the TID - # See: https://github.com/giampaolo/psutil/issues/687 - t = ThreadTask() - t.start() - try: - p = psutil.Process() - tid = p.threads()[1].id - assert not psutil.pid_exists(tid), tid - pt = psutil.Process(tid) - pt.as_dict() - self.assertNotIn(tid, psutil.pids()) - finally: - t.stop() - - def test_pid_exists_no_proc_status(self): - # Internally pid_exists relies on /proc/{pid}/status. - # Emulate a case where this file is empty in which case - # psutil is supposed to fall back on using pids(). - with mock_open_content("/proc/%s/status", "") as m: - assert psutil.pid_exists(os.getpid()) - assert m.called - - -# ===================================================================== -# --- sensors -# ===================================================================== - - -@unittest.skipIf(not LINUX, "LINUX only") -@unittest.skipIf(not HAS_BATTERY, "no battery") -class TestSensorsBattery(unittest.TestCase): - - @unittest.skipIf(not which("acpi"), "acpi utility not available") - def test_percent(self): - out = sh("acpi -b") - acpi_value = int(out.split(",")[1].strip().replace('%', '')) - psutil_value = psutil.sensors_battery().percent - self.assertAlmostEqual(acpi_value, psutil_value, delta=1) - - @unittest.skipIf(not which("acpi"), "acpi utility not available") - def test_power_plugged(self): - out = sh("acpi -b") - if 'unknown' in out.lower(): - return unittest.skip("acpi output not reliable") - if 'discharging at zero rate' in out: - plugged = True - else: - plugged = "Charging" in out.split('\n')[0] - self.assertEqual(psutil.sensors_battery().power_plugged, plugged) - - def test_emulate_power_plugged(self): - # Pretend the AC power cable is connected. - def open_mock(name, *args, **kwargs): - if name.endswith("AC0/online") or name.endswith("AC/online"): - return io.BytesIO(b"1") - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock) as m: - self.assertEqual(psutil.sensors_battery().power_plugged, True) - self.assertEqual( - psutil.sensors_battery().secsleft, psutil.POWER_TIME_UNLIMITED) - assert m.called - - def test_emulate_power_plugged_2(self): - # Same as above but pretend /AC0/online does not exist in which - # case code relies on /status file. - def open_mock(name, *args, **kwargs): - if name.endswith("AC0/online") or name.endswith("AC/online"): - raise IOError(errno.ENOENT, "") - elif name.endswith("/status"): - return io.StringIO(u("charging")) - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock) as m: - self.assertEqual(psutil.sensors_battery().power_plugged, True) - assert m.called - - def test_emulate_power_not_plugged(self): - # Pretend the AC power cable is not connected. - def open_mock(name, *args, **kwargs): - if name.endswith("AC0/online") or name.endswith("AC/online"): - return io.BytesIO(b"0") - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock) as m: - self.assertEqual(psutil.sensors_battery().power_plugged, False) - assert m.called - - def test_emulate_power_not_plugged_2(self): - # Same as above but pretend /AC0/online does not exist in which - # case code relies on /status file. - def open_mock(name, *args, **kwargs): - if name.endswith("AC0/online") or name.endswith("AC/online"): - raise IOError(errno.ENOENT, "") - elif name.endswith("/status"): - return io.StringIO(u("discharging")) - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock) as m: - self.assertEqual(psutil.sensors_battery().power_plugged, False) - assert m.called - - def test_emulate_power_undetermined(self): - # Pretend we can't know whether the AC power cable not - # connected (assert fallback to False). - def open_mock(name, *args, **kwargs): - if name.startswith("/sys/class/power_supply/AC0/online") or \ - name.startswith("/sys/class/power_supply/AC/online"): - raise IOError(errno.ENOENT, "") - elif name.startswith("/sys/class/power_supply/BAT0/status"): - return io.BytesIO(b"???") - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock) as m: - self.assertIsNone(psutil.sensors_battery().power_plugged) - assert m.called - - def test_emulate_no_base_files(self): - # Emulate a case where base metrics files are not present, - # in which case we're supposed to get None. - with mock_open_exception( - "/sys/class/power_supply/BAT0/energy_now", - IOError(errno.ENOENT, "")): - with mock_open_exception( - "/sys/class/power_supply/BAT0/charge_now", - IOError(errno.ENOENT, "")): - self.assertIsNone(psutil.sensors_battery()) - - def test_emulate_energy_full_0(self): - # Emulate a case where energy_full files returns 0. - with mock_open_content( - "/sys/class/power_supply/BAT0/energy_full", b"0") as m: - self.assertEqual(psutil.sensors_battery().percent, 0) - assert m.called - - def test_emulate_energy_full_not_avail(self): - # Emulate a case where energy_full file does not exist. - # Expected fallback on /capacity. - with mock_open_exception( - "/sys/class/power_supply/BAT0/energy_full", - IOError(errno.ENOENT, "")): - with mock_open_exception( - "/sys/class/power_supply/BAT0/charge_full", - IOError(errno.ENOENT, "")): - with mock_open_content( - "/sys/class/power_supply/BAT0/capacity", b"88"): - self.assertEqual(psutil.sensors_battery().percent, 88) - - def test_emulate_no_power(self): - # Emulate a case where /AC0/online file nor /BAT0/status exist. - with mock_open_exception( - "/sys/class/power_supply/AC/online", - IOError(errno.ENOENT, "")): - with mock_open_exception( - "/sys/class/power_supply/AC0/online", - IOError(errno.ENOENT, "")): - with mock_open_exception( - "/sys/class/power_supply/BAT0/status", - IOError(errno.ENOENT, "")): - self.assertIsNone(psutil.sensors_battery().power_plugged) - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestSensorsTemperatures(unittest.TestCase): - - @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") - def test_emulate_eio_error(self): - def open_mock(name, *args, **kwargs): - if name.endswith("_input"): - raise OSError(errno.EIO, "") - elif name.endswith("temp"): - raise OSError(errno.EIO, "") - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock) as m: - with warnings.catch_warnings(record=True) as ws: - self.assertEqual(psutil.sensors_temperatures(), {}) - assert m.called - self.assertIn("ignoring", str(ws[0].message)) - - def test_emulate_class_hwmon(self): - def open_mock(name, *args, **kwargs): - if name.endswith('/name'): - return io.StringIO(u("name")) - elif name.endswith('/temp1_label'): - return io.StringIO(u("label")) - elif name.endswith('/temp1_input'): - return io.BytesIO(b"30000") - elif name.endswith('/temp1_max'): - return io.BytesIO(b"40000") - elif name.endswith('/temp1_crit'): - return io.BytesIO(b"50000") - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock): - # Test case with /sys/class/hwmon - with mock.patch('glob.glob', - return_value=['/sys/class/hwmon/hwmon0/temp1']): - temp = psutil.sensors_temperatures()['name'][0] - self.assertEqual(temp.label, 'label') - self.assertEqual(temp.current, 30.0) - self.assertEqual(temp.high, 40.0) - self.assertEqual(temp.critical, 50.0) - - def test_emulate_class_thermal(self): - def open_mock(name, *args, **kwargs): - if name.endswith('0_temp'): - return io.BytesIO(b"50000") - elif name.endswith('temp'): - return io.BytesIO(b"30000") - elif name.endswith('0_type'): - return io.StringIO(u("critical")) - elif name.endswith('type'): - return io.StringIO(u("name")) - else: - return orig_open(name, *args, **kwargs) - - def glob_mock(path): - if path == '/sys/class/hwmon/hwmon*/temp*_*': - return [] - elif path == '/sys/class/hwmon/hwmon*/device/temp*_*': - return [] - elif path == '/sys/class/thermal/thermal_zone*': - return ['/sys/class/thermal/thermal_zone0'] - elif path == '/sys/class/thermal/thermal_zone0/trip_point*': - return ['/sys/class/thermal/thermal_zone1/trip_point_0_type', - '/sys/class/thermal/thermal_zone1/trip_point_0_temp'] - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock): - with mock.patch('glob.glob', create=True, side_effect=glob_mock): - temp = psutil.sensors_temperatures()['name'][0] - self.assertEqual(temp.label, '') - self.assertEqual(temp.current, 30.0) - self.assertEqual(temp.high, 50.0) - self.assertEqual(temp.critical, 50.0) - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestSensorsFans(unittest.TestCase): - - def test_emulate_data(self): - def open_mock(name, *args, **kwargs): - if name.endswith('/name'): - return io.StringIO(u("name")) - elif name.endswith('/fan1_label'): - return io.StringIO(u("label")) - elif name.endswith('/fan1_input'): - return io.StringIO(u("2000")) - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock): - with mock.patch('glob.glob', - return_value=['/sys/class/hwmon/hwmon2/fan1']): - fan = psutil.sensors_fans()['name'][0] - self.assertEqual(fan.label, 'label') - self.assertEqual(fan.current, 2000) - - -# ===================================================================== -# --- test process -# ===================================================================== - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestProcess(unittest.TestCase): - - def setUp(self): - safe_rmpath(TESTFN) - - tearDown = setUp - - def test_memory_full_info(self): - src = textwrap.dedent(""" - import time - with open("%s", "w") as f: - time.sleep(10) - """ % TESTFN) - sproc = pyrun(src) - self.addCleanup(reap_children) - call_until(lambda: os.listdir('.'), "'%s' not in ret" % TESTFN) - p = psutil.Process(sproc.pid) - time.sleep(.1) - mem = p.memory_full_info() - maps = p.memory_maps(grouped=False) - self.assertAlmostEqual( - mem.uss, sum([x.private_dirty + x.private_clean for x in maps]), - delta=4096) - self.assertAlmostEqual( - mem.pss, sum([x.pss for x in maps]), delta=4096) - self.assertAlmostEqual( - mem.swap, sum([x.swap for x in maps]), delta=4096) - - def test_memory_full_info_mocked(self): - # See: https://github.com/giampaolo/psutil/issues/1222 - with mock_open_content( - "/proc/%s/smaps" % os.getpid(), - textwrap.dedent("""\ - fffff0 r-xp 00000000 00:00 0 [vsyscall] - Size: 1 kB - Rss: 2 kB - Pss: 3 kB - Shared_Clean: 4 kB - Shared_Dirty: 5 kB - Private_Clean: 6 kB - Private_Dirty: 7 kB - Referenced: 8 kB - Anonymous: 9 kB - LazyFree: 10 kB - AnonHugePages: 11 kB - ShmemPmdMapped: 12 kB - Shared_Hugetlb: 13 kB - Private_Hugetlb: 14 kB - Swap: 15 kB - SwapPss: 16 kB - KernelPageSize: 17 kB - MMUPageSize: 18 kB - Locked: 19 kB - VmFlags: rd ex - """).encode()) as m: - p = psutil.Process() - mem = p.memory_full_info() - assert m.called - self.assertEqual(mem.uss, (6 + 7 + 14) * 1024) - self.assertEqual(mem.pss, 3 * 1024) - self.assertEqual(mem.swap, 15 * 1024) - - # On PYPY file descriptors are not closed fast enough. - @unittest.skipIf(PYPY, "unreliable on PYPY") - def test_open_files_mode(self): - def get_test_file(): - p = psutil.Process() - giveup_at = time.time() + 2 - while True: - for file in p.open_files(): - if file.path == os.path.abspath(TESTFN): - return file - elif time.time() > giveup_at: - break - raise RuntimeError("timeout looking for test file") - - # - with open(TESTFN, "w"): - self.assertEqual(get_test_file().mode, "w") - with open(TESTFN, "r"): - self.assertEqual(get_test_file().mode, "r") - with open(TESTFN, "a"): - self.assertEqual(get_test_file().mode, "a") - # - with open(TESTFN, "r+"): - self.assertEqual(get_test_file().mode, "r+") - with open(TESTFN, "w+"): - self.assertEqual(get_test_file().mode, "r+") - with open(TESTFN, "a+"): - self.assertEqual(get_test_file().mode, "a+") - # note: "x" bit is not supported - if PY3: - safe_rmpath(TESTFN) - with open(TESTFN, "x"): - self.assertEqual(get_test_file().mode, "w") - safe_rmpath(TESTFN) - with open(TESTFN, "x+"): - self.assertEqual(get_test_file().mode, "r+") - - def test_open_files_file_gone(self): - # simulates a file which gets deleted during open_files() - # execution - p = psutil.Process() - files = p.open_files() - with tempfile.NamedTemporaryFile(): - # give the kernel some time to see the new file - call_until(p.open_files, "len(ret) != %i" % len(files)) - with mock.patch('psutil._pslinux.os.readlink', - side_effect=OSError(errno.ENOENT, "")) as m: - files = p.open_files() - assert not files - assert m.called - # also simulate the case where os.readlink() returns EINVAL - # in which case psutil is supposed to 'continue' - with mock.patch('psutil._pslinux.os.readlink', - side_effect=OSError(errno.EINVAL, "")) as m: - self.assertEqual(p.open_files(), []) - assert m.called - - def test_open_files_fd_gone(self): - # Simulate a case where /proc/{pid}/fdinfo/{fd} disappears - # while iterating through fds. - # https://travis-ci.org/giampaolo/psutil/jobs/225694530 - p = psutil.Process() - files = p.open_files() - with tempfile.NamedTemporaryFile(): - # give the kernel some time to see the new file - call_until(p.open_files, "len(ret) != %i" % len(files)) - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, - side_effect=IOError(errno.ENOENT, "")) as m: - files = p.open_files() - assert not files - assert m.called - - # --- mocked tests - - def test_terminal_mocked(self): - with mock.patch('psutil._pslinux._psposix.get_terminal_map', - return_value={}) as m: - self.assertIsNone(psutil._pslinux.Process(os.getpid()).terminal()) - assert m.called - - # TODO: re-enable this test. - # def test_num_ctx_switches_mocked(self): - # with mock.patch('psutil._common.open', create=True) as m: - # self.assertRaises( - # NotImplementedError, - # psutil._pslinux.Process(os.getpid()).num_ctx_switches) - # assert m.called - - def test_cmdline_mocked(self): - # see: https://github.com/giampaolo/psutil/issues/639 - p = psutil.Process() - fake_file = io.StringIO(u('foo\x00bar\x00')) - with mock.patch('psutil._common.open', - return_value=fake_file, create=True) as m: - self.assertEqual(p.cmdline(), ['foo', 'bar']) - assert m.called - fake_file = io.StringIO(u('foo\x00bar\x00\x00')) - with mock.patch('psutil._common.open', - return_value=fake_file, create=True) as m: - self.assertEqual(p.cmdline(), ['foo', 'bar', '']) - assert m.called - - def test_cmdline_spaces_mocked(self): - # see: https://github.com/giampaolo/psutil/issues/1179 - p = psutil.Process() - fake_file = io.StringIO(u('foo bar ')) - with mock.patch('psutil._common.open', - return_value=fake_file, create=True) as m: - self.assertEqual(p.cmdline(), ['foo', 'bar']) - assert m.called - fake_file = io.StringIO(u('foo bar ')) - with mock.patch('psutil._common.open', - return_value=fake_file, create=True) as m: - self.assertEqual(p.cmdline(), ['foo', 'bar', '']) - assert m.called - - def test_readlink_path_deleted_mocked(self): - with mock.patch('psutil._pslinux.os.readlink', - return_value='/home/foo (deleted)'): - self.assertEqual(psutil.Process().exe(), "/home/foo") - self.assertEqual(psutil.Process().cwd(), "/home/foo") - - def test_threads_mocked(self): - # Test the case where os.listdir() returns a file (thread) - # which no longer exists by the time we open() it (race - # condition). threads() is supposed to ignore that instead - # of raising NSP. - def open_mock(name, *args, **kwargs): - if name.startswith('/proc/%s/task' % os.getpid()): - raise IOError(errno.ENOENT, "") - else: - return orig_open(name, *args, **kwargs) - - orig_open = open - patch_point = 'builtins.open' if PY3 else '__builtin__.open' - with mock.patch(patch_point, side_effect=open_mock) as m: - ret = psutil.Process().threads() - assert m.called - self.assertEqual(ret, []) - - # ...but if it bumps into something != ENOENT we want an - # exception. - def open_mock(name, *args, **kwargs): - if name.startswith('/proc/%s/task' % os.getpid()): - raise IOError(errno.EPERM, "") - else: - return orig_open(name, *args, **kwargs) - - with mock.patch(patch_point, side_effect=open_mock): - self.assertRaises(psutil.AccessDenied, psutil.Process().threads) - - def test_exe_mocked(self): - with mock.patch('psutil._pslinux.readlink', - side_effect=OSError(errno.ENOENT, "")) as m1: - with mock.patch('psutil.Process.cmdline', - side_effect=psutil.AccessDenied(0, "")) as m2: - # No such file error; might be raised also if /proc/pid/exe - # path actually exists for system processes with low pids - # (about 0-20). In this case psutil is supposed to return - # an empty string. - ret = psutil.Process().exe() - assert m1.called - assert m2.called - self.assertEqual(ret, "") - - # ...but if /proc/pid no longer exist we're supposed to treat - # it as an alias for zombie process - with mock.patch('psutil._pslinux.os.path.lexists', - return_value=False): - self.assertRaises( - psutil.ZombieProcess, psutil.Process().exe) - - def test_issue_1014(self): - # Emulates a case where smaps file does not exist. In this case - # wrap_exception decorator should not raise NoSuchProcess. - with mock_open_exception( - '/proc/%s/smaps' % os.getpid(), - IOError(errno.ENOENT, "")) as m: - p = psutil.Process() - with self.assertRaises(IOError) as err: - p.memory_maps() - self.assertEqual(err.exception.errno, errno.ENOENT) - assert m.called - - @unittest.skipIf(not HAS_RLIMIT, "not supported") - def test_rlimit_zombie(self): - # Emulate a case where rlimit() raises ENOSYS, which may - # happen in case of zombie process: - # https://travis-ci.org/giampaolo/psutil/jobs/51368273 - with mock.patch("psutil._pslinux.cext.linux_prlimit", - side_effect=OSError(errno.ENOSYS, "")) as m: - p = psutil.Process() - p.name() - with self.assertRaises(psutil.ZombieProcess) as exc: - p.rlimit(psutil.RLIMIT_NOFILE) - assert m.called - self.assertEqual(exc.exception.pid, p.pid) - self.assertEqual(exc.exception.name, p.name()) - - def test_cwd_zombie(self): - with mock.patch("psutil._pslinux.os.readlink", - side_effect=OSError(errno.ENOENT, "")) as m: - p = psutil.Process() - p.name() - with self.assertRaises(psutil.ZombieProcess) as exc: - p.cwd() - assert m.called - self.assertEqual(exc.exception.pid, p.pid) - self.assertEqual(exc.exception.name, p.name()) - - def test_stat_file_parsing(self): - from psutil._pslinux import CLOCK_TICKS - - args = [ - "0", # pid - "(cat)", # name - "Z", # status - "1", # ppid - "0", # pgrp - "0", # session - "0", # tty - "0", # tpgid - "0", # flags - "0", # minflt - "0", # cminflt - "0", # majflt - "0", # cmajflt - "2", # utime - "3", # stime - "4", # cutime - "5", # cstime - "0", # priority - "0", # nice - "0", # num_threads - "0", # itrealvalue - "6", # starttime - "0", # vsize - "0", # rss - "0", # rsslim - "0", # startcode - "0", # endcode - "0", # startstack - "0", # kstkesp - "0", # kstkeip - "0", # signal - "0", # blocked - "0", # sigignore - "0", # sigcatch - "0", # wchan - "0", # nswap - "0", # cnswap - "0", # exit_signal - "6", # processor - ] - content = " ".join(args).encode() - with mock_open_content('/proc/%s/stat' % os.getpid(), content): - p = psutil.Process() - self.assertEqual(p.name(), 'cat') - self.assertEqual(p.status(), psutil.STATUS_ZOMBIE) - self.assertEqual(p.ppid(), 1) - self.assertEqual( - p.create_time(), 6 / CLOCK_TICKS + psutil.boot_time()) - cpu = p.cpu_times() - self.assertEqual(cpu.user, 2 / CLOCK_TICKS) - self.assertEqual(cpu.system, 3 / CLOCK_TICKS) - self.assertEqual(cpu.children_user, 4 / CLOCK_TICKS) - self.assertEqual(cpu.children_system, 5 / CLOCK_TICKS) - self.assertEqual(p.cpu_num(), 6) - - def test_status_file_parsing(self): - with mock_open_content( - '/proc/%s/status' % os.getpid(), - textwrap.dedent("""\ - Uid:\t1000\t1001\t1002\t1003 - Gid:\t1004\t1005\t1006\t1007 - Threads:\t66 - Cpus_allowed:\tf - Cpus_allowed_list:\t0-7 - voluntary_ctxt_switches:\t12 - nonvoluntary_ctxt_switches:\t13""").encode()): - p = psutil.Process() - self.assertEqual(p.num_ctx_switches().voluntary, 12) - self.assertEqual(p.num_ctx_switches().involuntary, 13) - self.assertEqual(p.num_threads(), 66) - uids = p.uids() - self.assertEqual(uids.real, 1000) - self.assertEqual(uids.effective, 1001) - self.assertEqual(uids.saved, 1002) - gids = p.gids() - self.assertEqual(gids.real, 1004) - self.assertEqual(gids.effective, 1005) - self.assertEqual(gids.saved, 1006) - self.assertEqual(p._proc._get_eligible_cpus(), list(range(0, 8))) - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestProcessAgainstStatus(unittest.TestCase): - """/proc/pid/stat and /proc/pid/status have many values in common. - Whenever possible, psutil uses /proc/pid/stat (it's faster). - For all those cases we check that the value found in - /proc/pid/stat (by psutil) matches the one found in - /proc/pid/status. - """ - - @classmethod - def setUpClass(cls): - cls.proc = psutil.Process() - - def read_status_file(self, linestart): - with psutil._psplatform.open_text( - '/proc/%s/status' % self.proc.pid) as f: - for line in f: - line = line.strip() - if line.startswith(linestart): - value = line.partition('\t')[2] - try: - return int(value) - except ValueError: - return value - raise ValueError("can't find %r" % linestart) - - def test_name(self): - value = self.read_status_file("Name:") - self.assertEqual(self.proc.name(), value) - - def test_status(self): - value = self.read_status_file("State:") - value = value[value.find('(') + 1:value.rfind(')')] - value = value.replace(' ', '-') - self.assertEqual(self.proc.status(), value) - - def test_ppid(self): - value = self.read_status_file("PPid:") - self.assertEqual(self.proc.ppid(), value) - - def test_num_threads(self): - value = self.read_status_file("Threads:") - self.assertEqual(self.proc.num_threads(), value) - - def test_uids(self): - value = self.read_status_file("Uid:") - value = tuple(map(int, value.split()[1:4])) - self.assertEqual(self.proc.uids(), value) - - def test_gids(self): - value = self.read_status_file("Gid:") - value = tuple(map(int, value.split()[1:4])) - self.assertEqual(self.proc.gids(), value) - - @retry_on_failure() - def test_num_ctx_switches(self): - value = self.read_status_file("voluntary_ctxt_switches:") - self.assertEqual(self.proc.num_ctx_switches().voluntary, value) - value = self.read_status_file("nonvoluntary_ctxt_switches:") - self.assertEqual(self.proc.num_ctx_switches().involuntary, value) - - def test_cpu_affinity(self): - value = self.read_status_file("Cpus_allowed_list:") - if '-' in str(value): - min_, max_ = map(int, value.split('-')) - self.assertEqual( - self.proc.cpu_affinity(), list(range(min_, max_ + 1))) - - def test_cpu_affinity_eligible_cpus(self): - value = self.read_status_file("Cpus_allowed_list:") - with mock.patch("psutil._pslinux.per_cpu_times") as m: - self.proc._proc._get_eligible_cpus() - if '-' in str(value): - assert not m.called - else: - assert m.called - - -# ===================================================================== -# --- test utils -# ===================================================================== - - -@unittest.skipIf(not LINUX, "LINUX only") -class TestUtils(unittest.TestCase): - - def test_readlink(self): - with mock.patch("os.readlink", return_value="foo (deleted)") as m: - self.assertEqual(psutil._psplatform.readlink("bar"), "foo") - assert m.called - - def test_cat(self): - fname = os.path.abspath(TESTFN) - with open(fname, "wt") as f: - f.write("foo ") - self.assertEqual(psutil._psplatform.cat(TESTFN, binary=False), "foo") - self.assertEqual(psutil._psplatform.cat(TESTFN, binary=True), b"foo") - self.assertEqual( - psutil._psplatform.cat(TESTFN + '??', fallback="bar"), "bar") - - -if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Linux specific tests.""" + +from __future__ import division + +import collections +import contextlib +import errno +import glob +import io +import os +import re +import shutil +import socket +import struct +import textwrap +import time +import unittest +import warnings + +import psutil +from psutil import LINUX +from psutil._compat import PY3 +from psutil._compat import FileNotFoundError +from psutil._compat import basestring +from psutil._compat import u +from psutil.tests import GITHUB_ACTIONS +from psutil.tests import GLOBAL_TIMEOUT +from psutil.tests import HAS_BATTERY +from psutil.tests import HAS_CPU_FREQ +from psutil.tests import HAS_GETLOADAVG +from psutil.tests import HAS_RLIMIT +from psutil.tests import PYPY +from psutil.tests import TOLERANCE_DISK_USAGE +from psutil.tests import TOLERANCE_SYS_MEM +from psutil.tests import PsutilTestCase +from psutil.tests import ThreadTask +from psutil.tests import call_until +from psutil.tests import mock +from psutil.tests import reload_module +from psutil.tests import retry_on_failure +from psutil.tests import safe_rmpath +from psutil.tests import sh +from psutil.tests import skip_on_not_implemented +from psutil.tests import which + + +if LINUX: + from psutil._pslinux import CLOCK_TICKS + from psutil._pslinux import RootFsDeviceFinder + from psutil._pslinux import calculate_avail_vmem + from psutil._pslinux import open_binary + + +HERE = os.path.abspath(os.path.dirname(__file__)) +SIOCGIFADDR = 0x8915 +SIOCGIFCONF = 0x8912 +SIOCGIFHWADDR = 0x8927 +SIOCGIFNETMASK = 0x891B +SIOCGIFBRDADDR = 0x8919 +if LINUX: + SECTOR_SIZE = 512 +EMPTY_TEMPERATURES = not glob.glob('/sys/class/hwmon/hwmon*') + + +# ===================================================================== +# --- utils +# ===================================================================== + + +def get_ipv4_address(ifname): + import fcntl + + ifname = ifname[:15] + if PY3: + ifname = bytes(ifname, 'ascii') + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + with contextlib.closing(s): + return socket.inet_ntoa( + fcntl.ioctl(s.fileno(), SIOCGIFADDR, struct.pack('256s', ifname))[ + 20:24 + ] + ) + + +def get_ipv4_netmask(ifname): + import fcntl + + ifname = ifname[:15] + if PY3: + ifname = bytes(ifname, 'ascii') + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + with contextlib.closing(s): + return socket.inet_ntoa( + fcntl.ioctl( + s.fileno(), SIOCGIFNETMASK, struct.pack('256s', ifname) + )[20:24] + ) + + +def get_ipv4_broadcast(ifname): + import fcntl + + ifname = ifname[:15] + if PY3: + ifname = bytes(ifname, 'ascii') + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + with contextlib.closing(s): + return socket.inet_ntoa( + fcntl.ioctl( + s.fileno(), SIOCGIFBRDADDR, struct.pack('256s', ifname) + )[20:24] + ) + + +def get_ipv6_addresses(ifname): + with open("/proc/net/if_inet6") as f: + all_fields = [] + for line in f.readlines(): + fields = line.split() + if fields[-1] == ifname: + all_fields.append(fields) + + if len(all_fields) == 0: + raise ValueError("could not find interface %r" % ifname) + + for i in range(len(all_fields)): + unformatted = all_fields[i][0] + groups = [] + for j in range(0, len(unformatted), 4): + groups.append(unformatted[j : j + 4]) + formatted = ":".join(groups) + packed = socket.inet_pton(socket.AF_INET6, formatted) + all_fields[i] = socket.inet_ntop(socket.AF_INET6, packed) + return all_fields + + +def get_mac_address(ifname): + import fcntl + + ifname = ifname[:15] + if PY3: + ifname = bytes(ifname, 'ascii') + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + with contextlib.closing(s): + info = fcntl.ioctl( + s.fileno(), SIOCGIFHWADDR, struct.pack('256s', ifname) + ) + if PY3: + + def ord(x): + return x + + else: + import __builtin__ + + ord = __builtin__.ord + return ''.join(['%02x:' % ord(char) for char in info[18:24]])[:-1] + + +def free_swap(): + """Parse 'free' cmd and return swap memory's s total, used and free + values. + """ + out = sh(["free", "-b"], env={"LANG": "C.UTF-8"}) + lines = out.split('\n') + for line in lines: + if line.startswith('Swap'): + _, total, used, free = line.split() + nt = collections.namedtuple('free', 'total used free') + return nt(int(total), int(used), int(free)) + raise ValueError( + "can't find 'Swap' in 'free' output:\n%s" % '\n'.join(lines) + ) + + +def free_physmem(): + """Parse 'free' cmd and return physical memory's total, used + and free values. + """ + # Note: free can have 2 different formats, invalidating 'shared' + # and 'cached' memory which may have different positions so we + # do not return them. + # https://github.com/giampaolo/psutil/issues/538#issuecomment-57059946 + out = sh(["free", "-b"], env={"LANG": "C.UTF-8"}) + lines = out.split('\n') + for line in lines: + if line.startswith('Mem'): + total, used, free, shared = (int(x) for x in line.split()[1:5]) + nt = collections.namedtuple( + 'free', 'total used free shared output' + ) + return nt(total, used, free, shared, out) + raise ValueError( + "can't find 'Mem' in 'free' output:\n%s" % '\n'.join(lines) + ) + + +def vmstat(stat): + out = sh(["vmstat", "-s"], env={"LANG": "C.UTF-8"}) + for line in out.split("\n"): + line = line.strip() + if stat in line: + return int(line.split(' ')[0]) + raise ValueError("can't find %r in 'vmstat' output" % stat) + + +def get_free_version_info(): + out = sh(["free", "-V"]).strip() + if 'UNKNOWN' in out: + raise unittest.SkipTest("can't determine free version") + return tuple(map(int, re.findall(r'\d+', out.split()[-1]))) + + +@contextlib.contextmanager +def mock_open_content(pairs): + """Mock open() builtin and forces it to return a certain content + for a given path. `pairs` is a {"path": "content", ...} dict. + """ + + def open_mock(name, *args, **kwargs): + if name in pairs: + content = pairs[name] + if PY3: + if isinstance(content, basestring): + return io.StringIO(content) + else: + return io.BytesIO(content) + else: + return io.BytesIO(content) + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + patch_point = 'builtins.open' if PY3 else '__builtin__.open' + with mock.patch(patch_point, create=True, side_effect=open_mock) as m: + yield m + + +@contextlib.contextmanager +def mock_open_exception(for_path, exc): + """Mock open() builtin and raises `exc` if the path being opened + matches `for_path`. + """ + + def open_mock(name, *args, **kwargs): + if name == for_path: + raise exc + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + patch_point = 'builtins.open' if PY3 else '__builtin__.open' + with mock.patch(patch_point, create=True, side_effect=open_mock) as m: + yield m + + +# ===================================================================== +# --- system virtual memory +# ===================================================================== + + +@unittest.skipIf(not LINUX, "LINUX only") +class TestSystemVirtualMemoryAgainstFree(PsutilTestCase): + def test_total(self): + cli_value = free_physmem().total + psutil_value = psutil.virtual_memory().total + self.assertEqual(cli_value, psutil_value) + + @retry_on_failure() + def test_used(self): + # Older versions of procps used slab memory to calculate used memory. + # This got changed in: + # https://gitlab.com/procps-ng/procps/commit/ + # 05d751c4f076a2f0118b914c5e51cfbb4762ad8e + if get_free_version_info() < (3, 3, 12): + raise self.skipTest("old free version") + cli_value = free_physmem().used + psutil_value = psutil.virtual_memory().used + self.assertAlmostEqual( + cli_value, psutil_value, delta=TOLERANCE_SYS_MEM + ) + + @retry_on_failure() + def test_free(self): + cli_value = free_physmem().free + psutil_value = psutil.virtual_memory().free + self.assertAlmostEqual( + cli_value, psutil_value, delta=TOLERANCE_SYS_MEM + ) + + @retry_on_failure() + def test_shared(self): + free = free_physmem() + free_value = free.shared + if free_value == 0: + raise unittest.SkipTest("free does not support 'shared' column") + psutil_value = psutil.virtual_memory().shared + self.assertAlmostEqual( + free_value, + psutil_value, + delta=TOLERANCE_SYS_MEM, + msg='%s %s \n%s' % (free_value, psutil_value, free.output), + ) + + @retry_on_failure() + def test_available(self): + # "free" output format has changed at some point: + # https://github.com/giampaolo/psutil/issues/538#issuecomment-147192098 + out = sh(["free", "-b"]) + lines = out.split('\n') + if 'available' not in lines[0]: + raise unittest.SkipTest("free does not support 'available' column") + else: + free_value = int(lines[1].split()[-1]) + psutil_value = psutil.virtual_memory().available + self.assertAlmostEqual( + free_value, + psutil_value, + delta=TOLERANCE_SYS_MEM, + msg='%s %s \n%s' % (free_value, psutil_value, out), + ) + + +@unittest.skipIf(not LINUX, "LINUX only") +class TestSystemVirtualMemoryAgainstVmstat(PsutilTestCase): + def test_total(self): + vmstat_value = vmstat('total memory') * 1024 + psutil_value = psutil.virtual_memory().total + self.assertAlmostEqual( + vmstat_value, psutil_value, delta=TOLERANCE_SYS_MEM + ) + + @retry_on_failure() + def test_used(self): + # Older versions of procps used slab memory to calculate used memory. + # This got changed in: + # https://gitlab.com/procps-ng/procps/commit/ + # 05d751c4f076a2f0118b914c5e51cfbb4762ad8e + if get_free_version_info() < (3, 3, 12): + raise self.skipTest("old free version") + vmstat_value = vmstat('used memory') * 1024 + psutil_value = psutil.virtual_memory().used + self.assertAlmostEqual( + vmstat_value, psutil_value, delta=TOLERANCE_SYS_MEM + ) + + @retry_on_failure() + def test_free(self): + vmstat_value = vmstat('free memory') * 1024 + psutil_value = psutil.virtual_memory().free + self.assertAlmostEqual( + vmstat_value, psutil_value, delta=TOLERANCE_SYS_MEM + ) + + @retry_on_failure() + def test_buffers(self): + vmstat_value = vmstat('buffer memory') * 1024 + psutil_value = psutil.virtual_memory().buffers + self.assertAlmostEqual( + vmstat_value, psutil_value, delta=TOLERANCE_SYS_MEM + ) + + @retry_on_failure() + def test_active(self): + vmstat_value = vmstat('active memory') * 1024 + psutil_value = psutil.virtual_memory().active + self.assertAlmostEqual( + vmstat_value, psutil_value, delta=TOLERANCE_SYS_MEM + ) + + @retry_on_failure() + def test_inactive(self): + vmstat_value = vmstat('inactive memory') * 1024 + psutil_value = psutil.virtual_memory().inactive + self.assertAlmostEqual( + vmstat_value, psutil_value, delta=TOLERANCE_SYS_MEM + ) + + +@unittest.skipIf(not LINUX, "LINUX only") +class TestSystemVirtualMemoryMocks(PsutilTestCase): + def test_warnings_on_misses(self): + # Emulate a case where /proc/meminfo provides few info. + # psutil is supposed to set the missing fields to 0 and + # raise a warning. + content = textwrap.dedent("""\ + Active(anon): 6145416 kB + Active(file): 2950064 kB + Inactive(anon): 574764 kB + Inactive(file): 1567648 kB + MemAvailable: -1 kB + MemFree: 2057400 kB + MemTotal: 16325648 kB + SReclaimable: 346648 kB + """).encode() + with mock_open_content({'/proc/meminfo': content}) as m: + with warnings.catch_warnings(record=True) as ws: + warnings.simplefilter("always") + ret = psutil.virtual_memory() + assert m.called + self.assertEqual(len(ws), 1) + w = ws[0] + self.assertIn( + "memory stats couldn't be determined", str(w.message) + ) + self.assertIn("cached", str(w.message)) + self.assertIn("shared", str(w.message)) + self.assertIn("active", str(w.message)) + self.assertIn("inactive", str(w.message)) + self.assertIn("buffers", str(w.message)) + self.assertIn("available", str(w.message)) + self.assertEqual(ret.cached, 0) + self.assertEqual(ret.active, 0) + self.assertEqual(ret.inactive, 0) + self.assertEqual(ret.shared, 0) + self.assertEqual(ret.buffers, 0) + self.assertEqual(ret.available, 0) + self.assertEqual(ret.slab, 0) + + @retry_on_failure() + def test_avail_old_percent(self): + # Make sure that our calculation of avail mem for old kernels + # is off by max 15%. + mems = {} + with open_binary('/proc/meminfo') as f: + for line in f: + fields = line.split() + mems[fields[0]] = int(fields[1]) * 1024 + + a = calculate_avail_vmem(mems) + if b'MemAvailable:' in mems: + b = mems[b'MemAvailable:'] + diff_percent = abs(a - b) / a * 100 + self.assertLess(diff_percent, 15) + + def test_avail_old_comes_from_kernel(self): + # Make sure "MemAvailable:" coluimn is used instead of relying + # on our internal algorithm to calculate avail mem. + content = textwrap.dedent("""\ + Active: 9444728 kB + Active(anon): 6145416 kB + Active(file): 2950064 kB + Buffers: 287952 kB + Cached: 4818144 kB + Inactive(file): 1578132 kB + Inactive(anon): 574764 kB + Inactive(file): 1567648 kB + MemAvailable: 6574984 kB + MemFree: 2057400 kB + MemTotal: 16325648 kB + Shmem: 577588 kB + SReclaimable: 346648 kB + """).encode() + with mock_open_content({'/proc/meminfo': content}) as m: + with warnings.catch_warnings(record=True) as ws: + ret = psutil.virtual_memory() + assert m.called + self.assertEqual(ret.available, 6574984 * 1024) + w = ws[0] + self.assertIn( + "inactive memory stats couldn't be determined", str(w.message) + ) + + def test_avail_old_missing_fields(self): + # Remove Active(file), Inactive(file) and SReclaimable + # from /proc/meminfo and make sure the fallback is used + # (free + cached), + content = textwrap.dedent("""\ + Active: 9444728 kB + Active(anon): 6145416 kB + Buffers: 287952 kB + Cached: 4818144 kB + Inactive(file): 1578132 kB + Inactive(anon): 574764 kB + MemFree: 2057400 kB + MemTotal: 16325648 kB + Shmem: 577588 kB + """).encode() + with mock_open_content({"/proc/meminfo": content}) as m: + with warnings.catch_warnings(record=True) as ws: + ret = psutil.virtual_memory() + assert m.called + self.assertEqual(ret.available, 2057400 * 1024 + 4818144 * 1024) + w = ws[0] + self.assertIn( + "inactive memory stats couldn't be determined", str(w.message) + ) + + def test_avail_old_missing_zoneinfo(self): + # Remove /proc/zoneinfo file. Make sure fallback is used + # (free + cached). + content = textwrap.dedent("""\ + Active: 9444728 kB + Active(anon): 6145416 kB + Active(file): 2950064 kB + Buffers: 287952 kB + Cached: 4818144 kB + Inactive(file): 1578132 kB + Inactive(anon): 574764 kB + Inactive(file): 1567648 kB + MemFree: 2057400 kB + MemTotal: 16325648 kB + Shmem: 577588 kB + SReclaimable: 346648 kB + """).encode() + with mock_open_content({"/proc/meminfo": content}): + with mock_open_exception( + "/proc/zoneinfo", + IOError(errno.ENOENT, 'no such file or directory'), + ): + with warnings.catch_warnings(record=True) as ws: + ret = psutil.virtual_memory() + self.assertEqual( + ret.available, 2057400 * 1024 + 4818144 * 1024 + ) + w = ws[0] + self.assertIn( + "inactive memory stats couldn't be determined", + str(w.message), + ) + + def test_virtual_memory_mocked(self): + # Emulate /proc/meminfo because neither vmstat nor free return slab. + content = textwrap.dedent("""\ + MemTotal: 100 kB + MemFree: 2 kB + MemAvailable: 3 kB + Buffers: 4 kB + Cached: 5 kB + SwapCached: 6 kB + Active: 7 kB + Inactive: 8 kB + Active(anon): 9 kB + Inactive(anon): 10 kB + Active(file): 11 kB + Inactive(file): 12 kB + Unevictable: 13 kB + Mlocked: 14 kB + SwapTotal: 15 kB + SwapFree: 16 kB + Dirty: 17 kB + Writeback: 18 kB + AnonPages: 19 kB + Mapped: 20 kB + Shmem: 21 kB + Slab: 22 kB + SReclaimable: 23 kB + SUnreclaim: 24 kB + KernelStack: 25 kB + PageTables: 26 kB + NFS_Unstable: 27 kB + Bounce: 28 kB + WritebackTmp: 29 kB + CommitLimit: 30 kB + Committed_AS: 31 kB + VmallocTotal: 32 kB + VmallocUsed: 33 kB + VmallocChunk: 34 kB + HardwareCorrupted: 35 kB + AnonHugePages: 36 kB + ShmemHugePages: 37 kB + ShmemPmdMapped: 38 kB + CmaTotal: 39 kB + CmaFree: 40 kB + HugePages_Total: 41 kB + HugePages_Free: 42 kB + HugePages_Rsvd: 43 kB + HugePages_Surp: 44 kB + Hugepagesize: 45 kB + DirectMap46k: 46 kB + DirectMap47M: 47 kB + DirectMap48G: 48 kB + """).encode() + with mock_open_content({"/proc/meminfo": content}) as m: + mem = psutil.virtual_memory() + assert m.called + self.assertEqual(mem.total, 100 * 1024) + self.assertEqual(mem.free, 2 * 1024) + self.assertEqual(mem.buffers, 4 * 1024) + # cached mem also includes reclaimable memory + self.assertEqual(mem.cached, (5 + 23) * 1024) + self.assertEqual(mem.shared, 21 * 1024) + self.assertEqual(mem.active, 7 * 1024) + self.assertEqual(mem.inactive, 8 * 1024) + self.assertEqual(mem.slab, 22 * 1024) + self.assertEqual(mem.available, 3 * 1024) + + +# ===================================================================== +# --- system swap memory +# ===================================================================== + + +@unittest.skipIf(not LINUX, "LINUX only") +class TestSystemSwapMemory(PsutilTestCase): + @staticmethod + def meminfo_has_swap_info(): + """Return True if /proc/meminfo provides swap metrics.""" + with open("/proc/meminfo") as f: + data = f.read() + return 'SwapTotal:' in data and 'SwapFree:' in data + + def test_total(self): + free_value = free_swap().total + psutil_value = psutil.swap_memory().total + return self.assertAlmostEqual( + free_value, psutil_value, delta=TOLERANCE_SYS_MEM + ) + + @retry_on_failure() + def test_used(self): + free_value = free_swap().used + psutil_value = psutil.swap_memory().used + return self.assertAlmostEqual( + free_value, psutil_value, delta=TOLERANCE_SYS_MEM + ) + + @retry_on_failure() + def test_free(self): + free_value = free_swap().free + psutil_value = psutil.swap_memory().free + return self.assertAlmostEqual( + free_value, psutil_value, delta=TOLERANCE_SYS_MEM + ) + + def test_missing_sin_sout(self): + with mock.patch('psutil._common.open', create=True) as m: + with warnings.catch_warnings(record=True) as ws: + warnings.simplefilter("always") + ret = psutil.swap_memory() + assert m.called + self.assertEqual(len(ws), 1) + w = ws[0] + self.assertIn( + "'sin' and 'sout' swap memory stats couldn't " + "be determined", + str(w.message), + ) + self.assertEqual(ret.sin, 0) + self.assertEqual(ret.sout, 0) + + def test_no_vmstat_mocked(self): + # see https://github.com/giampaolo/psutil/issues/722 + with mock_open_exception( + "/proc/vmstat", IOError(errno.ENOENT, 'no such file or directory') + ) as m: + with warnings.catch_warnings(record=True) as ws: + warnings.simplefilter("always") + ret = psutil.swap_memory() + assert m.called + self.assertEqual(len(ws), 1) + w = ws[0] + self.assertIn( + "'sin' and 'sout' swap memory stats couldn't " + "be determined and were set to 0", + str(w.message), + ) + self.assertEqual(ret.sin, 0) + self.assertEqual(ret.sout, 0) + + def test_meminfo_against_sysinfo(self): + # Make sure the content of /proc/meminfo about swap memory + # matches sysinfo() syscall, see: + # https://github.com/giampaolo/psutil/issues/1015 + if not self.meminfo_has_swap_info(): + return unittest.skip("/proc/meminfo has no swap metrics") + with mock.patch('psutil._pslinux.cext.linux_sysinfo') as m: + swap = psutil.swap_memory() + assert not m.called + import psutil._psutil_linux as cext + + _, _, _, _, total, free, unit_multiplier = cext.linux_sysinfo() + total *= unit_multiplier + free *= unit_multiplier + self.assertEqual(swap.total, total) + self.assertAlmostEqual(swap.free, free, delta=TOLERANCE_SYS_MEM) + + def test_emulate_meminfo_has_no_metrics(self): + # Emulate a case where /proc/meminfo provides no swap metrics + # in which case sysinfo() syscall is supposed to be used + # as a fallback. + with mock_open_content({"/proc/meminfo": b""}) as m: + psutil.swap_memory() + assert m.called + + +# ===================================================================== +# --- system CPU +# ===================================================================== + + +@unittest.skipIf(not LINUX, "LINUX only") +class TestSystemCPUTimes(PsutilTestCase): + def test_fields(self): + fields = psutil.cpu_times()._fields + kernel_ver = re.findall(r'\d+\.\d+\.\d+', os.uname()[2])[0] + kernel_ver_info = tuple(map(int, kernel_ver.split('.'))) + if kernel_ver_info >= (2, 6, 11): + self.assertIn('steal', fields) + else: + self.assertNotIn('steal', fields) + if kernel_ver_info >= (2, 6, 24): + self.assertIn('guest', fields) + else: + self.assertNotIn('guest', fields) + if kernel_ver_info >= (3, 2, 0): + self.assertIn('guest_nice', fields) + else: + self.assertNotIn('guest_nice', fields) + + +@unittest.skipIf(not LINUX, "LINUX only") +class TestSystemCPUCountLogical(PsutilTestCase): + @unittest.skipIf( + not os.path.exists("/sys/devices/system/cpu/online"), + "/sys/devices/system/cpu/online does not exist", + ) + def test_against_sysdev_cpu_online(self): + with open("/sys/devices/system/cpu/online") as f: + value = f.read().strip() + if "-" in str(value): + value = int(value.split('-')[1]) + 1 + self.assertEqual(psutil.cpu_count(), value) + + @unittest.skipIf( + not os.path.exists("/sys/devices/system/cpu"), + "/sys/devices/system/cpu does not exist", + ) + def test_against_sysdev_cpu_num(self): + ls = os.listdir("/sys/devices/system/cpu") + count = len([x for x in ls if re.search(r"cpu\d+$", x) is not None]) + self.assertEqual(psutil.cpu_count(), count) + + @unittest.skipIf(not which("nproc"), "nproc utility not available") + def test_against_nproc(self): + num = int(sh("nproc --all")) + self.assertEqual(psutil.cpu_count(logical=True), num) + + @unittest.skipIf(not which("lscpu"), "lscpu utility not available") + def test_against_lscpu(self): + out = sh("lscpu -p") + num = len([x for x in out.split('\n') if not x.startswith('#')]) + self.assertEqual(psutil.cpu_count(logical=True), num) + + def test_emulate_fallbacks(self): + import psutil._pslinux + + original = psutil._pslinux.cpu_count_logical() + # Here we want to mock os.sysconf("SC_NPROCESSORS_ONLN") in + # order to cause the parsing of /proc/cpuinfo and /proc/stat. + with mock.patch( + 'psutil._pslinux.os.sysconf', side_effect=ValueError + ) as m: + self.assertEqual(psutil._pslinux.cpu_count_logical(), original) + assert m.called + + # Let's have open() return empty data and make sure None is + # returned ('cause we mimic os.cpu_count()). + with mock.patch('psutil._common.open', create=True) as m: + self.assertIsNone(psutil._pslinux.cpu_count_logical()) + self.assertEqual(m.call_count, 2) + # /proc/stat should be the last one + self.assertEqual(m.call_args[0][0], '/proc/stat') + + # Let's push this a bit further and make sure /proc/cpuinfo + # parsing works as expected. + with open('/proc/cpuinfo', 'rb') as f: + cpuinfo_data = f.read() + fake_file = io.BytesIO(cpuinfo_data) + with mock.patch( + 'psutil._common.open', return_value=fake_file, create=True + ) as m: + self.assertEqual(psutil._pslinux.cpu_count_logical(), original) + + # Finally, let's make /proc/cpuinfo return meaningless data; + # this way we'll fall back on relying on /proc/stat + with mock_open_content({"/proc/cpuinfo": b""}) as m: + self.assertEqual(psutil._pslinux.cpu_count_logical(), original) + assert m.called + + +@unittest.skipIf(not LINUX, "LINUX only") +class TestSystemCPUCountCores(PsutilTestCase): + @unittest.skipIf(not which("lscpu"), "lscpu utility not available") + def test_against_lscpu(self): + out = sh("lscpu -p") + core_ids = set() + for line in out.split('\n'): + if not line.startswith('#'): + fields = line.split(',') + core_ids.add(fields[1]) + self.assertEqual(psutil.cpu_count(logical=False), len(core_ids)) + + def test_method_2(self): + meth_1 = psutil._pslinux.cpu_count_cores() + with mock.patch('glob.glob', return_value=[]) as m: + meth_2 = psutil._pslinux.cpu_count_cores() + assert m.called + if meth_1 is not None: + self.assertEqual(meth_1, meth_2) + + def test_emulate_none(self): + with mock.patch('glob.glob', return_value=[]) as m1: + with mock.patch('psutil._common.open', create=True) as m2: + self.assertIsNone(psutil._pslinux.cpu_count_cores()) + assert m1.called + assert m2.called + + +@unittest.skipIf(not LINUX, "LINUX only") +class TestSystemCPUFrequency(PsutilTestCase): + @unittest.skipIf(not HAS_CPU_FREQ, "not supported") + def test_emulate_use_second_file(self): + # https://github.com/giampaolo/psutil/issues/981 + def path_exists_mock(path): + if path.startswith("/sys/devices/system/cpu/cpufreq/policy"): + return False + else: + return orig_exists(path) + + orig_exists = os.path.exists + with mock.patch( + "os.path.exists", side_effect=path_exists_mock, create=True + ): + assert psutil.cpu_freq() + + @unittest.skipIf(not HAS_CPU_FREQ, "not supported") + def test_emulate_use_cpuinfo(self): + # Emulate a case where /sys/devices/system/cpu/cpufreq* does not + # exist and /proc/cpuinfo is used instead. + def path_exists_mock(path): + if path.startswith('/sys/devices/system/cpu/'): + return False + else: + return os_path_exists(path) + + os_path_exists = os.path.exists + try: + with mock.patch("os.path.exists", side_effect=path_exists_mock): + reload_module(psutil._pslinux) + ret = psutil.cpu_freq() + assert ret, ret + self.assertEqual(ret.max, 0.0) + self.assertEqual(ret.min, 0.0) + for freq in psutil.cpu_freq(percpu=True): + self.assertEqual(freq.max, 0.0) + self.assertEqual(freq.min, 0.0) + finally: + reload_module(psutil._pslinux) + reload_module(psutil) + + @unittest.skipIf(not HAS_CPU_FREQ, "not supported") + def test_emulate_data(self): + def open_mock(name, *args, **kwargs): + if name.endswith('/scaling_cur_freq') and name.startswith( + "/sys/devices/system/cpu/cpufreq/policy" + ): + return io.BytesIO(b"500000") + elif name.endswith('/scaling_min_freq') and name.startswith( + "/sys/devices/system/cpu/cpufreq/policy" + ): + return io.BytesIO(b"600000") + elif name.endswith('/scaling_max_freq') and name.startswith( + "/sys/devices/system/cpu/cpufreq/policy" + ): + return io.BytesIO(b"700000") + elif name == '/proc/cpuinfo': + return io.BytesIO(b"cpu MHz : 500") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + patch_point = 'builtins.open' if PY3 else '__builtin__.open' + with mock.patch(patch_point, side_effect=open_mock): + with mock.patch('os.path.exists', return_value=True): + freq = psutil.cpu_freq() + self.assertEqual(freq.current, 500.0) + # when /proc/cpuinfo is used min and max frequencies are not + # available and are set to 0. + if freq.min != 0.0: + self.assertEqual(freq.min, 600.0) + if freq.max != 0.0: + self.assertEqual(freq.max, 700.0) + + @unittest.skipIf(not HAS_CPU_FREQ, "not supported") + def test_emulate_multi_cpu(self): + def open_mock(name, *args, **kwargs): + n = name + if n.endswith('/scaling_cur_freq') and n.startswith( + "/sys/devices/system/cpu/cpufreq/policy0" + ): + return io.BytesIO(b"100000") + elif n.endswith('/scaling_min_freq') and n.startswith( + "/sys/devices/system/cpu/cpufreq/policy0" + ): + return io.BytesIO(b"200000") + elif n.endswith('/scaling_max_freq') and n.startswith( + "/sys/devices/system/cpu/cpufreq/policy0" + ): + return io.BytesIO(b"300000") + elif n.endswith('/scaling_cur_freq') and n.startswith( + "/sys/devices/system/cpu/cpufreq/policy1" + ): + return io.BytesIO(b"400000") + elif n.endswith('/scaling_min_freq') and n.startswith( + "/sys/devices/system/cpu/cpufreq/policy1" + ): + return io.BytesIO(b"500000") + elif n.endswith('/scaling_max_freq') and n.startswith( + "/sys/devices/system/cpu/cpufreq/policy1" + ): + return io.BytesIO(b"600000") + elif name == '/proc/cpuinfo': + return io.BytesIO(b"cpu MHz : 100\ncpu MHz : 400") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + patch_point = 'builtins.open' if PY3 else '__builtin__.open' + with mock.patch(patch_point, side_effect=open_mock): + with mock.patch('os.path.exists', return_value=True): + with mock.patch( + 'psutil._pslinux.cpu_count_logical', return_value=2 + ): + freq = psutil.cpu_freq(percpu=True) + self.assertEqual(freq[0].current, 100.0) + if freq[0].min != 0.0: + self.assertEqual(freq[0].min, 200.0) + if freq[0].max != 0.0: + self.assertEqual(freq[0].max, 300.0) + self.assertEqual(freq[1].current, 400.0) + if freq[1].min != 0.0: + self.assertEqual(freq[1].min, 500.0) + if freq[1].max != 0.0: + self.assertEqual(freq[1].max, 600.0) + + @unittest.skipIf(not HAS_CPU_FREQ, "not supported") + def test_emulate_no_scaling_cur_freq_file(self): + # See: https://github.com/giampaolo/psutil/issues/1071 + def open_mock(name, *args, **kwargs): + if name.endswith('/scaling_cur_freq'): + raise IOError(errno.ENOENT, "") + elif name.endswith('/cpuinfo_cur_freq'): + return io.BytesIO(b"200000") + elif name == '/proc/cpuinfo': + return io.BytesIO(b"cpu MHz : 200") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + patch_point = 'builtins.open' if PY3 else '__builtin__.open' + with mock.patch(patch_point, side_effect=open_mock): + with mock.patch('os.path.exists', return_value=True): + with mock.patch( + 'psutil._pslinux.cpu_count_logical', return_value=1 + ): + freq = psutil.cpu_freq() + self.assertEqual(freq.current, 200) + + +@unittest.skipIf(not LINUX, "LINUX only") +class TestSystemCPUStats(PsutilTestCase): + def test_ctx_switches(self): + vmstat_value = vmstat("context switches") + psutil_value = psutil.cpu_stats().ctx_switches + self.assertAlmostEqual(vmstat_value, psutil_value, delta=500) + + def test_interrupts(self): + vmstat_value = vmstat("interrupts") + psutil_value = psutil.cpu_stats().interrupts + self.assertAlmostEqual(vmstat_value, psutil_value, delta=500) + + +@unittest.skipIf(not LINUX, "LINUX only") +class TestLoadAvg(PsutilTestCase): + @unittest.skipIf(not HAS_GETLOADAVG, "not supported") + def test_getloadavg(self): + psutil_value = psutil.getloadavg() + with open("/proc/loadavg") as f: + proc_value = f.read().split() + + self.assertAlmostEqual(float(proc_value[0]), psutil_value[0], delta=1) + self.assertAlmostEqual(float(proc_value[1]), psutil_value[1], delta=1) + self.assertAlmostEqual(float(proc_value[2]), psutil_value[2], delta=1) + + +# ===================================================================== +# --- system network +# ===================================================================== + + +@unittest.skipIf(not LINUX, "LINUX only") +class TestSystemNetIfAddrs(PsutilTestCase): + def test_ips(self): + for name, addrs in psutil.net_if_addrs().items(): + for addr in addrs: + if addr.family == psutil.AF_LINK: + self.assertEqual(addr.address, get_mac_address(name)) + elif addr.family == socket.AF_INET: + self.assertEqual(addr.address, get_ipv4_address(name)) + self.assertEqual(addr.netmask, get_ipv4_netmask(name)) + if addr.broadcast is not None: + self.assertEqual( + addr.broadcast, get_ipv4_broadcast(name) + ) + else: + self.assertEqual(get_ipv4_broadcast(name), '0.0.0.0') + elif addr.family == socket.AF_INET6: + # IPv6 addresses can have a percent symbol at the end. + # E.g. these 2 are equivalent: + # "fe80::1ff:fe23:4567:890a" + # "fe80::1ff:fe23:4567:890a%eth0" + # That is the "zone id" portion, which usually is the name + # of the network interface. + address = addr.address.split('%')[0] + self.assertIn(address, get_ipv6_addresses(name)) + + # XXX - not reliable when having virtual NICs installed by Docker. + # @unittest.skipIf(not which('ip'), "'ip' utility not available") + # def test_net_if_names(self): + # out = sh("ip addr").strip() + # nics = [x for x in psutil.net_if_addrs().keys() if ':' not in x] + # found = 0 + # for line in out.split('\n'): + # line = line.strip() + # if re.search(r"^\d+:", line): + # found += 1 + # name = line.split(':')[1].strip() + # self.assertIn(name, nics) + # self.assertEqual(len(nics), found, msg="%s\n---\n%s" % ( + # pprint.pformat(nics), out)) + + +@unittest.skipIf(not LINUX, "LINUX only") +class TestSystemNetIfStats(PsutilTestCase): + @unittest.skipIf(not which("ifconfig"), "ifconfig utility not available") + def test_against_ifconfig(self): + for name, stats in psutil.net_if_stats().items(): + try: + out = sh("ifconfig %s" % name) + except RuntimeError: + pass + else: + self.assertEqual(stats.isup, 'RUNNING' in out, msg=out) + self.assertEqual( + stats.mtu, int(re.findall(r'(?i)MTU[: ](\d+)', out)[0]) + ) + + def test_mtu(self): + for name, stats in psutil.net_if_stats().items(): + with open("/sys/class/net/%s/mtu" % name) as f: + self.assertEqual(stats.mtu, int(f.read().strip())) + + @unittest.skipIf(not which("ifconfig"), "ifconfig utility not available") + def test_flags(self): + # first line looks like this: + # "eth0: flags=4163 mtu 1500" + matches_found = 0 + for name, stats in psutil.net_if_stats().items(): + try: + out = sh("ifconfig %s" % name) + except RuntimeError: + pass + else: + match = re.search(r"flags=(\d+)?<(.*?)>", out) + if match and len(match.groups()) >= 2: + matches_found += 1 + ifconfig_flags = set(match.group(2).lower().split(",")) + psutil_flags = set(stats.flags.split(",")) + self.assertEqual(ifconfig_flags, psutil_flags) + else: + # ifconfig has a different output on CentOS 6 + # let's try that + match = re.search(r"(.*) MTU:(\d+) Metric:(\d+)", out) + if match and len(match.groups()) >= 3: + matches_found += 1 + ifconfig_flags = set(match.group(1).lower().split()) + psutil_flags = set(stats.flags.split(",")) + self.assertEqual(ifconfig_flags, psutil_flags) + + if not matches_found: + raise self.fail("no matches were found") + + +@unittest.skipIf(not LINUX, "LINUX only") +class TestSystemNetIOCounters(PsutilTestCase): + @unittest.skipIf(not which("ifconfig"), "ifconfig utility not available") + @retry_on_failure() + def test_against_ifconfig(self): + def ifconfig(nic): + ret = {} + out = sh("ifconfig %s" % nic) + ret['packets_recv'] = int( + re.findall(r'RX packets[: ](\d+)', out)[0] + ) + ret['packets_sent'] = int( + re.findall(r'TX packets[: ](\d+)', out)[0] + ) + ret['errin'] = int(re.findall(r'errors[: ](\d+)', out)[0]) + ret['errout'] = int(re.findall(r'errors[: ](\d+)', out)[1]) + ret['dropin'] = int(re.findall(r'dropped[: ](\d+)', out)[0]) + ret['dropout'] = int(re.findall(r'dropped[: ](\d+)', out)[1]) + ret['bytes_recv'] = int( + re.findall(r'RX (?:packets \d+ +)?bytes[: ](\d+)', out)[0] + ) + ret['bytes_sent'] = int( + re.findall(r'TX (?:packets \d+ +)?bytes[: ](\d+)', out)[0] + ) + return ret + + nio = psutil.net_io_counters(pernic=True, nowrap=False) + for name, stats in nio.items(): + try: + ifconfig_ret = ifconfig(name) + except RuntimeError: + continue + self.assertAlmostEqual( + stats.bytes_recv, ifconfig_ret['bytes_recv'], delta=1024 * 5 + ) + self.assertAlmostEqual( + stats.bytes_sent, ifconfig_ret['bytes_sent'], delta=1024 * 5 + ) + self.assertAlmostEqual( + stats.packets_recv, ifconfig_ret['packets_recv'], delta=1024 + ) + self.assertAlmostEqual( + stats.packets_sent, ifconfig_ret['packets_sent'], delta=1024 + ) + self.assertAlmostEqual( + stats.errin, ifconfig_ret['errin'], delta=10 + ) + self.assertAlmostEqual( + stats.errout, ifconfig_ret['errout'], delta=10 + ) + self.assertAlmostEqual( + stats.dropin, ifconfig_ret['dropin'], delta=10 + ) + self.assertAlmostEqual( + stats.dropout, ifconfig_ret['dropout'], delta=10 + ) + + +@unittest.skipIf(not LINUX, "LINUX only") +class TestSystemNetConnections(PsutilTestCase): + @mock.patch('psutil._pslinux.socket.inet_ntop', side_effect=ValueError) + @mock.patch('psutil._pslinux.supports_ipv6', return_value=False) + def test_emulate_ipv6_unsupported(self, supports_ipv6, inet_ntop): + # see: https://github.com/giampaolo/psutil/issues/623 + try: + s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + self.addCleanup(s.close) + s.bind(("::1", 0)) + except socket.error: + pass + psutil.net_connections(kind='inet6') + + def test_emulate_unix(self): + content = textwrap.dedent("""\ + 0: 00000003 000 000 0001 03 462170 @/tmp/dbus-Qw2hMPIU3n + 0: 00000003 000 000 0001 03 35010 @/tmp/dbus-tB2X8h69BQ + 0: 00000003 000 000 0001 03 34424 @/tmp/dbus-cHy80Y8O + 000000000000000000000000000000000000000000000000000000 + """) + with mock_open_content({"/proc/net/unix": content}) as m: + psutil.net_connections(kind='unix') + assert m.called + + +# ===================================================================== +# --- system disks +# ===================================================================== + + +@unittest.skipIf(not LINUX, "LINUX only") +class TestSystemDiskPartitions(PsutilTestCase): + @unittest.skipIf(not hasattr(os, 'statvfs'), "os.statvfs() not available") + @skip_on_not_implemented() + def test_against_df(self): + # test psutil.disk_usage() and psutil.disk_partitions() + # against "df -a" + def df(path): + out = sh('df -P -B 1 "%s"' % path).strip() + lines = out.split('\n') + lines.pop(0) + line = lines.pop(0) + dev, total, used, free = line.split()[:4] + if dev == 'none': + dev = '' + total, used, free = int(total), int(used), int(free) + return dev, total, used, free + + for part in psutil.disk_partitions(all=False): + usage = psutil.disk_usage(part.mountpoint) + _, total, used, free = df(part.mountpoint) + self.assertEqual(usage.total, total) + self.assertAlmostEqual( + usage.free, free, delta=TOLERANCE_DISK_USAGE + ) + self.assertAlmostEqual( + usage.used, used, delta=TOLERANCE_DISK_USAGE + ) + + def test_zfs_fs(self): + # Test that ZFS partitions are returned. + with open("/proc/filesystems") as f: + data = f.read() + if 'zfs' in data: + for part in psutil.disk_partitions(): + if part.fstype == 'zfs': + break + else: + raise self.fail("couldn't find any ZFS partition") + else: + # No ZFS partitions on this system. Let's fake one. + fake_file = io.StringIO(u("nodev\tzfs\n")) + with mock.patch( + 'psutil._common.open', return_value=fake_file, create=True + ) as m1: + with mock.patch( + 'psutil._pslinux.cext.disk_partitions', + return_value=[('/dev/sdb3', '/', 'zfs', 'rw')], + ) as m2: + ret = psutil.disk_partitions() + assert m1.called + assert m2.called + assert ret + self.assertEqual(ret[0].fstype, 'zfs') + + def test_emulate_realpath_fail(self): + # See: https://github.com/giampaolo/psutil/issues/1307 + try: + with mock.patch( + 'os.path.realpath', return_value='/non/existent' + ) as m: + with self.assertRaises(FileNotFoundError): + psutil.disk_partitions() + assert m.called + finally: + psutil.PROCFS_PATH = "/proc" + + +@unittest.skipIf(not LINUX, "LINUX only") +class TestSystemDiskIoCounters(PsutilTestCase): + def test_emulate_kernel_2_4(self): + # Tests /proc/diskstats parsing format for 2.4 kernels, see: + # https://github.com/giampaolo/psutil/issues/767 + content = " 3 0 1 hda 2 3 4 5 6 7 8 9 10 11 12" + with mock_open_content({'/proc/diskstats': content}): + with mock.patch( + 'psutil._pslinux.is_storage_device', return_value=True + ): + ret = psutil.disk_io_counters(nowrap=False) + self.assertEqual(ret.read_count, 1) + self.assertEqual(ret.read_merged_count, 2) + self.assertEqual(ret.read_bytes, 3 * SECTOR_SIZE) + self.assertEqual(ret.read_time, 4) + self.assertEqual(ret.write_count, 5) + self.assertEqual(ret.write_merged_count, 6) + self.assertEqual(ret.write_bytes, 7 * SECTOR_SIZE) + self.assertEqual(ret.write_time, 8) + self.assertEqual(ret.busy_time, 10) + + def test_emulate_kernel_2_6_full(self): + # Tests /proc/diskstats parsing format for 2.6 kernels, + # lines reporting all metrics: + # https://github.com/giampaolo/psutil/issues/767 + content = " 3 0 hda 1 2 3 4 5 6 7 8 9 10 11" + with mock_open_content({"/proc/diskstats": content}): + with mock.patch( + 'psutil._pslinux.is_storage_device', return_value=True + ): + ret = psutil.disk_io_counters(nowrap=False) + self.assertEqual(ret.read_count, 1) + self.assertEqual(ret.read_merged_count, 2) + self.assertEqual(ret.read_bytes, 3 * SECTOR_SIZE) + self.assertEqual(ret.read_time, 4) + self.assertEqual(ret.write_count, 5) + self.assertEqual(ret.write_merged_count, 6) + self.assertEqual(ret.write_bytes, 7 * SECTOR_SIZE) + self.assertEqual(ret.write_time, 8) + self.assertEqual(ret.busy_time, 10) + + def test_emulate_kernel_2_6_limited(self): + # Tests /proc/diskstats parsing format for 2.6 kernels, + # where one line of /proc/partitions return a limited + # amount of metrics when it bumps into a partition + # (instead of a disk). See: + # https://github.com/giampaolo/psutil/issues/767 + with mock_open_content({"/proc/diskstats": " 3 1 hda 1 2 3 4"}): + with mock.patch( + 'psutil._pslinux.is_storage_device', return_value=True + ): + ret = psutil.disk_io_counters(nowrap=False) + self.assertEqual(ret.read_count, 1) + self.assertEqual(ret.read_bytes, 2 * SECTOR_SIZE) + self.assertEqual(ret.write_count, 3) + self.assertEqual(ret.write_bytes, 4 * SECTOR_SIZE) + + self.assertEqual(ret.read_merged_count, 0) + self.assertEqual(ret.read_time, 0) + self.assertEqual(ret.write_merged_count, 0) + self.assertEqual(ret.write_time, 0) + self.assertEqual(ret.busy_time, 0) + + def test_emulate_include_partitions(self): + # Make sure that when perdisk=True disk partitions are returned, + # see: + # https://github.com/giampaolo/psutil/pull/1313#issuecomment-408626842 + content = textwrap.dedent("""\ + 3 0 nvme0n1 1 2 3 4 5 6 7 8 9 10 11 + 3 0 nvme0n1p1 1 2 3 4 5 6 7 8 9 10 11 + """) + with mock_open_content({"/proc/diskstats": content}): + with mock.patch( + 'psutil._pslinux.is_storage_device', return_value=False + ): + ret = psutil.disk_io_counters(perdisk=True, nowrap=False) + self.assertEqual(len(ret), 2) + self.assertEqual(ret['nvme0n1'].read_count, 1) + self.assertEqual(ret['nvme0n1p1'].read_count, 1) + self.assertEqual(ret['nvme0n1'].write_count, 5) + self.assertEqual(ret['nvme0n1p1'].write_count, 5) + + def test_emulate_exclude_partitions(self): + # Make sure that when perdisk=False partitions (e.g. 'sda1', + # 'nvme0n1p1') are skipped and not included in the total count. + # https://github.com/giampaolo/psutil/pull/1313#issuecomment-408626842 + content = textwrap.dedent("""\ + 3 0 nvme0n1 1 2 3 4 5 6 7 8 9 10 11 + 3 0 nvme0n1p1 1 2 3 4 5 6 7 8 9 10 11 + """) + with mock_open_content({"/proc/diskstats": content}): + with mock.patch( + 'psutil._pslinux.is_storage_device', return_value=False + ): + ret = psutil.disk_io_counters(perdisk=False, nowrap=False) + self.assertIsNone(ret) + + # + def is_storage_device(name): + return name == 'nvme0n1' + + content = textwrap.dedent("""\ + 3 0 nvme0n1 1 2 3 4 5 6 7 8 9 10 11 + 3 0 nvme0n1p1 1 2 3 4 5 6 7 8 9 10 11 + """) + with mock_open_content({"/proc/diskstats": content}): + with mock.patch( + 'psutil._pslinux.is_storage_device', + create=True, + side_effect=is_storage_device, + ): + ret = psutil.disk_io_counters(perdisk=False, nowrap=False) + self.assertEqual(ret.read_count, 1) + self.assertEqual(ret.write_count, 5) + + def test_emulate_use_sysfs(self): + def exists(path): + if path == '/proc/diskstats': + return False + return True + + wprocfs = psutil.disk_io_counters(perdisk=True) + with mock.patch( + 'psutil._pslinux.os.path.exists', create=True, side_effect=exists + ): + wsysfs = psutil.disk_io_counters(perdisk=True) + self.assertEqual(len(wprocfs), len(wsysfs)) + + def test_emulate_not_impl(self): + def exists(path): + return False + + with mock.patch( + 'psutil._pslinux.os.path.exists', create=True, side_effect=exists + ): + self.assertRaises(NotImplementedError, psutil.disk_io_counters) + + +@unittest.skipIf(not LINUX, "LINUX only") +class TestRootFsDeviceFinder(PsutilTestCase): + def setUp(self): + dev = os.stat("/").st_dev + self.major = os.major(dev) + self.minor = os.minor(dev) + + def test_call_methods(self): + finder = RootFsDeviceFinder() + if os.path.exists("/proc/partitions"): + finder.ask_proc_partitions() + else: + self.assertRaises(FileNotFoundError, finder.ask_proc_partitions) + if os.path.exists( + "/sys/dev/block/%s:%s/uevent" % (self.major, self.minor) + ): + finder.ask_sys_dev_block() + else: + self.assertRaises(FileNotFoundError, finder.ask_sys_dev_block) + finder.ask_sys_class_block() + + @unittest.skipIf(GITHUB_ACTIONS, "unsupported on GITHUB_ACTIONS") + def test_comparisons(self): + finder = RootFsDeviceFinder() + self.assertIsNotNone(finder.find()) + + a = b = c = None + if os.path.exists("/proc/partitions"): + a = finder.ask_proc_partitions() + if os.path.exists( + "/sys/dev/block/%s:%s/uevent" % (self.major, self.minor) + ): + b = finder.ask_sys_class_block() + c = finder.ask_sys_dev_block() + + base = a or b or c + if base and a: + self.assertEqual(base, a) + if base and b: + self.assertEqual(base, b) + if base and c: + self.assertEqual(base, c) + + @unittest.skipIf(not which("findmnt"), "findmnt utility not available") + @unittest.skipIf(GITHUB_ACTIONS, "unsupported on GITHUB_ACTIONS") + def test_against_findmnt(self): + psutil_value = RootFsDeviceFinder().find() + findmnt_value = sh("findmnt -o SOURCE -rn /") + self.assertEqual(psutil_value, findmnt_value) + + def test_disk_partitions_mocked(self): + with mock.patch( + 'psutil._pslinux.cext.disk_partitions', + return_value=[('/dev/root', '/', 'ext4', 'rw')], + ) as m: + part = psutil.disk_partitions()[0] + assert m.called + if not GITHUB_ACTIONS: + self.assertNotEqual(part.device, "/dev/root") + self.assertEqual(part.device, RootFsDeviceFinder().find()) + else: + self.assertEqual(part.device, "/dev/root") + + +# ===================================================================== +# --- misc +# ===================================================================== + + +@unittest.skipIf(not LINUX, "LINUX only") +class TestMisc(PsutilTestCase): + def test_boot_time(self): + vmstat_value = vmstat('boot time') + psutil_value = psutil.boot_time() + self.assertEqual(int(vmstat_value), int(psutil_value)) + + def test_no_procfs_on_import(self): + my_procfs = self.get_testfn() + os.mkdir(my_procfs) + + with open(os.path.join(my_procfs, 'stat'), 'w') as f: + f.write('cpu 0 0 0 0 0 0 0 0 0 0\n') + f.write('cpu0 0 0 0 0 0 0 0 0 0 0\n') + f.write('cpu1 0 0 0 0 0 0 0 0 0 0\n') + + try: + orig_open = open + + def open_mock(name, *args, **kwargs): + if name.startswith('/proc'): + raise IOError(errno.ENOENT, 'rejecting access for test') + return orig_open(name, *args, **kwargs) + + patch_point = 'builtins.open' if PY3 else '__builtin__.open' + with mock.patch(patch_point, side_effect=open_mock): + reload_module(psutil) + + self.assertRaises(IOError, psutil.cpu_times) + self.assertRaises(IOError, psutil.cpu_times, percpu=True) + self.assertRaises(IOError, psutil.cpu_percent) + self.assertRaises(IOError, psutil.cpu_percent, percpu=True) + self.assertRaises(IOError, psutil.cpu_times_percent) + self.assertRaises( + IOError, psutil.cpu_times_percent, percpu=True + ) + + psutil.PROCFS_PATH = my_procfs + + self.assertEqual(psutil.cpu_percent(), 0) + self.assertEqual(sum(psutil.cpu_times_percent()), 0) + + # since we don't know the number of CPUs at import time, + # we awkwardly say there are none until the second call + per_cpu_percent = psutil.cpu_percent(percpu=True) + self.assertEqual(sum(per_cpu_percent), 0) + + # ditto awkward length + per_cpu_times_percent = psutil.cpu_times_percent(percpu=True) + self.assertEqual(sum(map(sum, per_cpu_times_percent)), 0) + + # much user, very busy + with open(os.path.join(my_procfs, 'stat'), 'w') as f: + f.write('cpu 1 0 0 0 0 0 0 0 0 0\n') + f.write('cpu0 1 0 0 0 0 0 0 0 0 0\n') + f.write('cpu1 1 0 0 0 0 0 0 0 0 0\n') + + self.assertNotEqual(psutil.cpu_percent(), 0) + self.assertNotEqual(sum(psutil.cpu_percent(percpu=True)), 0) + self.assertNotEqual(sum(psutil.cpu_times_percent()), 0) + self.assertNotEqual( + sum(map(sum, psutil.cpu_times_percent(percpu=True))), 0 + ) + finally: + shutil.rmtree(my_procfs) + reload_module(psutil) + + self.assertEqual(psutil.PROCFS_PATH, '/proc') + + def test_cpu_steal_decrease(self): + # Test cumulative cpu stats decrease. We should ignore this. + # See issue #1210. + content = textwrap.dedent("""\ + cpu 0 0 0 0 0 0 0 1 0 0 + cpu0 0 0 0 0 0 0 0 1 0 0 + cpu1 0 0 0 0 0 0 0 1 0 0 + """).encode() + with mock_open_content({"/proc/stat": content}) as m: + # first call to "percent" functions should read the new stat file + # and compare to the "real" file read at import time - so the + # values are meaningless + psutil.cpu_percent() + assert m.called + psutil.cpu_percent(percpu=True) + psutil.cpu_times_percent() + psutil.cpu_times_percent(percpu=True) + + content = textwrap.dedent("""\ + cpu 1 0 0 0 0 0 0 0 0 0 + cpu0 1 0 0 0 0 0 0 0 0 0 + cpu1 1 0 0 0 0 0 0 0 0 0 + """).encode() + with mock_open_content({"/proc/stat": content}): + # Increase "user" while steal goes "backwards" to zero. + cpu_percent = psutil.cpu_percent() + assert m.called + cpu_percent_percpu = psutil.cpu_percent(percpu=True) + cpu_times_percent = psutil.cpu_times_percent() + cpu_times_percent_percpu = psutil.cpu_times_percent(percpu=True) + self.assertNotEqual(cpu_percent, 0) + self.assertNotEqual(sum(cpu_percent_percpu), 0) + self.assertNotEqual(sum(cpu_times_percent), 0) + self.assertNotEqual(sum(cpu_times_percent), 100.0) + self.assertNotEqual(sum(map(sum, cpu_times_percent_percpu)), 0) + self.assertNotEqual(sum(map(sum, cpu_times_percent_percpu)), 100.0) + self.assertEqual(cpu_times_percent.steal, 0) + self.assertNotEqual(cpu_times_percent.user, 0) + + def test_boot_time_mocked(self): + with mock.patch('psutil._common.open', create=True) as m: + self.assertRaises(RuntimeError, psutil._pslinux.boot_time) + assert m.called + + def test_users(self): + # Make sure the C extension converts ':0' and ':0.0' to + # 'localhost'. + for user in psutil.users(): + self.assertNotIn(user.host, (":0", ":0.0")) + + def test_procfs_path(self): + tdir = self.get_testfn() + os.mkdir(tdir) + try: + psutil.PROCFS_PATH = tdir + self.assertRaises(IOError, psutil.virtual_memory) + self.assertRaises(IOError, psutil.cpu_times) + self.assertRaises(IOError, psutil.cpu_times, percpu=True) + self.assertRaises(IOError, psutil.boot_time) + # self.assertRaises(IOError, psutil.pids) + self.assertRaises(IOError, psutil.net_connections) + self.assertRaises(IOError, psutil.net_io_counters) + self.assertRaises(IOError, psutil.net_if_stats) + # self.assertRaises(IOError, psutil.disk_io_counters) + self.assertRaises(IOError, psutil.disk_partitions) + self.assertRaises(psutil.NoSuchProcess, psutil.Process) + finally: + psutil.PROCFS_PATH = "/proc" + + @retry_on_failure() + def test_issue_687(self): + # In case of thread ID: + # - pid_exists() is supposed to return False + # - Process(tid) is supposed to work + # - pids() should not return the TID + # See: https://github.com/giampaolo/psutil/issues/687 + with ThreadTask(): + p = psutil.Process() + threads = p.threads() + self.assertEqual(len(threads), 2) + tid = sorted(threads, key=lambda x: x.id)[1].id + self.assertNotEqual(p.pid, tid) + pt = psutil.Process(tid) + pt.as_dict() + self.assertNotIn(tid, psutil.pids()) + + def test_pid_exists_no_proc_status(self): + # Internally pid_exists relies on /proc/{pid}/status. + # Emulate a case where this file is empty in which case + # psutil is supposed to fall back on using pids(). + with mock_open_content({"/proc/%s/status": ""}) as m: + assert psutil.pid_exists(os.getpid()) + assert m.called + + +# ===================================================================== +# --- sensors +# ===================================================================== + + +@unittest.skipIf(not LINUX, "LINUX only") +@unittest.skipIf(not HAS_BATTERY, "no battery") +class TestSensorsBattery(PsutilTestCase): + @unittest.skipIf(not which("acpi"), "acpi utility not available") + def test_percent(self): + out = sh("acpi -b") + acpi_value = int(out.split(",")[1].strip().replace('%', '')) + psutil_value = psutil.sensors_battery().percent + self.assertAlmostEqual(acpi_value, psutil_value, delta=1) + + def test_emulate_power_plugged(self): + # Pretend the AC power cable is connected. + def open_mock(name, *args, **kwargs): + if name.endswith(('AC0/online', 'AC/online')): + return io.BytesIO(b"1") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + patch_point = 'builtins.open' if PY3 else '__builtin__.open' + with mock.patch(patch_point, side_effect=open_mock) as m: + self.assertEqual(psutil.sensors_battery().power_plugged, True) + self.assertEqual( + psutil.sensors_battery().secsleft, psutil.POWER_TIME_UNLIMITED + ) + assert m.called + + def test_emulate_power_plugged_2(self): + # Same as above but pretend /AC0/online does not exist in which + # case code relies on /status file. + def open_mock(name, *args, **kwargs): + if name.endswith(('AC0/online', 'AC/online')): + raise IOError(errno.ENOENT, "") + elif name.endswith("/status"): + return io.StringIO(u("charging")) + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + patch_point = 'builtins.open' if PY3 else '__builtin__.open' + with mock.patch(patch_point, side_effect=open_mock) as m: + self.assertEqual(psutil.sensors_battery().power_plugged, True) + assert m.called + + def test_emulate_power_not_plugged(self): + # Pretend the AC power cable is not connected. + def open_mock(name, *args, **kwargs): + if name.endswith(('AC0/online', 'AC/online')): + return io.BytesIO(b"0") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + patch_point = 'builtins.open' if PY3 else '__builtin__.open' + with mock.patch(patch_point, side_effect=open_mock) as m: + self.assertEqual(psutil.sensors_battery().power_plugged, False) + assert m.called + + def test_emulate_power_not_plugged_2(self): + # Same as above but pretend /AC0/online does not exist in which + # case code relies on /status file. + def open_mock(name, *args, **kwargs): + if name.endswith(('AC0/online', 'AC/online')): + raise IOError(errno.ENOENT, "") + elif name.endswith("/status"): + return io.StringIO(u("discharging")) + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + patch_point = 'builtins.open' if PY3 else '__builtin__.open' + with mock.patch(patch_point, side_effect=open_mock) as m: + self.assertEqual(psutil.sensors_battery().power_plugged, False) + assert m.called + + def test_emulate_power_undetermined(self): + # Pretend we can't know whether the AC power cable not + # connected (assert fallback to False). + def open_mock(name, *args, **kwargs): + if name.startswith(( + '/sys/class/power_supply/AC0/online', + '/sys/class/power_supply/AC/online', + )): + raise IOError(errno.ENOENT, "") + elif name.startswith("/sys/class/power_supply/BAT0/status"): + return io.BytesIO(b"???") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + patch_point = 'builtins.open' if PY3 else '__builtin__.open' + with mock.patch(patch_point, side_effect=open_mock) as m: + self.assertIsNone(psutil.sensors_battery().power_plugged) + assert m.called + + def test_emulate_energy_full_0(self): + # Emulate a case where energy_full files returns 0. + with mock_open_content( + {"/sys/class/power_supply/BAT0/energy_full": b"0"} + ) as m: + self.assertEqual(psutil.sensors_battery().percent, 0) + assert m.called + + def test_emulate_energy_full_not_avail(self): + # Emulate a case where energy_full file does not exist. + # Expected fallback on /capacity. + with mock_open_exception( + "/sys/class/power_supply/BAT0/energy_full", + IOError(errno.ENOENT, ""), + ): + with mock_open_exception( + "/sys/class/power_supply/BAT0/charge_full", + IOError(errno.ENOENT, ""), + ): + with mock_open_content( + {"/sys/class/power_supply/BAT0/capacity": b"88"} + ): + self.assertEqual(psutil.sensors_battery().percent, 88) + + def test_emulate_no_power(self): + # Emulate a case where /AC0/online file nor /BAT0/status exist. + with mock_open_exception( + "/sys/class/power_supply/AC/online", IOError(errno.ENOENT, "") + ): + with mock_open_exception( + "/sys/class/power_supply/AC0/online", IOError(errno.ENOENT, "") + ): + with mock_open_exception( + "/sys/class/power_supply/BAT0/status", + IOError(errno.ENOENT, ""), + ): + self.assertIsNone(psutil.sensors_battery().power_plugged) + + +@unittest.skipIf(not LINUX, "LINUX only") +class TestSensorsBatteryEmulated(PsutilTestCase): + def test_it(self): + def open_mock(name, *args, **kwargs): + if name.endswith("/energy_now"): + return io.StringIO(u("60000000")) + elif name.endswith("/power_now"): + return io.StringIO(u("0")) + elif name.endswith("/energy_full"): + return io.StringIO(u("60000001")) + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + patch_point = 'builtins.open' if PY3 else '__builtin__.open' + with mock.patch('os.listdir', return_value=["BAT0"]) as mlistdir: + with mock.patch(patch_point, side_effect=open_mock) as mopen: + self.assertIsNotNone(psutil.sensors_battery()) + assert mlistdir.called + assert mopen.called + + +@unittest.skipIf(not LINUX, "LINUX only") +class TestSensorsTemperatures(PsutilTestCase): + def test_emulate_class_hwmon(self): + def open_mock(name, *args, **kwargs): + if name.endswith('/name'): + return io.StringIO(u("name")) + elif name.endswith('/temp1_label'): + return io.StringIO(u("label")) + elif name.endswith('/temp1_input'): + return io.BytesIO(b"30000") + elif name.endswith('/temp1_max'): + return io.BytesIO(b"40000") + elif name.endswith('/temp1_crit'): + return io.BytesIO(b"50000") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + patch_point = 'builtins.open' if PY3 else '__builtin__.open' + with mock.patch(patch_point, side_effect=open_mock): + # Test case with /sys/class/hwmon + with mock.patch( + 'glob.glob', return_value=['/sys/class/hwmon/hwmon0/temp1'] + ): + temp = psutil.sensors_temperatures()['name'][0] + self.assertEqual(temp.label, 'label') + self.assertEqual(temp.current, 30.0) + self.assertEqual(temp.high, 40.0) + self.assertEqual(temp.critical, 50.0) + + def test_emulate_class_thermal(self): + def open_mock(name, *args, **kwargs): + if name.endswith('0_temp'): + return io.BytesIO(b"50000") + elif name.endswith('temp'): + return io.BytesIO(b"30000") + elif name.endswith('0_type'): + return io.StringIO(u("critical")) + elif name.endswith('type'): + return io.StringIO(u("name")) + else: + return orig_open(name, *args, **kwargs) + + def glob_mock(path): + if path == '/sys/class/hwmon/hwmon*/temp*_*': # noqa + return [] + elif path == '/sys/class/hwmon/hwmon*/device/temp*_*': + return [] + elif path == '/sys/class/thermal/thermal_zone*': + return ['/sys/class/thermal/thermal_zone0'] + elif path == '/sys/class/thermal/thermal_zone0/trip_point*': + return [ + '/sys/class/thermal/thermal_zone1/trip_point_0_type', + '/sys/class/thermal/thermal_zone1/trip_point_0_temp', + ] + return [] + + orig_open = open + patch_point = 'builtins.open' if PY3 else '__builtin__.open' + with mock.patch(patch_point, side_effect=open_mock): + with mock.patch('glob.glob', create=True, side_effect=glob_mock): + temp = psutil.sensors_temperatures()['name'][0] + self.assertEqual(temp.label, '') + self.assertEqual(temp.current, 30.0) + self.assertEqual(temp.high, 50.0) + self.assertEqual(temp.critical, 50.0) + + +@unittest.skipIf(not LINUX, "LINUX only") +class TestSensorsFans(PsutilTestCase): + def test_emulate_data(self): + def open_mock(name, *args, **kwargs): + if name.endswith('/name'): + return io.StringIO(u("name")) + elif name.endswith('/fan1_label'): + return io.StringIO(u("label")) + elif name.endswith('/fan1_input'): + return io.StringIO(u("2000")) + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + patch_point = 'builtins.open' if PY3 else '__builtin__.open' + with mock.patch(patch_point, side_effect=open_mock): + with mock.patch( + 'glob.glob', return_value=['/sys/class/hwmon/hwmon2/fan1'] + ): + fan = psutil.sensors_fans()['name'][0] + self.assertEqual(fan.label, 'label') + self.assertEqual(fan.current, 2000) + + +# ===================================================================== +# --- test process +# ===================================================================== + + +@unittest.skipIf(not LINUX, "LINUX only") +class TestProcess(PsutilTestCase): + @retry_on_failure() + def test_parse_smaps_vs_memory_maps(self): + sproc = self.spawn_testproc() + uss, pss, swap = psutil._pslinux.Process(sproc.pid)._parse_smaps() + maps = psutil.Process(sproc.pid).memory_maps(grouped=False) + self.assertAlmostEqual( + uss, + sum([x.private_dirty + x.private_clean for x in maps]), + delta=4096, + ) + self.assertAlmostEqual(pss, sum([x.pss for x in maps]), delta=4096) + self.assertAlmostEqual(swap, sum([x.swap for x in maps]), delta=4096) + + def test_parse_smaps_mocked(self): + # See: https://github.com/giampaolo/psutil/issues/1222 + content = textwrap.dedent("""\ + fffff0 r-xp 00000000 00:00 0 [vsyscall] + Size: 1 kB + Rss: 2 kB + Pss: 3 kB + Shared_Clean: 4 kB + Shared_Dirty: 5 kB + Private_Clean: 6 kB + Private_Dirty: 7 kB + Referenced: 8 kB + Anonymous: 9 kB + LazyFree: 10 kB + AnonHugePages: 11 kB + ShmemPmdMapped: 12 kB + Shared_Hugetlb: 13 kB + Private_Hugetlb: 14 kB + Swap: 15 kB + SwapPss: 16 kB + KernelPageSize: 17 kB + MMUPageSize: 18 kB + Locked: 19 kB + VmFlags: rd ex + """).encode() + with mock_open_content({"/proc/%s/smaps" % os.getpid(): content}) as m: + p = psutil._pslinux.Process(os.getpid()) + uss, pss, swap = p._parse_smaps() + assert m.called + self.assertEqual(uss, (6 + 7 + 14) * 1024) + self.assertEqual(pss, 3 * 1024) + self.assertEqual(swap, 15 * 1024) + + # On PYPY file descriptors are not closed fast enough. + @unittest.skipIf(PYPY, "unreliable on PYPY") + def test_open_files_mode(self): + def get_test_file(fname): + p = psutil.Process() + giveup_at = time.time() + GLOBAL_TIMEOUT + while True: + for file in p.open_files(): + if file.path == os.path.abspath(fname): + return file + elif time.time() > giveup_at: + break + raise RuntimeError("timeout looking for test file") + + # + testfn = self.get_testfn() + with open(testfn, "w"): + self.assertEqual(get_test_file(testfn).mode, "w") + with open(testfn): + self.assertEqual(get_test_file(testfn).mode, "r") + with open(testfn, "a"): + self.assertEqual(get_test_file(testfn).mode, "a") + # + with open(testfn, "r+"): + self.assertEqual(get_test_file(testfn).mode, "r+") + with open(testfn, "w+"): + self.assertEqual(get_test_file(testfn).mode, "r+") + with open(testfn, "a+"): + self.assertEqual(get_test_file(testfn).mode, "a+") + # note: "x" bit is not supported + if PY3: + safe_rmpath(testfn) + with open(testfn, "x"): + self.assertEqual(get_test_file(testfn).mode, "w") + safe_rmpath(testfn) + with open(testfn, "x+"): + self.assertEqual(get_test_file(testfn).mode, "r+") + + def test_open_files_file_gone(self): + # simulates a file which gets deleted during open_files() + # execution + p = psutil.Process() + files = p.open_files() + with open(self.get_testfn(), 'w'): + # give the kernel some time to see the new file + call_until(p.open_files, "len(ret) != %i" % len(files)) + with mock.patch( + 'psutil._pslinux.os.readlink', + side_effect=OSError(errno.ENOENT, ""), + ) as m: + self.assertEqual(p.open_files(), []) + assert m.called + # also simulate the case where os.readlink() returns EINVAL + # in which case psutil is supposed to 'continue' + with mock.patch( + 'psutil._pslinux.os.readlink', + side_effect=OSError(errno.EINVAL, ""), + ) as m: + self.assertEqual(p.open_files(), []) + assert m.called + + def test_open_files_fd_gone(self): + # Simulate a case where /proc/{pid}/fdinfo/{fd} disappears + # while iterating through fds. + # https://travis-ci.org/giampaolo/psutil/jobs/225694530 + p = psutil.Process() + files = p.open_files() + with open(self.get_testfn(), 'w'): + # give the kernel some time to see the new file + call_until(p.open_files, "len(ret) != %i" % len(files)) + patch_point = 'builtins.open' if PY3 else '__builtin__.open' + with mock.patch( + patch_point, side_effect=IOError(errno.ENOENT, "") + ) as m: + self.assertEqual(p.open_files(), []) + assert m.called + + def test_open_files_enametoolong(self): + # Simulate a case where /proc/{pid}/fd/{fd} symlink + # points to a file with full path longer than PATH_MAX, see: + # https://github.com/giampaolo/psutil/issues/1940 + p = psutil.Process() + files = p.open_files() + with open(self.get_testfn(), 'w'): + # give the kernel some time to see the new file + call_until(p.open_files, "len(ret) != %i" % len(files)) + patch_point = 'psutil._pslinux.os.readlink' + with mock.patch( + patch_point, side_effect=OSError(errno.ENAMETOOLONG, "") + ) as m: + with mock.patch("psutil._pslinux.debug"): + self.assertEqual(p.open_files(), []) + assert m.called + + # --- mocked tests + + def test_terminal_mocked(self): + with mock.patch( + 'psutil._pslinux._psposix.get_terminal_map', return_value={} + ) as m: + self.assertIsNone(psutil._pslinux.Process(os.getpid()).terminal()) + assert m.called + + # TODO: re-enable this test. + # def test_num_ctx_switches_mocked(self): + # with mock.patch('psutil._common.open', create=True) as m: + # self.assertRaises( + # NotImplementedError, + # psutil._pslinux.Process(os.getpid()).num_ctx_switches) + # assert m.called + + def test_cmdline_mocked(self): + # see: https://github.com/giampaolo/psutil/issues/639 + p = psutil.Process() + fake_file = io.StringIO(u('foo\x00bar\x00')) + with mock.patch( + 'psutil._common.open', return_value=fake_file, create=True + ) as m: + self.assertEqual(p.cmdline(), ['foo', 'bar']) + assert m.called + fake_file = io.StringIO(u('foo\x00bar\x00\x00')) + with mock.patch( + 'psutil._common.open', return_value=fake_file, create=True + ) as m: + self.assertEqual(p.cmdline(), ['foo', 'bar', '']) + assert m.called + + def test_cmdline_spaces_mocked(self): + # see: https://github.com/giampaolo/psutil/issues/1179 + p = psutil.Process() + fake_file = io.StringIO(u('foo bar ')) + with mock.patch( + 'psutil._common.open', return_value=fake_file, create=True + ) as m: + self.assertEqual(p.cmdline(), ['foo', 'bar']) + assert m.called + fake_file = io.StringIO(u('foo bar ')) + with mock.patch( + 'psutil._common.open', return_value=fake_file, create=True + ) as m: + self.assertEqual(p.cmdline(), ['foo', 'bar', '']) + assert m.called + + def test_cmdline_mixed_separators(self): + # https://github.com/giampaolo/psutil/issues/ + # 1179#issuecomment-552984549 + p = psutil.Process() + fake_file = io.StringIO(u('foo\x20bar\x00')) + with mock.patch( + 'psutil._common.open', return_value=fake_file, create=True + ) as m: + self.assertEqual(p.cmdline(), ['foo', 'bar']) + assert m.called + + def test_readlink_path_deleted_mocked(self): + with mock.patch( + 'psutil._pslinux.os.readlink', return_value='/home/foo (deleted)' + ): + self.assertEqual(psutil.Process().exe(), "/home/foo") + self.assertEqual(psutil.Process().cwd(), "/home/foo") + + def test_threads_mocked(self): + # Test the case where os.listdir() returns a file (thread) + # which no longer exists by the time we open() it (race + # condition). threads() is supposed to ignore that instead + # of raising NSP. + def open_mock_1(name, *args, **kwargs): + if name.startswith('/proc/%s/task' % os.getpid()): + raise IOError(errno.ENOENT, "") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + patch_point = 'builtins.open' if PY3 else '__builtin__.open' + with mock.patch(patch_point, side_effect=open_mock_1) as m: + ret = psutil.Process().threads() + assert m.called + self.assertEqual(ret, []) + + # ...but if it bumps into something != ENOENT we want an + # exception. + def open_mock_2(name, *args, **kwargs): + if name.startswith('/proc/%s/task' % os.getpid()): + raise IOError(errno.EPERM, "") + else: + return orig_open(name, *args, **kwargs) + + with mock.patch(patch_point, side_effect=open_mock_2): + self.assertRaises(psutil.AccessDenied, psutil.Process().threads) + + def test_exe_mocked(self): + with mock.patch( + 'psutil._pslinux.readlink', side_effect=OSError(errno.ENOENT, "") + ) as m: + ret = psutil.Process().exe() + assert m.called + self.assertEqual(ret, "") + + def test_issue_1014(self): + # Emulates a case where smaps file does not exist. In this case + # wrap_exception decorator should not raise NoSuchProcess. + with mock_open_exception( + '/proc/%s/smaps' % os.getpid(), IOError(errno.ENOENT, "") + ) as m: + p = psutil.Process() + with self.assertRaises(FileNotFoundError): + p.memory_maps() + assert m.called + + @unittest.skipIf(not HAS_RLIMIT, "not supported") + def test_rlimit_zombie(self): + # Emulate a case where rlimit() raises ENOSYS, which may + # happen in case of zombie process: + # https://travis-ci.org/giampaolo/psutil/jobs/51368273 + with mock.patch( + "psutil._pslinux.prlimit", side_effect=OSError(errno.ENOSYS, "") + ) as m1: + with mock.patch( + "psutil._pslinux.Process._is_zombie", return_value=True + ) as m2: + p = psutil.Process() + p.name() + with self.assertRaises(psutil.ZombieProcess) as exc: + p.rlimit(psutil.RLIMIT_NOFILE) + assert m1.called + assert m2.called + self.assertEqual(exc.exception.pid, p.pid) + self.assertEqual(exc.exception.name, p.name()) + + def test_stat_file_parsing(self): + args = [ + "0", # pid + "(cat)", # name + "Z", # status + "1", # ppid + "0", # pgrp + "0", # session + "0", # tty + "0", # tpgid + "0", # flags + "0", # minflt + "0", # cminflt + "0", # majflt + "0", # cmajflt + "2", # utime + "3", # stime + "4", # cutime + "5", # cstime + "0", # priority + "0", # nice + "0", # num_threads + "0", # itrealvalue + "6", # starttime + "0", # vsize + "0", # rss + "0", # rsslim + "0", # startcode + "0", # endcode + "0", # startstack + "0", # kstkesp + "0", # kstkeip + "0", # signal + "0", # blocked + "0", # sigignore + "0", # sigcatch + "0", # wchan + "0", # nswap + "0", # cnswap + "0", # exit_signal + "6", # processor + "0", # rt priority + "0", # policy + "7", # delayacct_blkio_ticks + ] + content = " ".join(args).encode() + with mock_open_content({"/proc/%s/stat" % os.getpid(): content}): + p = psutil.Process() + self.assertEqual(p.name(), 'cat') + self.assertEqual(p.status(), psutil.STATUS_ZOMBIE) + self.assertEqual(p.ppid(), 1) + self.assertEqual( + p.create_time(), 6 / CLOCK_TICKS + psutil.boot_time() + ) + cpu = p.cpu_times() + self.assertEqual(cpu.user, 2 / CLOCK_TICKS) + self.assertEqual(cpu.system, 3 / CLOCK_TICKS) + self.assertEqual(cpu.children_user, 4 / CLOCK_TICKS) + self.assertEqual(cpu.children_system, 5 / CLOCK_TICKS) + self.assertEqual(cpu.iowait, 7 / CLOCK_TICKS) + self.assertEqual(p.cpu_num(), 6) + + def test_status_file_parsing(self): + content = textwrap.dedent("""\ + Uid:\t1000\t1001\t1002\t1003 + Gid:\t1004\t1005\t1006\t1007 + Threads:\t66 + Cpus_allowed:\tf + Cpus_allowed_list:\t0-7 + voluntary_ctxt_switches:\t12 + nonvoluntary_ctxt_switches:\t13""").encode() + with mock_open_content({"/proc/%s/status" % os.getpid(): content}): + p = psutil.Process() + self.assertEqual(p.num_ctx_switches().voluntary, 12) + self.assertEqual(p.num_ctx_switches().involuntary, 13) + self.assertEqual(p.num_threads(), 66) + uids = p.uids() + self.assertEqual(uids.real, 1000) + self.assertEqual(uids.effective, 1001) + self.assertEqual(uids.saved, 1002) + gids = p.gids() + self.assertEqual(gids.real, 1004) + self.assertEqual(gids.effective, 1005) + self.assertEqual(gids.saved, 1006) + self.assertEqual(p._proc._get_eligible_cpus(), list(range(8))) + + def test_connections_enametoolong(self): + # Simulate a case where /proc/{pid}/fd/{fd} symlink points to + # a file with full path longer than PATH_MAX, see: + # https://github.com/giampaolo/psutil/issues/1940 + with mock.patch( + 'psutil._pslinux.os.readlink', + side_effect=OSError(errno.ENAMETOOLONG, ""), + ) as m: + p = psutil.Process() + with mock.patch("psutil._pslinux.debug"): + self.assertEqual(p.connections(), []) + assert m.called + + +@unittest.skipIf(not LINUX, "LINUX only") +class TestProcessAgainstStatus(PsutilTestCase): + """/proc/pid/stat and /proc/pid/status have many values in common. + Whenever possible, psutil uses /proc/pid/stat (it's faster). + For all those cases we check that the value found in + /proc/pid/stat (by psutil) matches the one found in + /proc/pid/status. + """ + + @classmethod + def setUpClass(cls): + cls.proc = psutil.Process() + + def read_status_file(self, linestart): + with psutil._psplatform.open_text( + '/proc/%s/status' % self.proc.pid + ) as f: + for line in f: + line = line.strip() + if line.startswith(linestart): + value = line.partition('\t')[2] + try: + return int(value) + except ValueError: + return value + raise ValueError("can't find %r" % linestart) + + def test_name(self): + value = self.read_status_file("Name:") + self.assertEqual(self.proc.name(), value) + + def test_status(self): + value = self.read_status_file("State:") + value = value[value.find('(') + 1 : value.rfind(')')] + value = value.replace(' ', '-') + self.assertEqual(self.proc.status(), value) + + def test_ppid(self): + value = self.read_status_file("PPid:") + self.assertEqual(self.proc.ppid(), value) + + def test_num_threads(self): + value = self.read_status_file("Threads:") + self.assertEqual(self.proc.num_threads(), value) + + def test_uids(self): + value = self.read_status_file("Uid:") + value = tuple(map(int, value.split()[1:4])) + self.assertEqual(self.proc.uids(), value) + + def test_gids(self): + value = self.read_status_file("Gid:") + value = tuple(map(int, value.split()[1:4])) + self.assertEqual(self.proc.gids(), value) + + @retry_on_failure() + def test_num_ctx_switches(self): + value = self.read_status_file("voluntary_ctxt_switches:") + self.assertEqual(self.proc.num_ctx_switches().voluntary, value) + value = self.read_status_file("nonvoluntary_ctxt_switches:") + self.assertEqual(self.proc.num_ctx_switches().involuntary, value) + + def test_cpu_affinity(self): + value = self.read_status_file("Cpus_allowed_list:") + if '-' in str(value): + min_, max_ = map(int, value.split('-')) + self.assertEqual( + self.proc.cpu_affinity(), list(range(min_, max_ + 1)) + ) + + def test_cpu_affinity_eligible_cpus(self): + value = self.read_status_file("Cpus_allowed_list:") + with mock.patch("psutil._pslinux.per_cpu_times") as m: + self.proc._proc._get_eligible_cpus() + if '-' in str(value): + assert not m.called + else: + assert m.called + + +# ===================================================================== +# --- test utils +# ===================================================================== + + +@unittest.skipIf(not LINUX, "LINUX only") +class TestUtils(PsutilTestCase): + def test_readlink(self): + with mock.patch("os.readlink", return_value="foo (deleted)") as m: + self.assertEqual(psutil._psplatform.readlink("bar"), "foo") + assert m.called + + +if __name__ == '__main__': + from psutil.tests.runner import run_from_name + + run_from_name(__file__) diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_linux.pyc b/addon/globalPlugins/soundmanager/psutil/tests/test_linux.pyc deleted file mode 100644 index c3c3b05..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/tests/test_linux.pyc and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_memory_leaks.py b/addon/globalPlugins/soundmanager/psutil/tests/test_memleaks.py similarity index 50% rename from addon/globalPlugins/soundmanager/psutil/tests/test_memory_leaks.py rename to addon/globalPlugins/soundmanager/psutil/tests/test_memleaks.py index dde50a5..9919b3b 100644 --- a/addon/globalPlugins/soundmanager/psutil/tests/test_memory_leaks.py +++ b/addon/globalPlugins/soundmanager/psutil/tests/test_memleaks.py @@ -1,597 +1,496 @@ -#!/usr/bin/env python - -# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -""" -Tests for detecting function memory leaks (typically the ones -implemented in C). It does so by calling a function many times and -checking whether process memory usage keeps increasing between -calls or over time. -Note that this may produce false positives (especially on Windows -for some reason). -""" - -from __future__ import print_function -import errno -import functools -import gc -import os -import sys -import threading -import time - -import psutil -import psutil._common -from psutil import LINUX -from psutil import MACOS -from psutil import OPENBSD -from psutil import POSIX -from psutil import SUNOS -from psutil import WINDOWS -from psutil._common import bytes2human -from psutil._compat import xrange -from psutil.tests import create_sockets -from psutil.tests import get_test_subprocess -from psutil.tests import HAS_CPU_AFFINITY -from psutil.tests import HAS_CPU_FREQ -from psutil.tests import HAS_GETLOADAVG -from psutil.tests import HAS_ENVIRON -from psutil.tests import HAS_IONICE -from psutil.tests import HAS_MEMORY_MAPS -from psutil.tests import HAS_NET_IO_COUNTERS -from psutil.tests import HAS_PROC_CPU_NUM -from psutil.tests import HAS_PROC_IO_COUNTERS -from psutil.tests import HAS_RLIMIT -from psutil.tests import HAS_SENSORS_BATTERY -from psutil.tests import HAS_SENSORS_FANS -from psutil.tests import HAS_SENSORS_TEMPERATURES -from psutil.tests import reap_children -from psutil.tests import safe_rmpath -from psutil.tests import skip_on_access_denied -from psutil.tests import TESTFN -from psutil.tests import TRAVIS -from psutil.tests import unittest - - -LOOPS = 1000 -MEMORY_TOLERANCE = 4096 -RETRY_FOR = 3 - -SKIP_PYTHON_IMPL = True if TRAVIS else False -cext = psutil._psplatform.cext -thisproc = psutil.Process() -SKIP_PYTHON_IMPL = True if TRAVIS else False - - -# =================================================================== -# utils -# =================================================================== - - -def skip_if_linux(): - return unittest.skipIf(LINUX and SKIP_PYTHON_IMPL, - "worthless on LINUX (pure python)") - - -class TestMemLeak(unittest.TestCase): - """Base framework class which calls a function many times and - produces a failure if process memory usage keeps increasing - between calls or over time. - """ - tolerance = MEMORY_TOLERANCE - loops = LOOPS - retry_for = RETRY_FOR - - def setUp(self): - gc.collect() - - def execute(self, fun, *args, **kwargs): - """Test a callable.""" - def call_many_times(): - for x in xrange(loops): - self._call(fun, *args, **kwargs) - del x - gc.collect() - - tolerance = kwargs.pop('tolerance_', None) or self.tolerance - loops = kwargs.pop('loops_', None) or self.loops - retry_for = kwargs.pop('retry_for_', None) or self.retry_for - - # warm up - for x in range(10): - self._call(fun, *args, **kwargs) - self.assertEqual(gc.garbage, []) - self.assertEqual(threading.active_count(), 1) - self.assertEqual(thisproc.children(), []) - - # Get 2 distinct memory samples, before and after having - # called fun repeadetly. - # step 1 - call_many_times() - mem1 = self._get_mem() - # step 2 - call_many_times() - mem2 = self._get_mem() - - diff1 = mem2 - mem1 - if diff1 > tolerance: - # This doesn't necessarily mean we have a leak yet. - # At this point we assume that after having called the - # function so many times the memory usage is stabilized - # and if there are no leaks it should not increase - # anymore. - # Let's keep calling fun for 3 more seconds and fail if - # we notice any difference. - ncalls = 0 - stop_at = time.time() + retry_for - while time.time() <= stop_at: - self._call(fun, *args, **kwargs) - ncalls += 1 - - del stop_at - gc.collect() - mem3 = self._get_mem() - diff2 = mem3 - mem2 - - if mem3 > mem2: - # failure - extra_proc_mem = bytes2human(diff1 + diff2) - print("exta proc mem: %s" % extra_proc_mem, file=sys.stderr) - msg = "+%s after %s calls, +%s after another %s calls, " - msg += "+%s extra proc mem" - msg = msg % ( - bytes2human(diff1), loops, bytes2human(diff2), ncalls, - extra_proc_mem) - self.fail(msg) - - def execute_w_exc(self, exc, fun, *args, **kwargs): - """Convenience function which tests a callable raising - an exception. - """ - def call(): - self.assertRaises(exc, fun, *args, **kwargs) - - self.execute(call) - - @staticmethod - def _get_mem(): - # By using USS memory it seems it's less likely to bump - # into false positives. - if LINUX or WINDOWS or MACOS: - return thisproc.memory_full_info().uss - else: - return thisproc.memory_info().rss - - @staticmethod - def _call(fun, *args, **kwargs): - fun(*args, **kwargs) - - -# =================================================================== -# Process class -# =================================================================== - - -class TestProcessObjectLeaks(TestMemLeak): - """Test leaks of Process class methods.""" - - proc = thisproc - - def test_coverage(self): - skip = set(( - "pid", "as_dict", "children", "cpu_affinity", "cpu_percent", - "ionice", "is_running", "kill", "memory_info_ex", "memory_percent", - "nice", "oneshot", "parent", "parents", "rlimit", "send_signal", - "suspend", "terminate", "wait")) - for name in dir(psutil.Process): - if name.startswith('_'): - continue - if name in skip: - continue - self.assertTrue(hasattr(self, "test_" + name), msg=name) - - @skip_if_linux() - def test_name(self): - self.execute(self.proc.name) - - @skip_if_linux() - def test_cmdline(self): - self.execute(self.proc.cmdline) - - @skip_if_linux() - def test_exe(self): - self.execute(self.proc.exe) - - @skip_if_linux() - def test_ppid(self): - self.execute(self.proc.ppid) - - @unittest.skipIf(not POSIX, "POSIX only") - @skip_if_linux() - def test_uids(self): - self.execute(self.proc.uids) - - @unittest.skipIf(not POSIX, "POSIX only") - @skip_if_linux() - def test_gids(self): - self.execute(self.proc.gids) - - @skip_if_linux() - def test_status(self): - self.execute(self.proc.status) - - def test_nice_get(self): - self.execute(self.proc.nice) - - def test_nice_set(self): - niceness = thisproc.nice() - self.execute(self.proc.nice, niceness) - - @unittest.skipIf(not HAS_IONICE, "not supported") - def test_ionice_get(self): - self.execute(self.proc.ionice) - - @unittest.skipIf(not HAS_IONICE, "not supported") - def test_ionice_set(self): - if WINDOWS: - value = thisproc.ionice() - self.execute(self.proc.ionice, value) - else: - self.execute(self.proc.ionice, psutil.IOPRIO_CLASS_NONE) - fun = functools.partial(cext.proc_ioprio_set, os.getpid(), -1, 0) - self.execute_w_exc(OSError, fun) - - @unittest.skipIf(not HAS_PROC_IO_COUNTERS, "not supported") - @skip_if_linux() - def test_io_counters(self): - self.execute(self.proc.io_counters) - - @unittest.skipIf(POSIX, "worthless on POSIX") - def test_username(self): - self.execute(self.proc.username) - - @skip_if_linux() - def test_create_time(self): - self.execute(self.proc.create_time) - - @skip_if_linux() - @skip_on_access_denied(only_if=OPENBSD) - def test_num_threads(self): - self.execute(self.proc.num_threads) - - @unittest.skipIf(not WINDOWS, "WINDOWS only") - def test_num_handles(self): - self.execute(self.proc.num_handles) - - @unittest.skipIf(not POSIX, "POSIX only") - @skip_if_linux() - def test_num_fds(self): - self.execute(self.proc.num_fds) - - @skip_if_linux() - def test_num_ctx_switches(self): - self.execute(self.proc.num_ctx_switches) - - @skip_if_linux() - @skip_on_access_denied(only_if=OPENBSD) - def test_threads(self): - self.execute(self.proc.threads) - - @skip_if_linux() - def test_cpu_times(self): - self.execute(self.proc.cpu_times) - - @skip_if_linux() - @unittest.skipIf(not HAS_PROC_CPU_NUM, "not supported") - def test_cpu_num(self): - self.execute(self.proc.cpu_num) - - @skip_if_linux() - def test_memory_info(self): - self.execute(self.proc.memory_info) - - @skip_if_linux() - def test_memory_full_info(self): - self.execute(self.proc.memory_full_info) - - @unittest.skipIf(not POSIX, "POSIX only") - @skip_if_linux() - def test_terminal(self): - self.execute(self.proc.terminal) - - @unittest.skipIf(POSIX and SKIP_PYTHON_IMPL, - "worthless on POSIX (pure python)") - def test_resume(self): - self.execute(self.proc.resume) - - @skip_if_linux() - def test_cwd(self): - self.execute(self.proc.cwd) - - @unittest.skipIf(not HAS_CPU_AFFINITY, "not supported") - def test_cpu_affinity_get(self): - self.execute(self.proc.cpu_affinity) - - @unittest.skipIf(not HAS_CPU_AFFINITY, "not supported") - def test_cpu_affinity_set(self): - affinity = thisproc.cpu_affinity() - self.execute(self.proc.cpu_affinity, affinity) - if not TRAVIS: - self.execute_w_exc(ValueError, self.proc.cpu_affinity, [-1]) - - @skip_if_linux() - def test_open_files(self): - safe_rmpath(TESTFN) # needed after UNIX socket test has run - with open(TESTFN, 'w'): - self.execute(self.proc.open_files) - - @unittest.skipIf(not HAS_MEMORY_MAPS, "not supported") - @skip_if_linux() - def test_memory_maps(self): - self.execute(self.proc.memory_maps) - - @unittest.skipIf(not LINUX, "LINUX only") - @unittest.skipIf(not HAS_RLIMIT, "not supported") - def test_rlimit_get(self): - self.execute(self.proc.rlimit, psutil.RLIMIT_NOFILE) - - @unittest.skipIf(not LINUX, "LINUX only") - @unittest.skipIf(not HAS_RLIMIT, "not supported") - def test_rlimit_set(self): - limit = thisproc.rlimit(psutil.RLIMIT_NOFILE) - self.execute(self.proc.rlimit, psutil.RLIMIT_NOFILE, limit) - self.execute_w_exc(OSError, self.proc.rlimit, -1) - - @skip_if_linux() - # Windows implementation is based on a single system-wide - # function (tested later). - @unittest.skipIf(WINDOWS, "worthless on WINDOWS") - def test_connections(self): - # TODO: UNIX sockets are temporarily implemented by parsing - # 'pfiles' cmd output; we don't want that part of the code to - # be executed. - with create_sockets(): - kind = 'inet' if SUNOS else 'all' - self.execute(self.proc.connections, kind) - - @unittest.skipIf(not HAS_ENVIRON, "not supported") - def test_environ(self): - self.execute(self.proc.environ) - - @unittest.skipIf(not WINDOWS, "WINDOWS only") - def test_proc_info(self): - self.execute(cext.proc_info, os.getpid()) - - -class TestProcessDualImplementation(TestMemLeak): - - if WINDOWS: - def test_cmdline_peb_true(self): - self.execute(cext.proc_cmdline, os.getpid(), use_peb=True) - - def test_cmdline_peb_false(self): - self.execute(cext.proc_cmdline, os.getpid(), use_peb=False) - - -class TestTerminatedProcessLeaks(TestProcessObjectLeaks): - """Repeat the tests above looking for leaks occurring when dealing - with terminated processes raising NoSuchProcess exception. - The C functions are still invoked but will follow different code - paths. We'll check those code paths. - """ - - @classmethod - def setUpClass(cls): - super(TestTerminatedProcessLeaks, cls).setUpClass() - p = get_test_subprocess() - cls.proc = psutil.Process(p.pid) - cls.proc.kill() - cls.proc.wait() - - @classmethod - def tearDownClass(cls): - super(TestTerminatedProcessLeaks, cls).tearDownClass() - reap_children() - - def _call(self, fun, *args, **kwargs): - try: - fun(*args, **kwargs) - except psutil.NoSuchProcess: - pass - - if WINDOWS: - - def test_kill(self): - self.execute(self.proc.kill) - - def test_terminate(self): - self.execute(self.proc.terminate) - - def test_suspend(self): - self.execute(self.proc.suspend) - - def test_resume(self): - self.execute(self.proc.resume) - - def test_wait(self): - self.execute(self.proc.wait) - - def test_proc_info(self): - # test dual implementation - def call(): - try: - return cext.proc_info(self.proc.pid) - except OSError as err: - if err.errno != errno.ESRCH: - raise - - self.execute(call) - - -# =================================================================== -# system APIs -# =================================================================== - - -class TestModuleFunctionsLeaks(TestMemLeak): - """Test leaks of psutil module functions.""" - - def test_coverage(self): - skip = set(( - "version_info", "__version__", "process_iter", "wait_procs", - "cpu_percent", "cpu_times_percent", "cpu_count")) - for name in psutil.__all__: - if not name.islower(): - continue - if name in skip: - continue - self.assertTrue(hasattr(self, "test_" + name), msg=name) - - # --- cpu - - @skip_if_linux() - def test_cpu_count_logical(self): - self.execute(psutil.cpu_count, logical=True) - - @skip_if_linux() - def test_cpu_count_physical(self): - self.execute(psutil.cpu_count, logical=False) - - @skip_if_linux() - def test_cpu_times(self): - self.execute(psutil.cpu_times) - - @skip_if_linux() - def test_per_cpu_times(self): - self.execute(psutil.cpu_times, percpu=True) - - def test_cpu_stats(self): - self.execute(psutil.cpu_stats) - - @skip_if_linux() - @unittest.skipIf(not HAS_CPU_FREQ, "not supported") - def test_cpu_freq(self): - self.execute(psutil.cpu_freq) - - @unittest.skipIf(not HAS_GETLOADAVG, "not supported") - def test_getloadavg(self): - self.execute(psutil.getloadavg) - - # --- mem - - def test_virtual_memory(self): - self.execute(psutil.virtual_memory) - - # TODO: remove this skip when this gets fixed - @unittest.skipIf(SUNOS, - "worthless on SUNOS (uses a subprocess)") - def test_swap_memory(self): - self.execute(psutil.swap_memory) - - @unittest.skipIf(POSIX and SKIP_PYTHON_IMPL, - "worthless on POSIX (pure python)") - def test_pid_exists(self): - self.execute(psutil.pid_exists, os.getpid()) - - # --- disk - - @unittest.skipIf(POSIX and SKIP_PYTHON_IMPL, - "worthless on POSIX (pure python)") - def test_disk_usage(self): - self.execute(psutil.disk_usage, '.') - - def test_disk_partitions(self): - self.execute(psutil.disk_partitions) - - @unittest.skipIf(LINUX and not os.path.exists('/proc/diskstats'), - '/proc/diskstats not available on this Linux version') - @skip_if_linux() - def test_disk_io_counters(self): - self.execute(psutil.disk_io_counters, nowrap=False) - - # --- proc - - @skip_if_linux() - def test_pids(self): - self.execute(psutil.pids) - - # --- net - - @unittest.skipIf(TRAVIS and MACOS, "false positive on travis") - @skip_if_linux() - @unittest.skipIf(not HAS_NET_IO_COUNTERS, 'not supported') - def test_net_io_counters(self): - self.execute(psutil.net_io_counters, nowrap=False) - - @unittest.skipIf(LINUX, - "worthless on Linux (pure python)") - @unittest.skipIf(MACOS and os.getuid() != 0, "need root access") - def test_net_connections(self): - with create_sockets(): - self.execute(psutil.net_connections) - - def test_net_if_addrs(self): - # Note: verified that on Windows this was a false positive. - self.execute(psutil.net_if_addrs, - tolerance_=80 * 1024 if WINDOWS else None) - - @unittest.skipIf(TRAVIS, "EPERM on travis") - def test_net_if_stats(self): - self.execute(psutil.net_if_stats) - - # --- sensors - - @skip_if_linux() - @unittest.skipIf(not HAS_SENSORS_BATTERY, "not supported") - def test_sensors_battery(self): - self.execute(psutil.sensors_battery) - - @skip_if_linux() - @unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported") - def test_sensors_temperatures(self): - self.execute(psutil.sensors_temperatures) - - @skip_if_linux() - @unittest.skipIf(not HAS_SENSORS_FANS, "not supported") - def test_sensors_fans(self): - self.execute(psutil.sensors_fans) - - # --- others - - @skip_if_linux() - def test_boot_time(self): - self.execute(psutil.boot_time) - - # XXX - on Windows this produces a false positive - @unittest.skipIf(WINDOWS, "XXX produces a false positive on Windows") - def test_users(self): - self.execute(psutil.users) - - if WINDOWS: - - # --- win services - - def test_win_service_iter(self): - self.execute(cext.winservice_enumerate) - - def test_win_service_get(self): - pass - - def test_win_service_get_config(self): - name = next(psutil.win_service_iter()).name() - self.execute(cext.winservice_query_config, name) - - def test_win_service_get_status(self): - name = next(psutil.win_service_iter()).name() - self.execute(cext.winservice_query_status, name) - - def test_win_service_get_description(self): - name = next(psutil.win_service_iter()).name() - self.execute(cext.winservice_query_descr, name) - - -if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Tests for detecting function memory leaks (typically the ones +implemented in C). It does so by calling a function many times and +checking whether process memory usage keeps increasing between +calls or over time. +Note that this may produce false positives (especially on Windows +for some reason). +PyPy appears to be completely unstable for this framework, probably +because of how its JIT handles memory, so tests are skipped. +""" + +from __future__ import print_function + +import functools +import os +import platform +import unittest + +import psutil +import psutil._common +from psutil import LINUX +from psutil import MACOS +from psutil import OPENBSD +from psutil import POSIX +from psutil import SUNOS +from psutil import WINDOWS +from psutil._compat import ProcessLookupError +from psutil._compat import super +from psutil.tests import HAS_CPU_AFFINITY +from psutil.tests import HAS_CPU_FREQ +from psutil.tests import HAS_ENVIRON +from psutil.tests import HAS_IONICE +from psutil.tests import HAS_MEMORY_MAPS +from psutil.tests import HAS_NET_IO_COUNTERS +from psutil.tests import HAS_PROC_CPU_NUM +from psutil.tests import HAS_PROC_IO_COUNTERS +from psutil.tests import HAS_RLIMIT +from psutil.tests import HAS_SENSORS_BATTERY +from psutil.tests import HAS_SENSORS_FANS +from psutil.tests import HAS_SENSORS_TEMPERATURES +from psutil.tests import TestMemoryLeak +from psutil.tests import create_sockets +from psutil.tests import get_testfn +from psutil.tests import process_namespace +from psutil.tests import skip_on_access_denied +from psutil.tests import spawn_testproc +from psutil.tests import system_namespace +from psutil.tests import terminate + + +cext = psutil._psplatform.cext +thisproc = psutil.Process() +FEW_TIMES = 5 + + +def fewtimes_if_linux(): + """Decorator for those Linux functions which are implemented in pure + Python, and which we want to run faster. + """ + + def decorator(fun): + @functools.wraps(fun) + def wrapper(self, *args, **kwargs): + if LINUX: + before = self.__class__.times + try: + self.__class__.times = FEW_TIMES + return fun(self, *args, **kwargs) + finally: + self.__class__.times = before + else: + return fun(self, *args, **kwargs) + + return wrapper + + return decorator + + +# =================================================================== +# Process class +# =================================================================== + + +class TestProcessObjectLeaks(TestMemoryLeak): + """Test leaks of Process class methods.""" + + proc = thisproc + + def test_coverage(self): + ns = process_namespace(None) + ns.test_class_coverage(self, ns.getters + ns.setters) + + @fewtimes_if_linux() + def test_name(self): + self.execute(self.proc.name) + + @fewtimes_if_linux() + def test_cmdline(self): + self.execute(self.proc.cmdline) + + @fewtimes_if_linux() + def test_exe(self): + self.execute(self.proc.exe) + + @fewtimes_if_linux() + def test_ppid(self): + self.execute(self.proc.ppid) + + @unittest.skipIf(not POSIX, "POSIX only") + @fewtimes_if_linux() + def test_uids(self): + self.execute(self.proc.uids) + + @unittest.skipIf(not POSIX, "POSIX only") + @fewtimes_if_linux() + def test_gids(self): + self.execute(self.proc.gids) + + @fewtimes_if_linux() + def test_status(self): + self.execute(self.proc.status) + + def test_nice(self): + self.execute(self.proc.nice) + + def test_nice_set(self): + niceness = thisproc.nice() + self.execute(lambda: self.proc.nice(niceness)) + + @unittest.skipIf(not HAS_IONICE, "not supported") + def test_ionice(self): + self.execute(self.proc.ionice) + + @unittest.skipIf(not HAS_IONICE, "not supported") + def test_ionice_set(self): + if WINDOWS: + value = thisproc.ionice() + self.execute(lambda: self.proc.ionice(value)) + else: + self.execute(lambda: self.proc.ionice(psutil.IOPRIO_CLASS_NONE)) + fun = functools.partial(cext.proc_ioprio_set, os.getpid(), -1, 0) + self.execute_w_exc(OSError, fun) + + @unittest.skipIf(not HAS_PROC_IO_COUNTERS, "not supported") + @fewtimes_if_linux() + def test_io_counters(self): + self.execute(self.proc.io_counters) + + @unittest.skipIf(POSIX, "worthless on POSIX") + def test_username(self): + # always open 1 handle on Windows (only once) + psutil.Process().username() + self.execute(self.proc.username) + + @fewtimes_if_linux() + def test_create_time(self): + self.execute(self.proc.create_time) + + @fewtimes_if_linux() + @skip_on_access_denied(only_if=OPENBSD) + def test_num_threads(self): + self.execute(self.proc.num_threads) + + @unittest.skipIf(not WINDOWS, "WINDOWS only") + def test_num_handles(self): + self.execute(self.proc.num_handles) + + @unittest.skipIf(not POSIX, "POSIX only") + @fewtimes_if_linux() + def test_num_fds(self): + self.execute(self.proc.num_fds) + + @fewtimes_if_linux() + def test_num_ctx_switches(self): + self.execute(self.proc.num_ctx_switches) + + @fewtimes_if_linux() + @skip_on_access_denied(only_if=OPENBSD) + def test_threads(self): + self.execute(self.proc.threads) + + @fewtimes_if_linux() + def test_cpu_times(self): + self.execute(self.proc.cpu_times) + + @fewtimes_if_linux() + @unittest.skipIf(not HAS_PROC_CPU_NUM, "not supported") + def test_cpu_num(self): + self.execute(self.proc.cpu_num) + + @fewtimes_if_linux() + def test_memory_info(self): + self.execute(self.proc.memory_info) + + @fewtimes_if_linux() + def test_memory_full_info(self): + self.execute(self.proc.memory_full_info) + + @unittest.skipIf(not POSIX, "POSIX only") + @fewtimes_if_linux() + def test_terminal(self): + self.execute(self.proc.terminal) + + def test_resume(self): + times = FEW_TIMES if POSIX else self.times + self.execute(self.proc.resume, times=times) + + @fewtimes_if_linux() + def test_cwd(self): + self.execute(self.proc.cwd) + + @unittest.skipIf(not HAS_CPU_AFFINITY, "not supported") + def test_cpu_affinity(self): + self.execute(self.proc.cpu_affinity) + + @unittest.skipIf(not HAS_CPU_AFFINITY, "not supported") + def test_cpu_affinity_set(self): + affinity = thisproc.cpu_affinity() + self.execute(lambda: self.proc.cpu_affinity(affinity)) + self.execute_w_exc(ValueError, lambda: self.proc.cpu_affinity([-1])) + + @fewtimes_if_linux() + def test_open_files(self): + with open(get_testfn(), 'w'): + self.execute(self.proc.open_files) + + @unittest.skipIf(not HAS_MEMORY_MAPS, "not supported") + @fewtimes_if_linux() + def test_memory_maps(self): + self.execute(self.proc.memory_maps) + + @unittest.skipIf(not LINUX, "LINUX only") + @unittest.skipIf(not HAS_RLIMIT, "not supported") + def test_rlimit(self): + self.execute(lambda: self.proc.rlimit(psutil.RLIMIT_NOFILE)) + + @unittest.skipIf(not LINUX, "LINUX only") + @unittest.skipIf(not HAS_RLIMIT, "not supported") + def test_rlimit_set(self): + limit = thisproc.rlimit(psutil.RLIMIT_NOFILE) + self.execute(lambda: self.proc.rlimit(psutil.RLIMIT_NOFILE, limit)) + self.execute_w_exc((OSError, ValueError), lambda: self.proc.rlimit(-1)) + + @fewtimes_if_linux() + # Windows implementation is based on a single system-wide + # function (tested later). + @unittest.skipIf(WINDOWS, "worthless on WINDOWS") + def test_connections(self): + # TODO: UNIX sockets are temporarily implemented by parsing + # 'pfiles' cmd output; we don't want that part of the code to + # be executed. + with create_sockets(): + kind = 'inet' if SUNOS else 'all' + self.execute(lambda: self.proc.connections(kind)) + + @unittest.skipIf(not HAS_ENVIRON, "not supported") + def test_environ(self): + self.execute(self.proc.environ) + + @unittest.skipIf(not WINDOWS, "WINDOWS only") + def test_proc_info(self): + self.execute(lambda: cext.proc_info(os.getpid())) + + +class TestTerminatedProcessLeaks(TestProcessObjectLeaks): + """Repeat the tests above looking for leaks occurring when dealing + with terminated processes raising NoSuchProcess exception. + The C functions are still invoked but will follow different code + paths. We'll check those code paths. + """ + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.subp = spawn_testproc() + cls.proc = psutil.Process(cls.subp.pid) + cls.proc.kill() + cls.proc.wait() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + terminate(cls.subp) + + def call(self, fun): + try: + fun() + except psutil.NoSuchProcess: + pass + + if WINDOWS: + + def test_kill(self): + self.execute(self.proc.kill) + + def test_terminate(self): + self.execute(self.proc.terminate) + + def test_suspend(self): + self.execute(self.proc.suspend) + + def test_resume(self): + self.execute(self.proc.resume) + + def test_wait(self): + self.execute(self.proc.wait) + + def test_proc_info(self): + # test dual implementation + def call(): + try: + return cext.proc_info(self.proc.pid) + except ProcessLookupError: + pass + + self.execute(call) + + +@unittest.skipIf(not WINDOWS, "WINDOWS only") +class TestProcessDualImplementation(TestMemoryLeak): + def test_cmdline_peb_true(self): + self.execute(lambda: cext.proc_cmdline(os.getpid(), use_peb=True)) + + def test_cmdline_peb_false(self): + self.execute(lambda: cext.proc_cmdline(os.getpid(), use_peb=False)) + + +# =================================================================== +# system APIs +# =================================================================== + + +class TestModuleFunctionsLeaks(TestMemoryLeak): + """Test leaks of psutil module functions.""" + + def test_coverage(self): + ns = system_namespace() + ns.test_class_coverage(self, ns.all) + + # --- cpu + + @fewtimes_if_linux() + def test_cpu_count(self): # logical + self.execute(lambda: psutil.cpu_count(logical=True)) + + @fewtimes_if_linux() + def test_cpu_count_cores(self): + self.execute(lambda: psutil.cpu_count(logical=False)) + + @fewtimes_if_linux() + def test_cpu_times(self): + self.execute(psutil.cpu_times) + + @fewtimes_if_linux() + def test_per_cpu_times(self): + self.execute(lambda: psutil.cpu_times(percpu=True)) + + @fewtimes_if_linux() + def test_cpu_stats(self): + self.execute(psutil.cpu_stats) + + @fewtimes_if_linux() + # TODO: remove this once 1892 is fixed + @unittest.skipIf( + MACOS and platform.machine() == 'arm64', "skipped due to #1892" + ) + @unittest.skipIf(not HAS_CPU_FREQ, "not supported") + def test_cpu_freq(self): + self.execute(psutil.cpu_freq) + + @unittest.skipIf(not WINDOWS, "WINDOWS only") + def test_getloadavg(self): + psutil.getloadavg() + self.execute(psutil.getloadavg) + + # --- mem + + def test_virtual_memory(self): + self.execute(psutil.virtual_memory) + + # TODO: remove this skip when this gets fixed + @unittest.skipIf(SUNOS, "worthless on SUNOS (uses a subprocess)") + def test_swap_memory(self): + self.execute(psutil.swap_memory) + + def test_pid_exists(self): + times = FEW_TIMES if POSIX else self.times + self.execute(lambda: psutil.pid_exists(os.getpid()), times=times) + + # --- disk + + def test_disk_usage(self): + times = FEW_TIMES if POSIX else self.times + self.execute(lambda: psutil.disk_usage('.'), times=times) + + def test_disk_partitions(self): + self.execute(psutil.disk_partitions) + + @unittest.skipIf( + LINUX and not os.path.exists('/proc/diskstats'), + '/proc/diskstats not available on this Linux version', + ) + @fewtimes_if_linux() + def test_disk_io_counters(self): + self.execute(lambda: psutil.disk_io_counters(nowrap=False)) + + # --- proc + + @fewtimes_if_linux() + def test_pids(self): + self.execute(psutil.pids) + + # --- net + + @fewtimes_if_linux() + @unittest.skipIf(not HAS_NET_IO_COUNTERS, 'not supported') + def test_net_io_counters(self): + self.execute(lambda: psutil.net_io_counters(nowrap=False)) + + @fewtimes_if_linux() + @unittest.skipIf(MACOS and os.getuid() != 0, "need root access") + def test_net_connections(self): + # always opens and handle on Windows() (once) + psutil.net_connections(kind='all') + with create_sockets(): + self.execute(lambda: psutil.net_connections(kind='all')) + + def test_net_if_addrs(self): + # Note: verified that on Windows this was a false positive. + tolerance = 80 * 1024 if WINDOWS else self.tolerance + self.execute(psutil.net_if_addrs, tolerance=tolerance) + + def test_net_if_stats(self): + self.execute(psutil.net_if_stats) + + # --- sensors + + @fewtimes_if_linux() + @unittest.skipIf(not HAS_SENSORS_BATTERY, "not supported") + def test_sensors_battery(self): + self.execute(psutil.sensors_battery) + + @fewtimes_if_linux() + @unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported") + def test_sensors_temperatures(self): + self.execute(psutil.sensors_temperatures) + + @fewtimes_if_linux() + @unittest.skipIf(not HAS_SENSORS_FANS, "not supported") + def test_sensors_fans(self): + self.execute(psutil.sensors_fans) + + # --- others + + @fewtimes_if_linux() + def test_boot_time(self): + self.execute(psutil.boot_time) + + def test_users(self): + self.execute(psutil.users) + + def test_set_debug(self): + self.execute(lambda: psutil._set_debug(False)) + + if WINDOWS: + + # --- win services + + def test_win_service_iter(self): + self.execute(cext.winservice_enumerate) + + def test_win_service_get(self): + pass + + def test_win_service_get_config(self): + name = next(psutil.win_service_iter()).name() + self.execute(lambda: cext.winservice_query_config(name)) + + def test_win_service_get_status(self): + name = next(psutil.win_service_iter()).name() + self.execute(lambda: cext.winservice_query_status(name)) + + def test_win_service_get_description(self): + name = next(psutil.win_service_iter()).name() + self.execute(lambda: cext.winservice_query_descr(name)) + + +if __name__ == '__main__': + from psutil.tests.runner import run_from_name + + run_from_name(__file__) diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_memory_leaks.pyc b/addon/globalPlugins/soundmanager/psutil/tests/test_memory_leaks.pyc deleted file mode 100644 index a1929e8..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/tests/test_memory_leaks.pyc and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_misc.py b/addon/globalPlugins/soundmanager/psutil/tests/test_misc.py index 29e1e41..367b66a 100644 --- a/addon/globalPlugins/soundmanager/psutil/tests/test_misc.py +++ b/addon/globalPlugins/soundmanager/psutil/tests/test_misc.py @@ -1,1057 +1,1014 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -""" -Miscellaneous tests. -""" - -import ast -import collections -import contextlib -import errno -import json -import os -import pickle -import socket -import stat - -from psutil import LINUX -from psutil import POSIX -from psutil import WINDOWS -from psutil._common import memoize -from psutil._common import memoize_when_activated -from psutil._common import supports_ipv6 -from psutil._common import wrap_numbers -from psutil._common import open_text -from psutil._common import open_binary -from psutil._compat import PY3 -from psutil.tests import APPVEYOR -from psutil.tests import bind_socket -from psutil.tests import bind_unix_socket -from psutil.tests import call_until -from psutil.tests import chdir -from psutil.tests import create_proc_children_pair -from psutil.tests import create_sockets -from psutil.tests import create_zombie_proc -from psutil.tests import DEVNULL -from psutil.tests import get_free_port -from psutil.tests import get_test_subprocess -from psutil.tests import HAS_BATTERY -from psutil.tests import HAS_CONNECTIONS_UNIX -from psutil.tests import HAS_MEMORY_MAPS -from psutil.tests import HAS_NET_IO_COUNTERS -from psutil.tests import HAS_SENSORS_BATTERY -from psutil.tests import HAS_SENSORS_FANS -from psutil.tests import HAS_SENSORS_TEMPERATURES -from psutil.tests import import_module_by_path -from psutil.tests import is_namedtuple -from psutil.tests import mock -from psutil.tests import PYTHON_EXE -from psutil.tests import reap_children -from psutil.tests import reload_module -from psutil.tests import retry -from psutil.tests import ROOT_DIR -from psutil.tests import safe_mkdir -from psutil.tests import safe_rmpath -from psutil.tests import SCRIPTS_DIR -from psutil.tests import sh -from psutil.tests import tcp_socketpair -from psutil.tests import TESTFN -from psutil.tests import TOX -from psutil.tests import TRAVIS -from psutil.tests import unittest -from psutil.tests import unix_socket_path -from psutil.tests import unix_socketpair -from psutil.tests import wait_for_file -from psutil.tests import wait_for_pid -import psutil -import psutil.tests - - -# =================================================================== -# --- Misc / generic tests. -# =================================================================== - - -class TestMisc(unittest.TestCase): - - def test_process__repr__(self, func=repr): - p = psutil.Process() - r = func(p) - self.assertIn("psutil.Process", r) - self.assertIn("pid=%s" % p.pid, r) - self.assertIn("name=", r) - self.assertIn(p.name(), r) - with mock.patch.object(psutil.Process, "name", - side_effect=psutil.ZombieProcess(os.getpid())): - p = psutil.Process() - r = func(p) - self.assertIn("pid=%s" % p.pid, r) - self.assertIn("zombie", r) - self.assertNotIn("name=", r) - with mock.patch.object(psutil.Process, "name", - side_effect=psutil.NoSuchProcess(os.getpid())): - p = psutil.Process() - r = func(p) - self.assertIn("pid=%s" % p.pid, r) - self.assertIn("terminated", r) - self.assertNotIn("name=", r) - with mock.patch.object(psutil.Process, "name", - side_effect=psutil.AccessDenied(os.getpid())): - p = psutil.Process() - r = func(p) - self.assertIn("pid=%s" % p.pid, r) - self.assertNotIn("name=", r) - - def test_process__str__(self): - self.test_process__repr__(func=str) - - def test_no_such_process__repr__(self, func=repr): - self.assertEqual( - repr(psutil.NoSuchProcess(321)), - "psutil.NoSuchProcess process no longer exists (pid=321)") - self.assertEqual( - repr(psutil.NoSuchProcess(321, name='foo')), - "psutil.NoSuchProcess process no longer exists (pid=321, " - "name='foo')") - self.assertEqual( - repr(psutil.NoSuchProcess(321, msg='foo')), - "psutil.NoSuchProcess foo") - - def test_zombie_process__repr__(self, func=repr): - self.assertEqual( - repr(psutil.ZombieProcess(321)), - "psutil.ZombieProcess process still exists but it's a zombie " - "(pid=321)") - self.assertEqual( - repr(psutil.ZombieProcess(321, name='foo')), - "psutil.ZombieProcess process still exists but it's a zombie " - "(pid=321, name='foo')") - self.assertEqual( - repr(psutil.ZombieProcess(321, name='foo', ppid=1)), - "psutil.ZombieProcess process still exists but it's a zombie " - "(pid=321, name='foo', ppid=1)") - self.assertEqual( - repr(psutil.ZombieProcess(321, msg='foo')), - "psutil.ZombieProcess foo") - - def test_access_denied__repr__(self, func=repr): - self.assertEqual( - repr(psutil.AccessDenied(321)), - "psutil.AccessDenied (pid=321)") - self.assertEqual( - repr(psutil.AccessDenied(321, name='foo')), - "psutil.AccessDenied (pid=321, name='foo')") - self.assertEqual( - repr(psutil.AccessDenied(321, msg='foo')), - "psutil.AccessDenied foo") - - def test_timeout_expired__repr__(self, func=repr): - self.assertEqual( - repr(psutil.TimeoutExpired(321)), - "psutil.TimeoutExpired timeout after 321 seconds") - self.assertEqual( - repr(psutil.TimeoutExpired(321, pid=111)), - "psutil.TimeoutExpired timeout after 321 seconds (pid=111)") - self.assertEqual( - repr(psutil.TimeoutExpired(321, pid=111, name='foo')), - "psutil.TimeoutExpired timeout after 321 seconds " - "(pid=111, name='foo')") - - def test_process__eq__(self): - p1 = psutil.Process() - p2 = psutil.Process() - self.assertEqual(p1, p2) - p2._ident = (0, 0) - self.assertNotEqual(p1, p2) - self.assertNotEqual(p1, 'foo') - - def test_process__hash__(self): - s = set([psutil.Process(), psutil.Process()]) - self.assertEqual(len(s), 1) - - def test__all__(self): - dir_psutil = dir(psutil) - for name in dir_psutil: - if name in ('callable', 'error', 'namedtuple', 'tests', - 'long', 'test', 'NUM_CPUS', 'BOOT_TIME', - 'TOTAL_PHYMEM'): - continue - if not name.startswith('_'): - try: - __import__(name) - except ImportError: - if name not in psutil.__all__: - fun = getattr(psutil, name) - if fun is None: - continue - if (fun.__doc__ is not None and - 'deprecated' not in fun.__doc__.lower()): - self.fail('%r not in psutil.__all__' % name) - - # Import 'star' will break if __all__ is inconsistent, see: - # https://github.com/giampaolo/psutil/issues/656 - # Can't do `from psutil import *` as it won't work on python 3 - # so we simply iterate over __all__. - for name in psutil.__all__: - self.assertIn(name, dir_psutil) - - def test_version(self): - self.assertEqual('.'.join([str(x) for x in psutil.version_info]), - psutil.__version__) - - def test_process_as_dict_no_new_names(self): - # See https://github.com/giampaolo/psutil/issues/813 - p = psutil.Process() - p.foo = '1' - self.assertNotIn('foo', p.as_dict()) - - def test_memoize(self): - @memoize - def foo(*args, **kwargs): - "foo docstring" - calls.append(None) - return (args, kwargs) - - calls = [] - # no args - for x in range(2): - ret = foo() - expected = ((), {}) - self.assertEqual(ret, expected) - self.assertEqual(len(calls), 1) - # with args - for x in range(2): - ret = foo(1) - expected = ((1, ), {}) - self.assertEqual(ret, expected) - self.assertEqual(len(calls), 2) - # with args + kwargs - for x in range(2): - ret = foo(1, bar=2) - expected = ((1, ), {'bar': 2}) - self.assertEqual(ret, expected) - self.assertEqual(len(calls), 3) - # clear cache - foo.cache_clear() - ret = foo() - expected = ((), {}) - self.assertEqual(ret, expected) - self.assertEqual(len(calls), 4) - # docstring - self.assertEqual(foo.__doc__, "foo docstring") - - def test_memoize_when_activated(self): - class Foo: - - @memoize_when_activated - def foo(self): - calls.append(None) - - f = Foo() - calls = [] - f.foo() - f.foo() - self.assertEqual(len(calls), 2) - - # activate - calls = [] - f.foo.cache_activate(f) - f.foo() - f.foo() - self.assertEqual(len(calls), 1) - - # deactivate - calls = [] - f.foo.cache_deactivate(f) - f.foo() - f.foo() - self.assertEqual(len(calls), 2) - - def test_parse_environ_block(self): - from psutil._common import parse_environ_block - - def k(s): - return s.upper() if WINDOWS else s - - self.assertEqual(parse_environ_block("a=1\0"), - {k("a"): "1"}) - self.assertEqual(parse_environ_block("a=1\0b=2\0\0"), - {k("a"): "1", k("b"): "2"}) - self.assertEqual(parse_environ_block("a=1\0b=\0\0"), - {k("a"): "1", k("b"): ""}) - # ignore everything after \0\0 - self.assertEqual(parse_environ_block("a=1\0b=2\0\0c=3\0"), - {k("a"): "1", k("b"): "2"}) - # ignore everything that is not an assignment - self.assertEqual(parse_environ_block("xxx\0a=1\0"), {k("a"): "1"}) - self.assertEqual(parse_environ_block("a=1\0=b=2\0"), {k("a"): "1"}) - # do not fail if the block is incomplete - self.assertEqual(parse_environ_block("a=1\0b=2"), {k("a"): "1"}) - - def test_supports_ipv6(self): - self.addCleanup(supports_ipv6.cache_clear) - if supports_ipv6(): - with mock.patch('psutil._common.socket') as s: - s.has_ipv6 = False - supports_ipv6.cache_clear() - assert not supports_ipv6() - - supports_ipv6.cache_clear() - with mock.patch('psutil._common.socket.socket', - side_effect=socket.error) as s: - assert not supports_ipv6() - assert s.called - - supports_ipv6.cache_clear() - with mock.patch('psutil._common.socket.socket', - side_effect=socket.gaierror) as s: - assert not supports_ipv6() - supports_ipv6.cache_clear() - assert s.called - - supports_ipv6.cache_clear() - with mock.patch('psutil._common.socket.socket.bind', - side_effect=socket.gaierror) as s: - assert not supports_ipv6() - supports_ipv6.cache_clear() - assert s.called - else: - with self.assertRaises(Exception): - sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) - sock.bind(("::1", 0)) - - def test_isfile_strict(self): - from psutil._common import isfile_strict - this_file = os.path.abspath(__file__) - assert isfile_strict(this_file) - assert not isfile_strict(os.path.dirname(this_file)) - with mock.patch('psutil._common.os.stat', - side_effect=OSError(errno.EPERM, "foo")): - self.assertRaises(OSError, isfile_strict, this_file) - with mock.patch('psutil._common.os.stat', - side_effect=OSError(errno.EACCES, "foo")): - self.assertRaises(OSError, isfile_strict, this_file) - with mock.patch('psutil._common.os.stat', - side_effect=OSError(errno.EINVAL, "foo")): - assert not isfile_strict(this_file) - with mock.patch('psutil._common.stat.S_ISREG', return_value=False): - assert not isfile_strict(this_file) - - def test_serialization(self): - def check(ret): - if json is not None: - json.loads(json.dumps(ret)) - a = pickle.dumps(ret) - b = pickle.loads(a) - self.assertEqual(ret, b) - - check(psutil.Process().as_dict()) - check(psutil.virtual_memory()) - check(psutil.swap_memory()) - check(psutil.cpu_times()) - check(psutil.cpu_times_percent(interval=0)) - check(psutil.net_io_counters()) - if LINUX and not os.path.exists('/proc/diskstats'): - pass - else: - if not APPVEYOR: - check(psutil.disk_io_counters()) - check(psutil.disk_partitions()) - check(psutil.disk_usage(os.getcwd())) - check(psutil.users()) - - def test_setup_script(self): - setup_py = os.path.join(ROOT_DIR, 'setup.py') - if TRAVIS and not os.path.exists(setup_py): - return self.skipTest("can't find setup.py") - module = import_module_by_path(setup_py) - self.assertRaises(SystemExit, module.setup) - self.assertEqual(module.get_version(), psutil.__version__) - - def test_ad_on_process_creation(self): - # We are supposed to be able to instantiate Process also in case - # of zombie processes or access denied. - with mock.patch.object(psutil.Process, 'create_time', - side_effect=psutil.AccessDenied) as meth: - psutil.Process() - assert meth.called - with mock.patch.object(psutil.Process, 'create_time', - side_effect=psutil.ZombieProcess(1)) as meth: - psutil.Process() - assert meth.called - with mock.patch.object(psutil.Process, 'create_time', - side_effect=ValueError) as meth: - with self.assertRaises(ValueError): - psutil.Process() - assert meth.called - - def test_sanity_version_check(self): - # see: https://github.com/giampaolo/psutil/issues/564 - with mock.patch( - "psutil._psplatform.cext.version", return_value="0.0.0"): - with self.assertRaises(ImportError) as cm: - reload_module(psutil) - self.assertIn("version conflict", str(cm.exception).lower()) - - -# =================================================================== -# --- Tests for wrap_numbers() function. -# =================================================================== - - -nt = collections.namedtuple('foo', 'a b c') - - -class TestWrapNumbers(unittest.TestCase): - - def setUp(self): - wrap_numbers.cache_clear() - - tearDown = setUp - - def test_first_call(self): - input = {'disk1': nt(5, 5, 5)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - - def test_input_hasnt_changed(self): - input = {'disk1': nt(5, 5, 5)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - - def test_increase_but_no_wrap(self): - input = {'disk1': nt(5, 5, 5)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - input = {'disk1': nt(10, 15, 20)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - input = {'disk1': nt(20, 25, 30)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - input = {'disk1': nt(20, 25, 30)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - - def test_wrap(self): - # let's say 100 is the threshold - input = {'disk1': nt(100, 100, 100)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - # first wrap restarts from 10 - input = {'disk1': nt(100, 100, 10)} - self.assertEqual(wrap_numbers(input, 'disk_io'), - {'disk1': nt(100, 100, 110)}) - # then it remains the same - input = {'disk1': nt(100, 100, 10)} - self.assertEqual(wrap_numbers(input, 'disk_io'), - {'disk1': nt(100, 100, 110)}) - # then it goes up - input = {'disk1': nt(100, 100, 90)} - self.assertEqual(wrap_numbers(input, 'disk_io'), - {'disk1': nt(100, 100, 190)}) - # then it wraps again - input = {'disk1': nt(100, 100, 20)} - self.assertEqual(wrap_numbers(input, 'disk_io'), - {'disk1': nt(100, 100, 210)}) - # and remains the same - input = {'disk1': nt(100, 100, 20)} - self.assertEqual(wrap_numbers(input, 'disk_io'), - {'disk1': nt(100, 100, 210)}) - # now wrap another num - input = {'disk1': nt(50, 100, 20)} - self.assertEqual(wrap_numbers(input, 'disk_io'), - {'disk1': nt(150, 100, 210)}) - # and again - input = {'disk1': nt(40, 100, 20)} - self.assertEqual(wrap_numbers(input, 'disk_io'), - {'disk1': nt(190, 100, 210)}) - # keep it the same - input = {'disk1': nt(40, 100, 20)} - self.assertEqual(wrap_numbers(input, 'disk_io'), - {'disk1': nt(190, 100, 210)}) - - def test_changing_keys(self): - # Emulate a case where the second call to disk_io() - # (or whatever) provides a new disk, then the new disk - # disappears on the third call. - input = {'disk1': nt(5, 5, 5)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - input = {'disk1': nt(5, 5, 5), - 'disk2': nt(7, 7, 7)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - input = {'disk1': nt(8, 8, 8)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - - def test_changing_keys_w_wrap(self): - input = {'disk1': nt(50, 50, 50), - 'disk2': nt(100, 100, 100)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - # disk 2 wraps - input = {'disk1': nt(50, 50, 50), - 'disk2': nt(100, 100, 10)} - self.assertEqual(wrap_numbers(input, 'disk_io'), - {'disk1': nt(50, 50, 50), - 'disk2': nt(100, 100, 110)}) - # disk 2 disappears - input = {'disk1': nt(50, 50, 50)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - - # then it appears again; the old wrap is supposed to be - # gone. - input = {'disk1': nt(50, 50, 50), - 'disk2': nt(100, 100, 100)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - # remains the same - input = {'disk1': nt(50, 50, 50), - 'disk2': nt(100, 100, 100)} - self.assertEqual(wrap_numbers(input, 'disk_io'), input) - # and then wraps again - input = {'disk1': nt(50, 50, 50), - 'disk2': nt(100, 100, 10)} - self.assertEqual(wrap_numbers(input, 'disk_io'), - {'disk1': nt(50, 50, 50), - 'disk2': nt(100, 100, 110)}) - - def test_real_data(self): - d = {'nvme0n1': (300, 508, 640, 1571, 5970, 1987, 2049, 451751, 47048), - 'nvme0n1p1': (1171, 2, 5600256, 1024, 516, 0, 0, 0, 8), - 'nvme0n1p2': (54, 54, 2396160, 5165056, 4, 24, 30, 1207, 28), - 'nvme0n1p3': (2389, 4539, 5154, 150, 4828, 1844, 2019, 398, 348)} - self.assertEqual(wrap_numbers(d, 'disk_io'), d) - self.assertEqual(wrap_numbers(d, 'disk_io'), d) - # decrease this ↓ - d = {'nvme0n1': (100, 508, 640, 1571, 5970, 1987, 2049, 451751, 47048), - 'nvme0n1p1': (1171, 2, 5600256, 1024, 516, 0, 0, 0, 8), - 'nvme0n1p2': (54, 54, 2396160, 5165056, 4, 24, 30, 1207, 28), - 'nvme0n1p3': (2389, 4539, 5154, 150, 4828, 1844, 2019, 398, 348)} - out = wrap_numbers(d, 'disk_io') - self.assertEqual(out['nvme0n1'][0], 400) - - # --- cache tests - - def test_cache_first_call(self): - input = {'disk1': nt(5, 5, 5)} - wrap_numbers(input, 'disk_io') - cache = wrap_numbers.cache_info() - self.assertEqual(cache[0], {'disk_io': input}) - self.assertEqual(cache[1], {'disk_io': {}}) - self.assertEqual(cache[2], {'disk_io': {}}) - - def test_cache_call_twice(self): - input = {'disk1': nt(5, 5, 5)} - wrap_numbers(input, 'disk_io') - input = {'disk1': nt(10, 10, 10)} - wrap_numbers(input, 'disk_io') - cache = wrap_numbers.cache_info() - self.assertEqual(cache[0], {'disk_io': input}) - self.assertEqual( - cache[1], - {'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 0}}) - self.assertEqual(cache[2], {'disk_io': {}}) - - def test_cache_wrap(self): - # let's say 100 is the threshold - input = {'disk1': nt(100, 100, 100)} - wrap_numbers(input, 'disk_io') - - # first wrap restarts from 10 - input = {'disk1': nt(100, 100, 10)} - wrap_numbers(input, 'disk_io') - cache = wrap_numbers.cache_info() - self.assertEqual(cache[0], {'disk_io': input}) - self.assertEqual( - cache[1], - {'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 100}}) - self.assertEqual(cache[2], {'disk_io': {'disk1': set([('disk1', 2)])}}) - - def assert_(): - cache = wrap_numbers.cache_info() - self.assertEqual( - cache[1], - {'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, - ('disk1', 2): 100}}) - self.assertEqual(cache[2], - {'disk_io': {'disk1': set([('disk1', 2)])}}) - - # then it remains the same - input = {'disk1': nt(100, 100, 10)} - wrap_numbers(input, 'disk_io') - cache = wrap_numbers.cache_info() - self.assertEqual(cache[0], {'disk_io': input}) - assert_() - - # then it goes up - input = {'disk1': nt(100, 100, 90)} - wrap_numbers(input, 'disk_io') - cache = wrap_numbers.cache_info() - self.assertEqual(cache[0], {'disk_io': input}) - assert_() - - # then it wraps again - input = {'disk1': nt(100, 100, 20)} - wrap_numbers(input, 'disk_io') - cache = wrap_numbers.cache_info() - self.assertEqual(cache[0], {'disk_io': input}) - self.assertEqual( - cache[1], - {'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 190}}) - self.assertEqual(cache[2], {'disk_io': {'disk1': set([('disk1', 2)])}}) - - def test_cache_changing_keys(self): - input = {'disk1': nt(5, 5, 5)} - wrap_numbers(input, 'disk_io') - input = {'disk1': nt(5, 5, 5), - 'disk2': nt(7, 7, 7)} - wrap_numbers(input, 'disk_io') - cache = wrap_numbers.cache_info() - self.assertEqual(cache[0], {'disk_io': input}) - self.assertEqual( - cache[1], - {'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 0}}) - self.assertEqual(cache[2], {'disk_io': {}}) - - def test_cache_clear(self): - input = {'disk1': nt(5, 5, 5)} - wrap_numbers(input, 'disk_io') - wrap_numbers(input, 'disk_io') - wrap_numbers.cache_clear('disk_io') - self.assertEqual(wrap_numbers.cache_info(), ({}, {}, {})) - wrap_numbers.cache_clear('disk_io') - wrap_numbers.cache_clear('?!?') - - @unittest.skipIf(not HAS_NET_IO_COUNTERS, 'not supported') - def test_cache_clear_public_apis(self): - if not psutil.disk_io_counters() or not psutil.net_io_counters(): - return self.skipTest("no disks or NICs available") - psutil.disk_io_counters() - psutil.net_io_counters() - caches = wrap_numbers.cache_info() - for cache in caches: - self.assertIn('psutil.disk_io_counters', cache) - self.assertIn('psutil.net_io_counters', cache) - - psutil.disk_io_counters.cache_clear() - caches = wrap_numbers.cache_info() - for cache in caches: - self.assertIn('psutil.net_io_counters', cache) - self.assertNotIn('psutil.disk_io_counters', cache) - - psutil.net_io_counters.cache_clear() - caches = wrap_numbers.cache_info() - self.assertEqual(caches, ({}, {}, {})) - - -# =================================================================== -# --- Example script tests -# =================================================================== - - -@unittest.skipIf(TOX, "can't test on TOX") -# See: https://travis-ci.org/giampaolo/psutil/jobs/295224806 -@unittest.skipIf(TRAVIS and not os.path.exists(SCRIPTS_DIR), - "can't locate scripts directory") -class TestScripts(unittest.TestCase): - """Tests for scripts in the "scripts" directory.""" - - @staticmethod - def assert_stdout(exe, *args, **kwargs): - exe = '%s' % os.path.join(SCRIPTS_DIR, exe) - cmd = [PYTHON_EXE, exe] - for arg in args: - cmd.append(arg) - try: - out = sh(cmd, **kwargs).strip() - except RuntimeError as err: - if 'AccessDenied' in str(err): - return str(err) - else: - raise - assert out, out - return out - - @staticmethod - def assert_syntax(exe, args=None): - exe = os.path.join(SCRIPTS_DIR, exe) - if PY3: - f = open(exe, 'rt', encoding='utf8') - else: - f = open(exe, 'rt') - with f: - src = f.read() - ast.parse(src) - - def test_coverage(self): - # make sure all example scripts have a test method defined - meths = dir(self) - for name in os.listdir(SCRIPTS_DIR): - if name.endswith('.py'): - if 'test_' + os.path.splitext(name)[0] not in meths: - # self.assert_stdout(name) - self.fail('no test defined for %r script' - % os.path.join(SCRIPTS_DIR, name)) - - @unittest.skipIf(not POSIX, "POSIX only") - def test_executable(self): - for name in os.listdir(SCRIPTS_DIR): - if name.endswith('.py'): - path = os.path.join(SCRIPTS_DIR, name) - if not stat.S_IXUSR & os.stat(path)[stat.ST_MODE]: - self.fail('%r is not executable' % path) - - def test_disk_usage(self): - self.assert_stdout('disk_usage.py') - - def test_free(self): - self.assert_stdout('free.py') - - def test_meminfo(self): - self.assert_stdout('meminfo.py') - - def test_procinfo(self): - self.assert_stdout('procinfo.py', str(os.getpid())) - - # can't find users on APPVEYOR or TRAVIS - @unittest.skipIf(APPVEYOR or TRAVIS and not psutil.users(), - "unreliable on APPVEYOR or TRAVIS") - def test_who(self): - self.assert_stdout('who.py') - - def test_ps(self): - self.assert_stdout('ps.py') - - def test_pstree(self): - self.assert_stdout('pstree.py') - - def test_netstat(self): - self.assert_stdout('netstat.py') - - # permission denied on travis - @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") - def test_ifconfig(self): - self.assert_stdout('ifconfig.py') - - @unittest.skipIf(not HAS_MEMORY_MAPS, "not supported") - def test_pmap(self): - self.assert_stdout('pmap.py', str(os.getpid())) - - def test_procsmem(self): - if 'uss' not in psutil.Process().memory_full_info()._fields: - raise self.skipTest("not supported") - self.assert_stdout('procsmem.py', stderr=DEVNULL) - - def test_killall(self): - self.assert_syntax('killall.py') - - def test_nettop(self): - self.assert_syntax('nettop.py') - - def test_top(self): - self.assert_syntax('top.py') - - def test_iotop(self): - self.assert_syntax('iotop.py') - - def test_pidof(self): - output = self.assert_stdout('pidof.py', psutil.Process().name()) - self.assertIn(str(os.getpid()), output) - - @unittest.skipIf(not WINDOWS, "WINDOWS only") - def test_winservices(self): - self.assert_stdout('winservices.py') - - def test_cpu_distribution(self): - self.assert_syntax('cpu_distribution.py') - - @unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported") - @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") - def test_temperatures(self): - self.assert_stdout('temperatures.py') - - @unittest.skipIf(not HAS_SENSORS_FANS, "not supported") - @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") - def test_fans(self): - self.assert_stdout('fans.py') - - @unittest.skipIf(not HAS_SENSORS_BATTERY, "not supported") - @unittest.skipIf(not HAS_BATTERY, "no battery") - def test_battery(self): - self.assert_stdout('battery.py') - - def test_sensors(self): - self.assert_stdout('sensors.py') - - -# =================================================================== -# --- Unit tests for test utilities. -# =================================================================== - - -class TestRetryDecorator(unittest.TestCase): - - @mock.patch('time.sleep') - def test_retry_success(self, sleep): - # Fail 3 times out of 5; make sure the decorated fun returns. - - @retry(retries=5, interval=1, logfun=None) - def foo(): - while queue: - queue.pop() - 1 / 0 - return 1 - - queue = list(range(3)) - self.assertEqual(foo(), 1) - self.assertEqual(sleep.call_count, 3) - - @mock.patch('time.sleep') - def test_retry_failure(self, sleep): - # Fail 6 times out of 5; th function is supposed to raise exc. - - @retry(retries=5, interval=1, logfun=None) - def foo(): - while queue: - queue.pop() - 1 / 0 - return 1 - - queue = list(range(6)) - self.assertRaises(ZeroDivisionError, foo) - self.assertEqual(sleep.call_count, 5) - - @mock.patch('time.sleep') - def test_exception_arg(self, sleep): - @retry(exception=ValueError, interval=1) - def foo(): - raise TypeError - - self.assertRaises(TypeError, foo) - self.assertEqual(sleep.call_count, 0) - - @mock.patch('time.sleep') - def test_no_interval_arg(self, sleep): - # if interval is not specified sleep is not supposed to be called - - @retry(retries=5, interval=None, logfun=None) - def foo(): - 1 / 0 - - self.assertRaises(ZeroDivisionError, foo) - self.assertEqual(sleep.call_count, 0) - - @mock.patch('time.sleep') - def test_retries_arg(self, sleep): - - @retry(retries=5, interval=1, logfun=None) - def foo(): - 1 / 0 - - self.assertRaises(ZeroDivisionError, foo) - self.assertEqual(sleep.call_count, 5) - - @mock.patch('time.sleep') - def test_retries_and_timeout_args(self, sleep): - self.assertRaises(ValueError, retry, retries=5, timeout=1) - - -class TestSyncTestUtils(unittest.TestCase): - - def tearDown(self): - safe_rmpath(TESTFN) - - def test_wait_for_pid(self): - wait_for_pid(os.getpid()) - nopid = max(psutil.pids()) + 99999 - with mock.patch('psutil.tests.retry.__iter__', return_value=iter([0])): - self.assertRaises(psutil.NoSuchProcess, wait_for_pid, nopid) - - def test_wait_for_file(self): - with open(TESTFN, 'w') as f: - f.write('foo') - wait_for_file(TESTFN) - assert not os.path.exists(TESTFN) - - def test_wait_for_file_empty(self): - with open(TESTFN, 'w'): - pass - wait_for_file(TESTFN, empty=True) - assert not os.path.exists(TESTFN) - - def test_wait_for_file_no_file(self): - with mock.patch('psutil.tests.retry.__iter__', return_value=iter([0])): - self.assertRaises(IOError, wait_for_file, TESTFN) - - def test_wait_for_file_no_delete(self): - with open(TESTFN, 'w') as f: - f.write('foo') - wait_for_file(TESTFN, delete=False) - assert os.path.exists(TESTFN) - - def test_call_until(self): - ret = call_until(lambda: 1, "ret == 1") - self.assertEqual(ret, 1) - - -class TestFSTestUtils(unittest.TestCase): - - def setUp(self): - safe_rmpath(TESTFN) - - tearDown = setUp - - def test_open_text(self): - with open_text(__file__) as f: - self.assertEqual(f.mode, 'rt') - - def test_open_binary(self): - with open_binary(__file__) as f: - self.assertEqual(f.mode, 'rb') - - def test_safe_mkdir(self): - safe_mkdir(TESTFN) - assert os.path.isdir(TESTFN) - safe_mkdir(TESTFN) - assert os.path.isdir(TESTFN) - - def test_safe_rmpath(self): - # test file is removed - open(TESTFN, 'w').close() - safe_rmpath(TESTFN) - assert not os.path.exists(TESTFN) - # test no exception if path does not exist - safe_rmpath(TESTFN) - # test dir is removed - os.mkdir(TESTFN) - safe_rmpath(TESTFN) - assert not os.path.exists(TESTFN) - # test other exceptions are raised - with mock.patch('psutil.tests.os.stat', - side_effect=OSError(errno.EINVAL, "")) as m: - with self.assertRaises(OSError): - safe_rmpath(TESTFN) - assert m.called - - def test_chdir(self): - base = os.getcwd() - os.mkdir(TESTFN) - with chdir(TESTFN): - self.assertEqual(os.getcwd(), os.path.join(base, TESTFN)) - self.assertEqual(os.getcwd(), base) - - -class TestProcessUtils(unittest.TestCase): - - def test_reap_children(self): - subp = get_test_subprocess() - p = psutil.Process(subp.pid) - assert p.is_running() - reap_children() - assert not p.is_running() - assert not psutil.tests._pids_started - assert not psutil.tests._subprocesses_started - - def test_create_proc_children_pair(self): - p1, p2 = create_proc_children_pair() - self.assertNotEqual(p1.pid, p2.pid) - assert p1.is_running() - assert p2.is_running() - children = psutil.Process().children(recursive=True) - self.assertEqual(len(children), 2) - self.assertIn(p1, children) - self.assertIn(p2, children) - self.assertEqual(p1.ppid(), os.getpid()) - self.assertEqual(p2.ppid(), p1.pid) - - # make sure both of them are cleaned up - reap_children() - assert not p1.is_running() - assert not p2.is_running() - assert not psutil.tests._pids_started - assert not psutil.tests._subprocesses_started - - @unittest.skipIf(not POSIX, "POSIX only") - def test_create_zombie_proc(self): - zpid = create_zombie_proc() - self.addCleanup(reap_children, recursive=True) - p = psutil.Process(zpid) - self.assertEqual(p.status(), psutil.STATUS_ZOMBIE) - - -class TestNetUtils(unittest.TestCase): - - def bind_socket(self): - port = get_free_port() - with contextlib.closing(bind_socket(addr=('', port))) as s: - self.assertEqual(s.getsockname()[1], port) - - @unittest.skipIf(not POSIX, "POSIX only") - def test_bind_unix_socket(self): - with unix_socket_path() as name: - sock = bind_unix_socket(name) - with contextlib.closing(sock): - self.assertEqual(sock.family, socket.AF_UNIX) - self.assertEqual(sock.type, socket.SOCK_STREAM) - self.assertEqual(sock.getsockname(), name) - assert os.path.exists(name) - assert stat.S_ISSOCK(os.stat(name).st_mode) - # UDP - with unix_socket_path() as name: - sock = bind_unix_socket(name, type=socket.SOCK_DGRAM) - with contextlib.closing(sock): - self.assertEqual(sock.type, socket.SOCK_DGRAM) - - def tcp_tcp_socketpair(self): - addr = ("127.0.0.1", get_free_port()) - server, client = tcp_socketpair(socket.AF_INET, addr=addr) - with contextlib.closing(server): - with contextlib.closing(client): - # Ensure they are connected and the positions are - # correct. - self.assertEqual(server.getsockname(), addr) - self.assertEqual(client.getpeername(), addr) - self.assertNotEqual(client.getsockname(), addr) - - @unittest.skipIf(not POSIX, "POSIX only") - def test_unix_socketpair(self): - p = psutil.Process() - num_fds = p.num_fds() - assert not p.connections(kind='unix') - with unix_socket_path() as name: - server, client = unix_socketpair(name) - try: - assert os.path.exists(name) - assert stat.S_ISSOCK(os.stat(name).st_mode) - self.assertEqual(p.num_fds() - num_fds, 2) - self.assertEqual(len(p.connections(kind='unix')), 2) - self.assertEqual(server.getsockname(), name) - self.assertEqual(client.getpeername(), name) - finally: - client.close() - server.close() - - def test_create_sockets(self): - with create_sockets() as socks: - fams = collections.defaultdict(int) - types = collections.defaultdict(int) - for s in socks: - fams[s.family] += 1 - # work around http://bugs.python.org/issue30204 - types[s.getsockopt(socket.SOL_SOCKET, socket.SO_TYPE)] += 1 - self.assertGreaterEqual(fams[socket.AF_INET], 2) - if supports_ipv6(): - self.assertGreaterEqual(fams[socket.AF_INET6], 2) - if POSIX and HAS_CONNECTIONS_UNIX: - self.assertGreaterEqual(fams[socket.AF_UNIX], 2) - self.assertGreaterEqual(types[socket.SOCK_STREAM], 2) - self.assertGreaterEqual(types[socket.SOCK_DGRAM], 2) - - -class TestOtherUtils(unittest.TestCase): - - def test_is_namedtuple(self): - assert is_namedtuple(collections.namedtuple('foo', 'a b c')(1, 2, 3)) - assert not is_namedtuple(tuple()) - - -if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Miscellaneous tests.""" + +import ast +import collections +import errno +import json +import os +import pickle +import socket +import stat +import unittest + +import psutil +import psutil.tests +from psutil import LINUX +from psutil import POSIX +from psutil import WINDOWS +from psutil._common import bcat +from psutil._common import cat +from psutil._common import debug +from psutil._common import isfile_strict +from psutil._common import memoize +from psutil._common import memoize_when_activated +from psutil._common import parse_environ_block +from psutil._common import supports_ipv6 +from psutil._common import wrap_numbers +from psutil._compat import PY3 +from psutil._compat import FileNotFoundError +from psutil._compat import redirect_stderr +from psutil.tests import APPVEYOR +from psutil.tests import CI_TESTING +from psutil.tests import HAS_BATTERY +from psutil.tests import HAS_MEMORY_MAPS +from psutil.tests import HAS_NET_IO_COUNTERS +from psutil.tests import HAS_SENSORS_BATTERY +from psutil.tests import HAS_SENSORS_FANS +from psutil.tests import HAS_SENSORS_TEMPERATURES +from psutil.tests import PYTHON_EXE +from psutil.tests import PYTHON_EXE_ENV +from psutil.tests import SCRIPTS_DIR +from psutil.tests import PsutilTestCase +from psutil.tests import mock +from psutil.tests import reload_module +from psutil.tests import sh + + +# =================================================================== +# --- Test classes' repr(), str(), ... +# =================================================================== + + +class TestSpecialMethods(PsutilTestCase): + def test_check_pid_range(self): + with self.assertRaises(OverflowError): + psutil._psplatform.cext.check_pid_range(2**128) + with self.assertRaises(psutil.NoSuchProcess): + psutil.Process(2**128) + + def test_process__repr__(self, func=repr): + p = psutil.Process(self.spawn_testproc().pid) + r = func(p) + self.assertIn("psutil.Process", r) + self.assertIn("pid=%s" % p.pid, r) + self.assertIn( + "name='%s'" % str(p.name()), r.replace("name=u'", "name='") + ) + self.assertIn("status=", r) + self.assertNotIn("exitcode=", r) + p.terminate() + p.wait() + r = func(p) + self.assertIn("status='terminated'", r) + self.assertIn("exitcode=", r) + + with mock.patch.object( + psutil.Process, + "name", + side_effect=psutil.ZombieProcess(os.getpid()), + ): + p = psutil.Process() + r = func(p) + self.assertIn("pid=%s" % p.pid, r) + self.assertIn("status='zombie'", r) + self.assertNotIn("name=", r) + with mock.patch.object( + psutil.Process, + "name", + side_effect=psutil.NoSuchProcess(os.getpid()), + ): + p = psutil.Process() + r = func(p) + self.assertIn("pid=%s" % p.pid, r) + self.assertIn("terminated", r) + self.assertNotIn("name=", r) + with mock.patch.object( + psutil.Process, + "name", + side_effect=psutil.AccessDenied(os.getpid()), + ): + p = psutil.Process() + r = func(p) + self.assertIn("pid=%s" % p.pid, r) + self.assertNotIn("name=", r) + + def test_process__str__(self): + self.test_process__repr__(func=str) + + def test_error__repr__(self): + self.assertEqual(repr(psutil.Error()), "psutil.Error()") + + def test_error__str__(self): + self.assertEqual(str(psutil.Error()), "") + + def test_no_such_process__repr__(self): + self.assertEqual( + repr(psutil.NoSuchProcess(321)), + "psutil.NoSuchProcess(pid=321, msg='process no longer exists')", + ) + self.assertEqual( + repr(psutil.NoSuchProcess(321, name="name", msg="msg")), + "psutil.NoSuchProcess(pid=321, name='name', msg='msg')", + ) + + def test_no_such_process__str__(self): + self.assertEqual( + str(psutil.NoSuchProcess(321)), + "process no longer exists (pid=321)", + ) + self.assertEqual( + str(psutil.NoSuchProcess(321, name="name", msg="msg")), + "msg (pid=321, name='name')", + ) + + def test_zombie_process__repr__(self): + self.assertEqual( + repr(psutil.ZombieProcess(321)), + 'psutil.ZombieProcess(pid=321, msg="PID still ' + 'exists but it\'s a zombie")', + ) + self.assertEqual( + repr(psutil.ZombieProcess(321, name="name", ppid=320, msg="foo")), + "psutil.ZombieProcess(pid=321, ppid=320, name='name', msg='foo')", + ) + + def test_zombie_process__str__(self): + self.assertEqual( + str(psutil.ZombieProcess(321)), + "PID still exists but it's a zombie (pid=321)", + ) + self.assertEqual( + str(psutil.ZombieProcess(321, name="name", ppid=320, msg="foo")), + "foo (pid=321, ppid=320, name='name')", + ) + + def test_access_denied__repr__(self): + self.assertEqual( + repr(psutil.AccessDenied(321)), "psutil.AccessDenied(pid=321)" + ) + self.assertEqual( + repr(psutil.AccessDenied(321, name="name", msg="msg")), + "psutil.AccessDenied(pid=321, name='name', msg='msg')", + ) + + def test_access_denied__str__(self): + self.assertEqual(str(psutil.AccessDenied(321)), "(pid=321)") + self.assertEqual( + str(psutil.AccessDenied(321, name="name", msg="msg")), + "msg (pid=321, name='name')", + ) + + def test_timeout_expired__repr__(self): + self.assertEqual( + repr(psutil.TimeoutExpired(5)), + "psutil.TimeoutExpired(seconds=5, msg='timeout after 5 seconds')", + ) + self.assertEqual( + repr(psutil.TimeoutExpired(5, pid=321, name="name")), + "psutil.TimeoutExpired(pid=321, name='name', seconds=5, " + "msg='timeout after 5 seconds')", + ) + + def test_timeout_expired__str__(self): + self.assertEqual( + str(psutil.TimeoutExpired(5)), "timeout after 5 seconds" + ) + self.assertEqual( + str(psutil.TimeoutExpired(5, pid=321, name="name")), + "timeout after 5 seconds (pid=321, name='name')", + ) + + def test_process__eq__(self): + p1 = psutil.Process() + p2 = psutil.Process() + self.assertEqual(p1, p2) + p2._ident = (0, 0) + self.assertNotEqual(p1, p2) + self.assertNotEqual(p1, 'foo') + + def test_process__hash__(self): + s = set([psutil.Process(), psutil.Process()]) + self.assertEqual(len(s), 1) + + +# =================================================================== +# --- Misc, generic, corner cases +# =================================================================== + + +class TestMisc(PsutilTestCase): + def test__all__(self): + dir_psutil = dir(psutil) + for name in dir_psutil: + if name in ( + 'long', + 'tests', + 'test', + 'PermissionError', + 'ProcessLookupError', + ): + continue + if not name.startswith('_'): + try: + __import__(name) + except ImportError: + if name not in psutil.__all__: + fun = getattr(psutil, name) + if fun is None: + continue + if ( + fun.__doc__ is not None + and 'deprecated' not in fun.__doc__.lower() + ): + raise self.fail('%r not in psutil.__all__' % name) + + # Import 'star' will break if __all__ is inconsistent, see: + # https://github.com/giampaolo/psutil/issues/656 + # Can't do `from psutil import *` as it won't work on python 3 + # so we simply iterate over __all__. + for name in psutil.__all__: + self.assertIn(name, dir_psutil) + + def test_version(self): + self.assertEqual( + '.'.join([str(x) for x in psutil.version_info]), psutil.__version__ + ) + + def test_process_as_dict_no_new_names(self): + # See https://github.com/giampaolo/psutil/issues/813 + p = psutil.Process() + p.foo = '1' + self.assertNotIn('foo', p.as_dict()) + + def test_serialization(self): + def check(ret): + if json is not None: + json.loads(json.dumps(ret)) + a = pickle.dumps(ret) + b = pickle.loads(a) + self.assertEqual(ret, b) + + check(psutil.Process().as_dict()) + check(psutil.virtual_memory()) + check(psutil.swap_memory()) + check(psutil.cpu_times()) + check(psutil.cpu_times_percent(interval=0)) + check(psutil.net_io_counters()) + if LINUX and not os.path.exists('/proc/diskstats'): + pass + else: + if not APPVEYOR: + check(psutil.disk_io_counters()) + check(psutil.disk_partitions()) + check(psutil.disk_usage(os.getcwd())) + check(psutil.users()) + + # # XXX: https://github.com/pypa/setuptools/pull/2896 + # @unittest.skipIf(APPVEYOR, "temporarily disabled due to setuptools bug") + # def test_setup_script(self): + # setup_py = os.path.join(ROOT_DIR, 'setup.py') + # if CI_TESTING and not os.path.exists(setup_py): + # return self.skipTest("can't find setup.py") + # module = import_module_by_path(setup_py) + # self.assertRaises(SystemExit, module.setup) + # self.assertEqual(module.get_version(), psutil.__version__) + + def test_ad_on_process_creation(self): + # We are supposed to be able to instantiate Process also in case + # of zombie processes or access denied. + with mock.patch.object( + psutil.Process, 'create_time', side_effect=psutil.AccessDenied + ) as meth: + psutil.Process() + assert meth.called + with mock.patch.object( + psutil.Process, 'create_time', side_effect=psutil.ZombieProcess(1) + ) as meth: + psutil.Process() + assert meth.called + with mock.patch.object( + psutil.Process, 'create_time', side_effect=ValueError + ) as meth: + with self.assertRaises(ValueError): + psutil.Process() + assert meth.called + + def test_sanity_version_check(self): + # see: https://github.com/giampaolo/psutil/issues/564 + with mock.patch( + "psutil._psplatform.cext.version", return_value="0.0.0" + ): + with self.assertRaises(ImportError) as cm: + reload_module(psutil) + self.assertIn("version conflict", str(cm.exception).lower()) + + +# =================================================================== +# --- psutil/_common.py utils +# =================================================================== + + +class TestMemoizeDecorator(PsutilTestCase): + def setUp(self): + self.calls = [] + + tearDown = setUp + + def run_against(self, obj, expected_retval=None): + # no args + for _ in range(2): + ret = obj() + self.assertEqual(self.calls, [((), {})]) + if expected_retval is not None: + self.assertEqual(ret, expected_retval) + # with args + for _ in range(2): + ret = obj(1) + self.assertEqual(self.calls, [((), {}), ((1,), {})]) + if expected_retval is not None: + self.assertEqual(ret, expected_retval) + # with args + kwargs + for _ in range(2): + ret = obj(1, bar=2) + self.assertEqual( + self.calls, [((), {}), ((1,), {}), ((1,), {'bar': 2})] + ) + if expected_retval is not None: + self.assertEqual(ret, expected_retval) + # clear cache + self.assertEqual(len(self.calls), 3) + obj.cache_clear() + ret = obj() + if expected_retval is not None: + self.assertEqual(ret, expected_retval) + self.assertEqual(len(self.calls), 4) + # docstring + self.assertEqual(obj.__doc__, "My docstring.") + + def test_function(self): + @memoize + def foo(*args, **kwargs): + """My docstring.""" + baseclass.calls.append((args, kwargs)) + return 22 + + baseclass = self + self.run_against(foo, expected_retval=22) + + def test_class(self): + @memoize + class Foo: + """My docstring.""" + + def __init__(self, *args, **kwargs): + baseclass.calls.append((args, kwargs)) + + def bar(self): + return 22 + + baseclass = self + self.run_against(Foo, expected_retval=None) + self.assertEqual(Foo().bar(), 22) + + def test_class_singleton(self): + # @memoize can be used against classes to create singletons + @memoize + class Bar: + def __init__(self, *args, **kwargs): + pass + + self.assertIs(Bar(), Bar()) + self.assertEqual(id(Bar()), id(Bar())) + self.assertEqual(id(Bar(1)), id(Bar(1))) + self.assertEqual(id(Bar(1, foo=3)), id(Bar(1, foo=3))) + self.assertNotEqual(id(Bar(1)), id(Bar(2))) + + def test_staticmethod(self): + class Foo: + @staticmethod + @memoize + def bar(*args, **kwargs): + """My docstring.""" + baseclass.calls.append((args, kwargs)) + return 22 + + baseclass = self + self.run_against(Foo().bar, expected_retval=22) + + def test_classmethod(self): + class Foo: + @classmethod + @memoize + def bar(cls, *args, **kwargs): + """My docstring.""" + baseclass.calls.append((args, kwargs)) + return 22 + + baseclass = self + self.run_against(Foo().bar, expected_retval=22) + + def test_original(self): + # This was the original test before I made it dynamic to test it + # against different types. Keeping it anyway. + @memoize + def foo(*args, **kwargs): + """Foo docstring.""" + calls.append(None) + return (args, kwargs) + + calls = [] + # no args + for _ in range(2): + ret = foo() + expected = ((), {}) + self.assertEqual(ret, expected) + self.assertEqual(len(calls), 1) + # with args + for _ in range(2): + ret = foo(1) + expected = ((1,), {}) + self.assertEqual(ret, expected) + self.assertEqual(len(calls), 2) + # with args + kwargs + for _ in range(2): + ret = foo(1, bar=2) + expected = ((1,), {'bar': 2}) + self.assertEqual(ret, expected) + self.assertEqual(len(calls), 3) + # clear cache + foo.cache_clear() + ret = foo() + expected = ((), {}) + self.assertEqual(ret, expected) + self.assertEqual(len(calls), 4) + # docstring + self.assertEqual(foo.__doc__, "Foo docstring.") + + +class TestCommonModule(PsutilTestCase): + def test_memoize_when_activated(self): + class Foo: + @memoize_when_activated + def foo(self): + calls.append(None) + + f = Foo() + calls = [] + f.foo() + f.foo() + self.assertEqual(len(calls), 2) + + # activate + calls = [] + f.foo.cache_activate(f) + f.foo() + f.foo() + self.assertEqual(len(calls), 1) + + # deactivate + calls = [] + f.foo.cache_deactivate(f) + f.foo() + f.foo() + self.assertEqual(len(calls), 2) + + def test_parse_environ_block(self): + def k(s): + return s.upper() if WINDOWS else s + + self.assertEqual(parse_environ_block("a=1\0"), {k("a"): "1"}) + self.assertEqual( + parse_environ_block("a=1\0b=2\0\0"), {k("a"): "1", k("b"): "2"} + ) + self.assertEqual( + parse_environ_block("a=1\0b=\0\0"), {k("a"): "1", k("b"): ""} + ) + # ignore everything after \0\0 + self.assertEqual( + parse_environ_block("a=1\0b=2\0\0c=3\0"), + {k("a"): "1", k("b"): "2"}, + ) + # ignore everything that is not an assignment + self.assertEqual(parse_environ_block("xxx\0a=1\0"), {k("a"): "1"}) + self.assertEqual(parse_environ_block("a=1\0=b=2\0"), {k("a"): "1"}) + # do not fail if the block is incomplete + self.assertEqual(parse_environ_block("a=1\0b=2"), {k("a"): "1"}) + + def test_supports_ipv6(self): + self.addCleanup(supports_ipv6.cache_clear) + if supports_ipv6(): + with mock.patch('psutil._common.socket') as s: + s.has_ipv6 = False + supports_ipv6.cache_clear() + assert not supports_ipv6() + + supports_ipv6.cache_clear() + with mock.patch( + 'psutil._common.socket.socket', side_effect=socket.error + ) as s: + assert not supports_ipv6() + assert s.called + + supports_ipv6.cache_clear() + with mock.patch( + 'psutil._common.socket.socket', side_effect=socket.gaierror + ) as s: + assert not supports_ipv6() + supports_ipv6.cache_clear() + assert s.called + + supports_ipv6.cache_clear() + with mock.patch( + 'psutil._common.socket.socket.bind', + side_effect=socket.gaierror, + ) as s: + assert not supports_ipv6() + supports_ipv6.cache_clear() + assert s.called + else: + with self.assertRaises(socket.error): + sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + try: + sock.bind(("::1", 0)) + finally: + sock.close() + + def test_isfile_strict(self): + this_file = os.path.abspath(__file__) + assert isfile_strict(this_file) + assert not isfile_strict(os.path.dirname(this_file)) + with mock.patch( + 'psutil._common.os.stat', side_effect=OSError(errno.EPERM, "foo") + ): + self.assertRaises(OSError, isfile_strict, this_file) + with mock.patch( + 'psutil._common.os.stat', side_effect=OSError(errno.EACCES, "foo") + ): + self.assertRaises(OSError, isfile_strict, this_file) + with mock.patch( + 'psutil._common.os.stat', side_effect=OSError(errno.ENOENT, "foo") + ): + assert not isfile_strict(this_file) + with mock.patch('psutil._common.stat.S_ISREG', return_value=False): + assert not isfile_strict(this_file) + + def test_debug(self): + if PY3: + from io import StringIO + else: + from StringIO import StringIO + + with redirect_stderr(StringIO()) as f: + debug("hello") + msg = f.getvalue() + assert msg.startswith("psutil-debug"), msg + self.assertIn("hello", msg) + self.assertIn(__file__.replace('.pyc', '.py'), msg) + + # supposed to use repr(exc) + with redirect_stderr(StringIO()) as f: + debug(ValueError("this is an error")) + msg = f.getvalue() + self.assertIn("ignoring ValueError", msg) + self.assertIn("'this is an error'", msg) + + # supposed to use str(exc), because of extra info about file name + with redirect_stderr(StringIO()) as f: + exc = OSError(2, "no such file") + exc.filename = "/foo" + debug(exc) + msg = f.getvalue() + self.assertIn("no such file", msg) + self.assertIn("/foo", msg) + + def test_cat_bcat(self): + testfn = self.get_testfn() + with open(testfn, "w") as f: + f.write("foo") + self.assertEqual(cat(testfn), "foo") + self.assertEqual(bcat(testfn), b"foo") + self.assertRaises(FileNotFoundError, cat, testfn + '-invalid') + self.assertRaises(FileNotFoundError, bcat, testfn + '-invalid') + self.assertEqual(cat(testfn + '-invalid', fallback="bar"), "bar") + self.assertEqual(bcat(testfn + '-invalid', fallback="bar"), "bar") + + +# =================================================================== +# --- Tests for wrap_numbers() function. +# =================================================================== + + +nt = collections.namedtuple('foo', 'a b c') + + +class TestWrapNumbers(PsutilTestCase): + def setUp(self): + wrap_numbers.cache_clear() + + tearDown = setUp + + def test_first_call(self): + input = {'disk1': nt(5, 5, 5)} + self.assertEqual(wrap_numbers(input, 'disk_io'), input) + + def test_input_hasnt_changed(self): + input = {'disk1': nt(5, 5, 5)} + self.assertEqual(wrap_numbers(input, 'disk_io'), input) + self.assertEqual(wrap_numbers(input, 'disk_io'), input) + + def test_increase_but_no_wrap(self): + input = {'disk1': nt(5, 5, 5)} + self.assertEqual(wrap_numbers(input, 'disk_io'), input) + input = {'disk1': nt(10, 15, 20)} + self.assertEqual(wrap_numbers(input, 'disk_io'), input) + input = {'disk1': nt(20, 25, 30)} + self.assertEqual(wrap_numbers(input, 'disk_io'), input) + input = {'disk1': nt(20, 25, 30)} + self.assertEqual(wrap_numbers(input, 'disk_io'), input) + + def test_wrap(self): + # let's say 100 is the threshold + input = {'disk1': nt(100, 100, 100)} + self.assertEqual(wrap_numbers(input, 'disk_io'), input) + # first wrap restarts from 10 + input = {'disk1': nt(100, 100, 10)} + self.assertEqual( + wrap_numbers(input, 'disk_io'), {'disk1': nt(100, 100, 110)} + ) + # then it remains the same + input = {'disk1': nt(100, 100, 10)} + self.assertEqual( + wrap_numbers(input, 'disk_io'), {'disk1': nt(100, 100, 110)} + ) + # then it goes up + input = {'disk1': nt(100, 100, 90)} + self.assertEqual( + wrap_numbers(input, 'disk_io'), {'disk1': nt(100, 100, 190)} + ) + # then it wraps again + input = {'disk1': nt(100, 100, 20)} + self.assertEqual( + wrap_numbers(input, 'disk_io'), {'disk1': nt(100, 100, 210)} + ) + # and remains the same + input = {'disk1': nt(100, 100, 20)} + self.assertEqual( + wrap_numbers(input, 'disk_io'), {'disk1': nt(100, 100, 210)} + ) + # now wrap another num + input = {'disk1': nt(50, 100, 20)} + self.assertEqual( + wrap_numbers(input, 'disk_io'), {'disk1': nt(150, 100, 210)} + ) + # and again + input = {'disk1': nt(40, 100, 20)} + self.assertEqual( + wrap_numbers(input, 'disk_io'), {'disk1': nt(190, 100, 210)} + ) + # keep it the same + input = {'disk1': nt(40, 100, 20)} + self.assertEqual( + wrap_numbers(input, 'disk_io'), {'disk1': nt(190, 100, 210)} + ) + + def test_changing_keys(self): + # Emulate a case where the second call to disk_io() + # (or whatever) provides a new disk, then the new disk + # disappears on the third call. + input = {'disk1': nt(5, 5, 5)} + self.assertEqual(wrap_numbers(input, 'disk_io'), input) + input = {'disk1': nt(5, 5, 5), 'disk2': nt(7, 7, 7)} + self.assertEqual(wrap_numbers(input, 'disk_io'), input) + input = {'disk1': nt(8, 8, 8)} + self.assertEqual(wrap_numbers(input, 'disk_io'), input) + + def test_changing_keys_w_wrap(self): + input = {'disk1': nt(50, 50, 50), 'disk2': nt(100, 100, 100)} + self.assertEqual(wrap_numbers(input, 'disk_io'), input) + # disk 2 wraps + input = {'disk1': nt(50, 50, 50), 'disk2': nt(100, 100, 10)} + self.assertEqual( + wrap_numbers(input, 'disk_io'), + {'disk1': nt(50, 50, 50), 'disk2': nt(100, 100, 110)}, + ) + # disk 2 disappears + input = {'disk1': nt(50, 50, 50)} + self.assertEqual(wrap_numbers(input, 'disk_io'), input) + + # then it appears again; the old wrap is supposed to be + # gone. + input = {'disk1': nt(50, 50, 50), 'disk2': nt(100, 100, 100)} + self.assertEqual(wrap_numbers(input, 'disk_io'), input) + # remains the same + input = {'disk1': nt(50, 50, 50), 'disk2': nt(100, 100, 100)} + self.assertEqual(wrap_numbers(input, 'disk_io'), input) + # and then wraps again + input = {'disk1': nt(50, 50, 50), 'disk2': nt(100, 100, 10)} + self.assertEqual( + wrap_numbers(input, 'disk_io'), + {'disk1': nt(50, 50, 50), 'disk2': nt(100, 100, 110)}, + ) + + def test_real_data(self): + d = { + 'nvme0n1': (300, 508, 640, 1571, 5970, 1987, 2049, 451751, 47048), + 'nvme0n1p1': (1171, 2, 5600256, 1024, 516, 0, 0, 0, 8), + 'nvme0n1p2': (54, 54, 2396160, 5165056, 4, 24, 30, 1207, 28), + 'nvme0n1p3': (2389, 4539, 5154, 150, 4828, 1844, 2019, 398, 348), + } + self.assertEqual(wrap_numbers(d, 'disk_io'), d) + self.assertEqual(wrap_numbers(d, 'disk_io'), d) + # decrease this ↓ + d = { + 'nvme0n1': (100, 508, 640, 1571, 5970, 1987, 2049, 451751, 47048), + 'nvme0n1p1': (1171, 2, 5600256, 1024, 516, 0, 0, 0, 8), + 'nvme0n1p2': (54, 54, 2396160, 5165056, 4, 24, 30, 1207, 28), + 'nvme0n1p3': (2389, 4539, 5154, 150, 4828, 1844, 2019, 398, 348), + } + out = wrap_numbers(d, 'disk_io') + self.assertEqual(out['nvme0n1'][0], 400) + + # --- cache tests + + def test_cache_first_call(self): + input = {'disk1': nt(5, 5, 5)} + wrap_numbers(input, 'disk_io') + cache = wrap_numbers.cache_info() + self.assertEqual(cache[0], {'disk_io': input}) + self.assertEqual(cache[1], {'disk_io': {}}) + self.assertEqual(cache[2], {'disk_io': {}}) + + def test_cache_call_twice(self): + input = {'disk1': nt(5, 5, 5)} + wrap_numbers(input, 'disk_io') + input = {'disk1': nt(10, 10, 10)} + wrap_numbers(input, 'disk_io') + cache = wrap_numbers.cache_info() + self.assertEqual(cache[0], {'disk_io': input}) + self.assertEqual( + cache[1], + {'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 0}}, + ) + self.assertEqual(cache[2], {'disk_io': {}}) + + def test_cache_wrap(self): + # let's say 100 is the threshold + input = {'disk1': nt(100, 100, 100)} + wrap_numbers(input, 'disk_io') + + # first wrap restarts from 10 + input = {'disk1': nt(100, 100, 10)} + wrap_numbers(input, 'disk_io') + cache = wrap_numbers.cache_info() + self.assertEqual(cache[0], {'disk_io': input}) + self.assertEqual( + cache[1], + {'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 100}}, + ) + self.assertEqual(cache[2], {'disk_io': {'disk1': set([('disk1', 2)])}}) + + def check_cache_info(): + cache = wrap_numbers.cache_info() + self.assertEqual( + cache[1], + { + 'disk_io': { + ('disk1', 0): 0, + ('disk1', 1): 0, + ('disk1', 2): 100, + } + }, + ) + self.assertEqual( + cache[2], {'disk_io': {'disk1': set([('disk1', 2)])}} + ) + + # then it remains the same + input = {'disk1': nt(100, 100, 10)} + wrap_numbers(input, 'disk_io') + cache = wrap_numbers.cache_info() + self.assertEqual(cache[0], {'disk_io': input}) + check_cache_info() + + # then it goes up + input = {'disk1': nt(100, 100, 90)} + wrap_numbers(input, 'disk_io') + cache = wrap_numbers.cache_info() + self.assertEqual(cache[0], {'disk_io': input}) + check_cache_info() + + # then it wraps again + input = {'disk1': nt(100, 100, 20)} + wrap_numbers(input, 'disk_io') + cache = wrap_numbers.cache_info() + self.assertEqual(cache[0], {'disk_io': input}) + self.assertEqual( + cache[1], + {'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 190}}, + ) + self.assertEqual(cache[2], {'disk_io': {'disk1': set([('disk1', 2)])}}) + + def test_cache_changing_keys(self): + input = {'disk1': nt(5, 5, 5)} + wrap_numbers(input, 'disk_io') + input = {'disk1': nt(5, 5, 5), 'disk2': nt(7, 7, 7)} + wrap_numbers(input, 'disk_io') + cache = wrap_numbers.cache_info() + self.assertEqual(cache[0], {'disk_io': input}) + self.assertEqual( + cache[1], + {'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 0}}, + ) + self.assertEqual(cache[2], {'disk_io': {}}) + + def test_cache_clear(self): + input = {'disk1': nt(5, 5, 5)} + wrap_numbers(input, 'disk_io') + wrap_numbers(input, 'disk_io') + wrap_numbers.cache_clear('disk_io') + self.assertEqual(wrap_numbers.cache_info(), ({}, {}, {})) + wrap_numbers.cache_clear('disk_io') + wrap_numbers.cache_clear('?!?') + + @unittest.skipIf(not HAS_NET_IO_COUNTERS, 'not supported') + def test_cache_clear_public_apis(self): + if not psutil.disk_io_counters() or not psutil.net_io_counters(): + return self.skipTest("no disks or NICs available") + psutil.disk_io_counters() + psutil.net_io_counters() + caches = wrap_numbers.cache_info() + for cache in caches: + self.assertIn('psutil.disk_io_counters', cache) + self.assertIn('psutil.net_io_counters', cache) + + psutil.disk_io_counters.cache_clear() + caches = wrap_numbers.cache_info() + for cache in caches: + self.assertIn('psutil.net_io_counters', cache) + self.assertNotIn('psutil.disk_io_counters', cache) + + psutil.net_io_counters.cache_clear() + caches = wrap_numbers.cache_info() + self.assertEqual(caches, ({}, {}, {})) + + +# =================================================================== +# --- Example script tests +# =================================================================== + + +@unittest.skipIf( + not os.path.exists(SCRIPTS_DIR), "can't locate scripts directory" +) +class TestScripts(PsutilTestCase): + """Tests for scripts in the "scripts" directory.""" + + @staticmethod + def assert_stdout(exe, *args, **kwargs): + kwargs.setdefault("env", PYTHON_EXE_ENV) + exe = '%s' % os.path.join(SCRIPTS_DIR, exe) + cmd = [PYTHON_EXE, exe] + for arg in args: + cmd.append(arg) + try: + out = sh(cmd, **kwargs).strip() + except RuntimeError as err: + if 'AccessDenied' in str(err): + return str(err) + else: + raise + assert out, out + return out + + @staticmethod + def assert_syntax(exe): + exe = os.path.join(SCRIPTS_DIR, exe) + with open(exe, encoding="utf8") if PY3 else open(exe) as f: + src = f.read() + ast.parse(src) + + def test_coverage(self): + # make sure all example scripts have a test method defined + meths = dir(self) + for name in os.listdir(SCRIPTS_DIR): + if name.endswith('.py'): + if 'test_' + os.path.splitext(name)[0] not in meths: + # self.assert_stdout(name) + raise self.fail( + 'no test defined for %r script' + % os.path.join(SCRIPTS_DIR, name) + ) + + @unittest.skipIf(not POSIX, "POSIX only") + def test_executable(self): + for root, dirs, files in os.walk(SCRIPTS_DIR): + for file in files: + if file.endswith('.py'): + path = os.path.join(root, file) + if not stat.S_IXUSR & os.stat(path)[stat.ST_MODE]: + raise self.fail('%r is not executable' % path) + + def test_disk_usage(self): + self.assert_stdout('disk_usage.py') + + def test_free(self): + self.assert_stdout('free.py') + + def test_meminfo(self): + self.assert_stdout('meminfo.py') + + def test_procinfo(self): + self.assert_stdout('procinfo.py', str(os.getpid())) + + @unittest.skipIf(CI_TESTING and not psutil.users(), "no users") + def test_who(self): + self.assert_stdout('who.py') + + def test_ps(self): + self.assert_stdout('ps.py') + + def test_pstree(self): + self.assert_stdout('pstree.py') + + def test_netstat(self): + self.assert_stdout('netstat.py') + + def test_ifconfig(self): + self.assert_stdout('ifconfig.py') + + @unittest.skipIf(not HAS_MEMORY_MAPS, "not supported") + def test_pmap(self): + self.assert_stdout('pmap.py', str(os.getpid())) + + def test_procsmem(self): + if 'uss' not in psutil.Process().memory_full_info()._fields: + raise self.skipTest("not supported") + self.assert_stdout('procsmem.py') + + def test_killall(self): + self.assert_syntax('killall.py') + + def test_nettop(self): + self.assert_syntax('nettop.py') + + def test_top(self): + self.assert_syntax('top.py') + + def test_iotop(self): + self.assert_syntax('iotop.py') + + def test_pidof(self): + output = self.assert_stdout('pidof.py', psutil.Process().name()) + self.assertIn(str(os.getpid()), output) + + @unittest.skipIf(not WINDOWS, "WINDOWS only") + def test_winservices(self): + self.assert_stdout('winservices.py') + + def test_cpu_distribution(self): + self.assert_syntax('cpu_distribution.py') + + @unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported") + def test_temperatures(self): + if not psutil.sensors_temperatures(): + self.skipTest("no temperatures") + self.assert_stdout('temperatures.py') + + @unittest.skipIf(not HAS_SENSORS_FANS, "not supported") + def test_fans(self): + if not psutil.sensors_fans(): + self.skipTest("no fans") + self.assert_stdout('fans.py') + + @unittest.skipIf(not HAS_SENSORS_BATTERY, "not supported") + @unittest.skipIf(not HAS_BATTERY, "no battery") + def test_battery(self): + self.assert_stdout('battery.py') + + @unittest.skipIf(not HAS_SENSORS_BATTERY, "not supported") + @unittest.skipIf(not HAS_BATTERY, "no battery") + def test_sensors(self): + self.assert_stdout('sensors.py') + + +if __name__ == '__main__': + from psutil.tests.runner import run_from_name + + run_from_name(__file__) diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_misc.pyc b/addon/globalPlugins/soundmanager/psutil/tests/test_misc.pyc deleted file mode 100644 index d431612..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/tests/test_misc.pyc and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_osx.py b/addon/globalPlugins/soundmanager/psutil/tests/test_osx.py index 723b255..b0a6218 100644 --- a/addon/globalPlugins/soundmanager/psutil/tests/test_osx.py +++ b/addon/globalPlugins/soundmanager/psutil/tests/test_osx.py @@ -1,294 +1,210 @@ -#!/usr/bin/env python - -# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -"""MACOS specific tests.""" - -import os -import re -import time - -import psutil -from psutil import MACOS -from psutil.tests import create_zombie_proc -from psutil.tests import get_test_subprocess -from psutil.tests import HAS_BATTERY -from psutil.tests import MEMORY_TOLERANCE -from psutil.tests import reap_children -from psutil.tests import retry_on_failure -from psutil.tests import sh -from psutil.tests import unittest - - -PAGESIZE = os.sysconf("SC_PAGE_SIZE") if MACOS else None - - -def sysctl(cmdline): - """Expects a sysctl command with an argument and parse the result - returning only the value of interest. - """ - out = sh(cmdline) - result = out.split()[1] - try: - return int(result) - except ValueError: - return result - - -def vm_stat(field): - """Wrapper around 'vm_stat' cmdline utility.""" - out = sh('vm_stat') - for line in out.split('\n'): - if field in line: - break - else: - raise ValueError("line not found") - return int(re.search(r'\d+', line).group(0)) * PAGESIZE - - -# http://code.activestate.com/recipes/578019/ -def human2bytes(s): - SYMBOLS = { - 'customary': ('B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'), - } - init = s - num = "" - while s and s[0:1].isdigit() or s[0:1] == '.': - num += s[0] - s = s[1:] - num = float(num) - letter = s.strip() - for name, sset in SYMBOLS.items(): - if letter in sset: - break - else: - if letter == 'k': - sset = SYMBOLS['customary'] - letter = letter.upper() - else: - raise ValueError("can't interpret %r" % init) - prefix = {sset[0]: 1} - for i, s in enumerate(sset[1:]): - prefix[s] = 1 << (i + 1) * 10 - return int(num * prefix[letter]) - - -@unittest.skipIf(not MACOS, "MACOS only") -class TestProcess(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.pid = get_test_subprocess().pid - - @classmethod - def tearDownClass(cls): - reap_children() - - def test_process_create_time(self): - output = sh("ps -o lstart -p %s" % self.pid) - start_ps = output.replace('STARTED', '').strip() - hhmmss = start_ps.split(' ')[-2] - year = start_ps.split(' ')[-1] - start_psutil = psutil.Process(self.pid).create_time() - self.assertEqual( - hhmmss, - time.strftime("%H:%M:%S", time.localtime(start_psutil))) - self.assertEqual( - year, - time.strftime("%Y", time.localtime(start_psutil))) - - -@unittest.skipIf(not MACOS, "MACOS only") -class TestZombieProcessAPIs(unittest.TestCase): - - @classmethod - def setUpClass(cls): - zpid = create_zombie_proc() - cls.p = psutil.Process(zpid) - - @classmethod - def tearDownClass(cls): - reap_children(recursive=True) - - def test_pidtask_info(self): - self.assertEqual(self.p.status(), psutil.STATUS_ZOMBIE) - self.p.ppid() - self.p.uids() - self.p.gids() - self.p.terminal() - self.p.create_time() - - def test_exe(self): - self.assertRaises(psutil.ZombieProcess, self.p.exe) - - def test_cmdline(self): - self.assertRaises(psutil.ZombieProcess, self.p.cmdline) - - def test_environ(self): - self.assertRaises(psutil.ZombieProcess, self.p.environ) - - def test_cwd(self): - self.assertRaises(psutil.ZombieProcess, self.p.cwd) - - def test_memory_full_info(self): - self.assertRaises(psutil.ZombieProcess, self.p.memory_full_info) - - def test_cpu_times(self): - self.assertRaises(psutil.ZombieProcess, self.p.cpu_times) - - def test_num_ctx_switches(self): - self.assertRaises(psutil.ZombieProcess, self.p.num_ctx_switches) - - def test_num_threads(self): - self.assertRaises(psutil.ZombieProcess, self.p.num_threads) - - def test_open_files(self): - self.assertRaises(psutil.ZombieProcess, self.p.open_files) - - def test_connections(self): - self.assertRaises(psutil.ZombieProcess, self.p.connections) - - def test_num_fds(self): - self.assertRaises(psutil.ZombieProcess, self.p.num_fds) - - def test_threads(self): - self.assertRaises((psutil.ZombieProcess, psutil.AccessDenied), - self.p.threads) - - -@unittest.skipIf(not MACOS, "MACOS only") -class TestSystemAPIs(unittest.TestCase): - - # --- disk - - def test_disks(self): - # test psutil.disk_usage() and psutil.disk_partitions() - # against "df -a" - def df(path): - out = sh('df -k "%s"' % path).strip() - lines = out.split('\n') - lines.pop(0) - line = lines.pop(0) - dev, total, used, free = line.split()[:4] - if dev == 'none': - dev = '' - total = int(total) * 1024 - used = int(used) * 1024 - free = int(free) * 1024 - return dev, total, used, free - - for part in psutil.disk_partitions(all=False): - usage = psutil.disk_usage(part.mountpoint) - dev, total, used, free = df(part.mountpoint) - self.assertEqual(part.device, dev) - self.assertEqual(usage.total, total) - # 10 MB tollerance - if abs(usage.free - free) > 10 * 1024 * 1024: - self.fail("psutil=%s, df=%s" % usage.free, free) - if abs(usage.used - used) > 10 * 1024 * 1024: - self.fail("psutil=%s, df=%s" % usage.used, used) - - # --- cpu - - def test_cpu_count_logical(self): - num = sysctl("sysctl hw.logicalcpu") - self.assertEqual(num, psutil.cpu_count(logical=True)) - - def test_cpu_count_physical(self): - num = sysctl("sysctl hw.physicalcpu") - self.assertEqual(num, psutil.cpu_count(logical=False)) - - def test_cpu_freq(self): - freq = psutil.cpu_freq() - self.assertEqual( - freq.current * 1000 * 1000, sysctl("sysctl hw.cpufrequency")) - self.assertEqual( - freq.min * 1000 * 1000, sysctl("sysctl hw.cpufrequency_min")) - self.assertEqual( - freq.max * 1000 * 1000, sysctl("sysctl hw.cpufrequency_max")) - - # --- virtual mem - - def test_vmem_total(self): - sysctl_hwphymem = sysctl('sysctl hw.memsize') - self.assertEqual(sysctl_hwphymem, psutil.virtual_memory().total) - - @retry_on_failure() - def test_vmem_free(self): - vmstat_val = vm_stat("free") - psutil_val = psutil.virtual_memory().free - self.assertAlmostEqual(psutil_val, vmstat_val, delta=MEMORY_TOLERANCE) - - @retry_on_failure() - def test_vmem_active(self): - vmstat_val = vm_stat("active") - psutil_val = psutil.virtual_memory().active - self.assertAlmostEqual(psutil_val, vmstat_val, delta=MEMORY_TOLERANCE) - - @retry_on_failure() - def test_vmem_inactive(self): - vmstat_val = vm_stat("inactive") - psutil_val = psutil.virtual_memory().inactive - self.assertAlmostEqual(psutil_val, vmstat_val, delta=MEMORY_TOLERANCE) - - @retry_on_failure() - def test_vmem_wired(self): - vmstat_val = vm_stat("wired") - psutil_val = psutil.virtual_memory().wired - self.assertAlmostEqual(psutil_val, vmstat_val, delta=MEMORY_TOLERANCE) - - # --- swap mem - - @retry_on_failure() - def test_swapmem_sin(self): - vmstat_val = vm_stat("Pageins") - psutil_val = psutil.swap_memory().sin - self.assertEqual(psutil_val, vmstat_val) - - @retry_on_failure() - def test_swapmem_sout(self): - vmstat_val = vm_stat("Pageout") - psutil_val = psutil.swap_memory().sout - self.assertEqual(psutil_val, vmstat_val) - - # Not very reliable. - # def test_swapmem_total(self): - # out = sh('sysctl vm.swapusage') - # out = out.replace('vm.swapusage: ', '') - # total, used, free = re.findall('\d+.\d+\w', out) - # psutil_smem = psutil.swap_memory() - # self.assertEqual(psutil_smem.total, human2bytes(total)) - # self.assertEqual(psutil_smem.used, human2bytes(used)) - # self.assertEqual(psutil_smem.free, human2bytes(free)) - - # --- network - - def test_net_if_stats(self): - for name, stats in psutil.net_if_stats().items(): - try: - out = sh("ifconfig %s" % name) - except RuntimeError: - pass - else: - self.assertEqual(stats.isup, 'RUNNING' in out, msg=out) - self.assertEqual(stats.mtu, - int(re.findall(r'mtu (\d+)', out)[0])) - - # --- sensors_battery - - @unittest.skipIf(not HAS_BATTERY, "no battery") - def test_sensors_battery(self): - out = sh("pmset -g batt") - percent = re.search(r"(\d+)%", out).group(1) - drawing_from = re.search("Now drawing from '([^']+)'", out).group(1) - power_plugged = drawing_from == "AC Power" - psutil_result = psutil.sensors_battery() - self.assertEqual(psutil_result.power_plugged, power_plugged) - self.assertEqual(psutil_result.percent, int(percent)) - - -if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""macOS specific tests.""" + +import platform +import re +import time +import unittest + +import psutil +from psutil import MACOS +from psutil import POSIX +from psutil.tests import HAS_BATTERY +from psutil.tests import TOLERANCE_DISK_USAGE +from psutil.tests import TOLERANCE_SYS_MEM +from psutil.tests import PsutilTestCase +from psutil.tests import retry_on_failure +from psutil.tests import sh +from psutil.tests import spawn_testproc +from psutil.tests import terminate + + +if POSIX: + from psutil._psutil_posix import getpagesize + + +def sysctl(cmdline): + """Expects a sysctl command with an argument and parse the result + returning only the value of interest. + """ + out = sh(cmdline) + result = out.split()[1] + try: + return int(result) + except ValueError: + return result + + +def vm_stat(field): + """Wrapper around 'vm_stat' cmdline utility.""" + out = sh('vm_stat') + for line in out.split('\n'): + if field in line: + break + else: + raise ValueError("line not found") + return int(re.search(r'\d+', line).group(0)) * getpagesize() + + +@unittest.skipIf(not MACOS, "MACOS only") +class TestProcess(PsutilTestCase): + @classmethod + def setUpClass(cls): + cls.pid = spawn_testproc().pid + + @classmethod + def tearDownClass(cls): + terminate(cls.pid) + + def test_process_create_time(self): + output = sh("ps -o lstart -p %s" % self.pid) + start_ps = output.replace('STARTED', '').strip() + hhmmss = start_ps.split(' ')[-2] + year = start_ps.split(' ')[-1] + start_psutil = psutil.Process(self.pid).create_time() + self.assertEqual( + hhmmss, time.strftime("%H:%M:%S", time.localtime(start_psutil)) + ) + self.assertEqual( + year, time.strftime("%Y", time.localtime(start_psutil)) + ) + + +@unittest.skipIf(not MACOS, "MACOS only") +class TestSystemAPIs(PsutilTestCase): + + # --- disk + + @retry_on_failure() + def test_disks(self): + # test psutil.disk_usage() and psutil.disk_partitions() + # against "df -a" + def df(path): + out = sh('df -k "%s"' % path).strip() + lines = out.split('\n') + lines.pop(0) + line = lines.pop(0) + dev, total, used, free = line.split()[:4] + if dev == 'none': + dev = '' + total = int(total) * 1024 + used = int(used) * 1024 + free = int(free) * 1024 + return dev, total, used, free + + for part in psutil.disk_partitions(all=False): + usage = psutil.disk_usage(part.mountpoint) + dev, total, used, free = df(part.mountpoint) + self.assertEqual(part.device, dev) + self.assertEqual(usage.total, total) + self.assertAlmostEqual( + usage.free, free, delta=TOLERANCE_DISK_USAGE + ) + self.assertAlmostEqual( + usage.used, used, delta=TOLERANCE_DISK_USAGE + ) + + # --- cpu + + def test_cpu_count_logical(self): + num = sysctl("sysctl hw.logicalcpu") + self.assertEqual(num, psutil.cpu_count(logical=True)) + + def test_cpu_count_cores(self): + num = sysctl("sysctl hw.physicalcpu") + self.assertEqual(num, psutil.cpu_count(logical=False)) + + # TODO: remove this once 1892 is fixed + @unittest.skipIf(platform.machine() == 'arm64', "skipped due to #1892") + def test_cpu_freq(self): + freq = psutil.cpu_freq() + self.assertEqual( + freq.current * 1000 * 1000, sysctl("sysctl hw.cpufrequency") + ) + self.assertEqual( + freq.min * 1000 * 1000, sysctl("sysctl hw.cpufrequency_min") + ) + self.assertEqual( + freq.max * 1000 * 1000, sysctl("sysctl hw.cpufrequency_max") + ) + + # --- virtual mem + + def test_vmem_total(self): + sysctl_hwphymem = sysctl('sysctl hw.memsize') + self.assertEqual(sysctl_hwphymem, psutil.virtual_memory().total) + + @retry_on_failure() + def test_vmem_free(self): + vmstat_val = vm_stat("free") + psutil_val = psutil.virtual_memory().free + self.assertAlmostEqual(psutil_val, vmstat_val, delta=TOLERANCE_SYS_MEM) + + @retry_on_failure() + def test_vmem_active(self): + vmstat_val = vm_stat("active") + psutil_val = psutil.virtual_memory().active + self.assertAlmostEqual(psutil_val, vmstat_val, delta=TOLERANCE_SYS_MEM) + + @retry_on_failure() + def test_vmem_inactive(self): + vmstat_val = vm_stat("inactive") + psutil_val = psutil.virtual_memory().inactive + self.assertAlmostEqual(psutil_val, vmstat_val, delta=TOLERANCE_SYS_MEM) + + @retry_on_failure() + def test_vmem_wired(self): + vmstat_val = vm_stat("wired") + psutil_val = psutil.virtual_memory().wired + self.assertAlmostEqual(psutil_val, vmstat_val, delta=TOLERANCE_SYS_MEM) + + # --- swap mem + + @retry_on_failure() + def test_swapmem_sin(self): + vmstat_val = vm_stat("Pageins") + psutil_val = psutil.swap_memory().sin + self.assertAlmostEqual(psutil_val, vmstat_val, delta=TOLERANCE_SYS_MEM) + + @retry_on_failure() + def test_swapmem_sout(self): + vmstat_val = vm_stat("Pageout") + psutil_val = psutil.swap_memory().sout + self.assertAlmostEqual(psutil_val, vmstat_val, delta=TOLERANCE_SYS_MEM) + + # --- network + + def test_net_if_stats(self): + for name, stats in psutil.net_if_stats().items(): + try: + out = sh("ifconfig %s" % name) + except RuntimeError: + pass + else: + self.assertEqual(stats.isup, 'RUNNING' in out, msg=out) + self.assertEqual( + stats.mtu, int(re.findall(r'mtu (\d+)', out)[0]) + ) + + # --- sensors_battery + + @unittest.skipIf(not HAS_BATTERY, "no battery") + def test_sensors_battery(self): + out = sh("pmset -g batt") + percent = re.search(r"(\d+)%", out).group(1) + drawing_from = re.search("Now drawing from '([^']+)'", out).group(1) + power_plugged = drawing_from == "AC Power" + psutil_result = psutil.sensors_battery() + self.assertEqual(psutil_result.power_plugged, power_plugged) + self.assertEqual(psutil_result.percent, int(percent)) + + +if __name__ == '__main__': + from psutil.tests.runner import run_from_name + + run_from_name(__file__) diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_osx.pyc b/addon/globalPlugins/soundmanager/psutil/tests/test_osx.pyc deleted file mode 100644 index 82ecb47..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/tests/test_osx.pyc and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_posix.py b/addon/globalPlugins/soundmanager/psutil/tests/test_posix.py index d24abad..beae8eb 100644 --- a/addon/globalPlugins/soundmanager/psutil/tests/test_posix.py +++ b/addon/globalPlugins/soundmanager/psutil/tests/test_posix.py @@ -1,440 +1,493 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -"""POSIX specific tests.""" - -import datetime -import errno -import os -import re -import subprocess -import time - -import psutil -from psutil import AIX -from psutil import BSD -from psutil import LINUX -from psutil import MACOS -from psutil import OPENBSD -from psutil import POSIX -from psutil import SUNOS -from psutil.tests import APPVEYOR -from psutil.tests import get_kernel_version -from psutil.tests import get_test_subprocess -from psutil.tests import HAS_NET_IO_COUNTERS -from psutil.tests import mock -from psutil.tests import PYTHON_EXE -from psutil.tests import reap_children -from psutil.tests import retry_on_failure -from psutil.tests import sh -from psutil.tests import skip_on_access_denied -from psutil.tests import TRAVIS -from psutil.tests import unittest -from psutil.tests import wait_for_pid -from psutil.tests import which - - -def ps(fmt, pid=None): - """ - Wrapper for calling the ps command with a little bit of cross-platform - support for a narrow range of features. - """ - - cmd = ['ps'] - - if LINUX: - cmd.append('--no-headers') - - if pid is not None: - cmd.extend(['-p', str(pid)]) - else: - if SUNOS: - cmd.append('-A') - else: - cmd.append('ax') - - if SUNOS: - fmt_map = {'command', 'comm', - 'start', 'stime'} - fmt = fmt_map.get(fmt, fmt) - - cmd.extend(['-o', fmt]) - - output = sh(cmd) - - if LINUX: - output = output.splitlines() - else: - output = output.splitlines()[1:] - - all_output = [] - for line in output: - line = line.strip() - - try: - line = int(line) - except ValueError: - pass - - all_output.append(line) - - if pid is None: - return all_output - else: - return all_output[0] - -# ps "-o" field names differ wildly between platforms. -# "comm" means "only executable name" but is not available on BSD platforms. -# "args" means "command with all its arguments", and is also not available -# on BSD platforms. -# "command" is like "args" on most platforms, but like "comm" on AIX, -# and not available on SUNOS. -# so for the executable name we can use "comm" on Solaris and split "command" -# on other platforms. -# to get the cmdline (with args) we have to use "args" on AIX and -# Solaris, and can use "command" on all others. - - -def ps_name(pid): - field = "command" - if SUNOS: - field = "comm" - return ps(field, pid).split()[0] - - -def ps_args(pid): - field = "command" - if AIX or SUNOS: - field = "args" - return ps(field, pid) - - -@unittest.skipIf(not POSIX, "POSIX only") -class TestProcess(unittest.TestCase): - """Compare psutil results against 'ps' command line utility (mainly).""" - - @classmethod - def setUpClass(cls): - cls.pid = get_test_subprocess([PYTHON_EXE, "-E", "-O"], - stdin=subprocess.PIPE).pid - wait_for_pid(cls.pid) - - @classmethod - def tearDownClass(cls): - reap_children() - - def test_ppid(self): - ppid_ps = ps('ppid', self.pid) - ppid_psutil = psutil.Process(self.pid).ppid() - self.assertEqual(ppid_ps, ppid_psutil) - - def test_uid(self): - uid_ps = ps('uid', self.pid) - uid_psutil = psutil.Process(self.pid).uids().real - self.assertEqual(uid_ps, uid_psutil) - - def test_gid(self): - gid_ps = ps('rgid', self.pid) - gid_psutil = psutil.Process(self.pid).gids().real - self.assertEqual(gid_ps, gid_psutil) - - def test_username(self): - username_ps = ps('user', self.pid) - username_psutil = psutil.Process(self.pid).username() - self.assertEqual(username_ps, username_psutil) - - def test_username_no_resolution(self): - # Emulate a case where the system can't resolve the uid to - # a username in which case psutil is supposed to return - # the stringified uid. - p = psutil.Process() - with mock.patch("psutil.pwd.getpwuid", side_effect=KeyError) as fun: - self.assertEqual(p.username(), str(p.uids().real)) - assert fun.called - - @skip_on_access_denied() - @retry_on_failure() - def test_rss_memory(self): - # give python interpreter some time to properly initialize - # so that the results are the same - time.sleep(0.1) - rss_ps = ps('rss', self.pid) - rss_psutil = psutil.Process(self.pid).memory_info()[0] / 1024 - self.assertEqual(rss_ps, rss_psutil) - - @skip_on_access_denied() - @retry_on_failure() - def test_vsz_memory(self): - # give python interpreter some time to properly initialize - # so that the results are the same - time.sleep(0.1) - vsz_ps = ps('vsz', self.pid) - vsz_psutil = psutil.Process(self.pid).memory_info()[1] / 1024 - self.assertEqual(vsz_ps, vsz_psutil) - - def test_name(self): - name_ps = ps_name(self.pid) - # remove path if there is any, from the command - name_ps = os.path.basename(name_ps).lower() - name_psutil = psutil.Process(self.pid).name().lower() - # ...because of how we calculate PYTHON_EXE; on MACOS this may - # be "pythonX.Y". - name_ps = re.sub(r"\d.\d", "", name_ps) - name_psutil = re.sub(r"\d.\d", "", name_psutil) - # ...may also be "python.X" - name_ps = re.sub(r"\d", "", name_ps) - name_psutil = re.sub(r"\d", "", name_psutil) - self.assertEqual(name_ps, name_psutil) - - def test_name_long(self): - # On UNIX the kernel truncates the name to the first 15 - # characters. In such a case psutil tries to determine the - # full name from the cmdline. - name = "long-program-name" - cmdline = ["long-program-name-extended", "foo", "bar"] - with mock.patch("psutil._psplatform.Process.name", - return_value=name): - with mock.patch("psutil._psplatform.Process.cmdline", - return_value=cmdline): - p = psutil.Process() - self.assertEqual(p.name(), "long-program-name-extended") - - def test_name_long_cmdline_ad_exc(self): - # Same as above but emulates a case where cmdline() raises - # AccessDenied in which case psutil is supposed to return - # the truncated name instead of crashing. - name = "long-program-name" - with mock.patch("psutil._psplatform.Process.name", - return_value=name): - with mock.patch("psutil._psplatform.Process.cmdline", - side_effect=psutil.AccessDenied(0, "")): - p = psutil.Process() - self.assertEqual(p.name(), "long-program-name") - - def test_name_long_cmdline_nsp_exc(self): - # Same as above but emulates a case where cmdline() raises NSP - # which is supposed to propagate. - name = "long-program-name" - with mock.patch("psutil._psplatform.Process.name", - return_value=name): - with mock.patch("psutil._psplatform.Process.cmdline", - side_effect=psutil.NoSuchProcess(0, "")): - p = psutil.Process() - self.assertRaises(psutil.NoSuchProcess, p.name) - - @unittest.skipIf(MACOS or BSD, 'ps -o start not available') - def test_create_time(self): - time_ps = ps('start', self.pid) - time_psutil = psutil.Process(self.pid).create_time() - time_psutil_tstamp = datetime.datetime.fromtimestamp( - time_psutil).strftime("%H:%M:%S") - # sometimes ps shows the time rounded up instead of down, so we check - # for both possible values - round_time_psutil = round(time_psutil) - round_time_psutil_tstamp = datetime.datetime.fromtimestamp( - round_time_psutil).strftime("%H:%M:%S") - self.assertIn(time_ps, [time_psutil_tstamp, round_time_psutil_tstamp]) - - def test_exe(self): - ps_pathname = ps_name(self.pid) - psutil_pathname = psutil.Process(self.pid).exe() - try: - self.assertEqual(ps_pathname, psutil_pathname) - except AssertionError: - # certain platforms such as BSD are more accurate returning: - # "/usr/local/bin/python2.7" - # ...instead of: - # "/usr/local/bin/python" - # We do not want to consider this difference in accuracy - # an error. - adjusted_ps_pathname = ps_pathname[:len(ps_pathname)] - self.assertEqual(ps_pathname, adjusted_ps_pathname) - - def test_cmdline(self): - ps_cmdline = ps_args(self.pid) - psutil_cmdline = " ".join(psutil.Process(self.pid).cmdline()) - self.assertEqual(ps_cmdline, psutil_cmdline) - - # On SUNOS "ps" reads niceness /proc/pid/psinfo which returns an - # incorrect value (20); the real deal is getpriority(2) which - # returns 0; psutil relies on it, see: - # https://github.com/giampaolo/psutil/issues/1082 - # AIX has the same issue - @unittest.skipIf(SUNOS, "not reliable on SUNOS") - @unittest.skipIf(AIX, "not reliable on AIX") - def test_nice(self): - ps_nice = ps('nice', self.pid) - psutil_nice = psutil.Process().nice() - self.assertEqual(ps_nice, psutil_nice) - - def test_num_fds(self): - # Note: this fails from time to time; I'm keen on thinking - # it doesn't mean something is broken - def call(p, attr): - args = () - attr = getattr(p, name, None) - if attr is not None and callable(attr): - if name == 'rlimit': - args = (psutil.RLIMIT_NOFILE,) - attr(*args) - else: - attr - - p = psutil.Process(os.getpid()) - failures = [] - ignored_names = ['terminate', 'kill', 'suspend', 'resume', 'nice', - 'send_signal', 'wait', 'children', 'as_dict', - 'memory_info_ex', 'parent', 'parents'] - if LINUX and get_kernel_version() < (2, 6, 36): - ignored_names.append('rlimit') - if LINUX and get_kernel_version() < (2, 6, 23): - ignored_names.append('num_ctx_switches') - for name in dir(psutil.Process): - if (name.startswith('_') or name in ignored_names): - continue - else: - try: - num1 = p.num_fds() - for x in range(2): - call(p, name) - num2 = p.num_fds() - except psutil.AccessDenied: - pass - else: - if abs(num2 - num1) > 1: - fail = "failure while processing Process.%s method " \ - "(before=%s, after=%s)" % (name, num1, num2) - failures.append(fail) - if failures: - self.fail('\n' + '\n'.join(failures)) - - -@unittest.skipIf(not POSIX, "POSIX only") -class TestSystemAPIs(unittest.TestCase): - """Test some system APIs.""" - - @retry_on_failure() - def test_pids(self): - # Note: this test might fail if the OS is starting/killing - # other processes in the meantime - pids_ps = sorted(ps("pid")) - pids_psutil = psutil.pids() - - # on MACOS and OPENBSD ps doesn't show pid 0 - if MACOS or OPENBSD and 0 not in pids_ps: - pids_ps.insert(0, 0) - - # There will often be one more process in pids_ps for ps itself - if len(pids_ps) - len(pids_psutil) > 1: - difference = [x for x in pids_psutil if x not in pids_ps] + \ - [x for x in pids_ps if x not in pids_psutil] - self.fail("difference: " + str(difference)) - - # for some reason ifconfig -a does not report all interfaces - # returned by psutil - @unittest.skipIf(SUNOS, "unreliable on SUNOS") - @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") - @unittest.skipIf(not which('ifconfig'), "no ifconfig cmd") - @unittest.skipIf(not HAS_NET_IO_COUNTERS, "not supported") - def test_nic_names(self): - output = sh("ifconfig -a") - for nic in psutil.net_io_counters(pernic=True).keys(): - for line in output.split(): - if line.startswith(nic): - break - else: - self.fail( - "couldn't find %s nic in 'ifconfig -a' output\n%s" % ( - nic, output)) - - # can't find users on APPVEYOR or TRAVIS - @unittest.skipIf(APPVEYOR or TRAVIS and not psutil.users(), - "unreliable on APPVEYOR or TRAVIS") - @retry_on_failure() - def test_users(self): - out = sh("who") - lines = out.split('\n') - users = [x.split()[0] for x in lines] - terminals = [x.split()[1] for x in lines] - self.assertEqual(len(users), len(psutil.users())) - for u in psutil.users(): - self.assertIn(u.name, users) - self.assertIn(u.terminal, terminals) - - def test_pid_exists_let_raise(self): - # According to "man 2 kill" possible error values for kill - # are (EINVAL, EPERM, ESRCH). Test that any other errno - # results in an exception. - with mock.patch("psutil._psposix.os.kill", - side_effect=OSError(errno.EBADF, "")) as m: - self.assertRaises(OSError, psutil._psposix.pid_exists, os.getpid()) - assert m.called - - def test_os_waitpid_let_raise(self): - # os.waitpid() is supposed to catch EINTR and ECHILD only. - # Test that any other errno results in an exception. - with mock.patch("psutil._psposix.os.waitpid", - side_effect=OSError(errno.EBADF, "")) as m: - self.assertRaises(OSError, psutil._psposix.wait_pid, os.getpid()) - assert m.called - - def test_os_waitpid_eintr(self): - # os.waitpid() is supposed to "retry" on EINTR. - with mock.patch("psutil._psposix.os.waitpid", - side_effect=OSError(errno.EINTR, "")) as m: - self.assertRaises( - psutil._psposix.TimeoutExpired, - psutil._psposix.wait_pid, os.getpid(), timeout=0.01) - assert m.called - - def test_os_waitpid_bad_ret_status(self): - # Simulate os.waitpid() returning a bad status. - with mock.patch("psutil._psposix.os.waitpid", - return_value=(1, -1)) as m: - self.assertRaises(ValueError, - psutil._psposix.wait_pid, os.getpid()) - assert m.called - - # AIX can return '-' in df output instead of numbers, e.g. for /proc - @unittest.skipIf(AIX, "unreliable on AIX") - def test_disk_usage(self): - def df(device): - out = sh("df -k %s" % device).strip() - line = out.split('\n')[1] - fields = line.split() - total = int(fields[1]) * 1024 - used = int(fields[2]) * 1024 - free = int(fields[3]) * 1024 - percent = float(fields[4].replace('%', '')) - return (total, used, free, percent) - - tolerance = 4 * 1024 * 1024 # 4MB - for part in psutil.disk_partitions(all=False): - usage = psutil.disk_usage(part.mountpoint) - try: - total, used, free, percent = df(part.device) - except RuntimeError as err: - # see: - # https://travis-ci.org/giampaolo/psutil/jobs/138338464 - # https://travis-ci.org/giampaolo/psutil/jobs/138343361 - err = str(err).lower() - if "no such file or directory" in err or \ - "raw devices not supported" in err or \ - "permission denied" in err: - continue - else: - raise - else: - self.assertAlmostEqual(usage.total, total, delta=tolerance) - self.assertAlmostEqual(usage.used, used, delta=tolerance) - self.assertAlmostEqual(usage.free, free, delta=tolerance) - self.assertAlmostEqual(usage.percent, percent, delta=1) - - -if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""POSIX specific tests.""" + +import datetime +import errno +import os +import re +import subprocess +import time +import unittest + +import psutil +from psutil import AIX +from psutil import BSD +from psutil import LINUX +from psutil import MACOS +from psutil import OPENBSD +from psutil import POSIX +from psutil import SUNOS +from psutil.tests import HAS_NET_IO_COUNTERS +from psutil.tests import PYTHON_EXE +from psutil.tests import PsutilTestCase +from psutil.tests import mock +from psutil.tests import retry_on_failure +from psutil.tests import sh +from psutil.tests import skip_on_access_denied +from psutil.tests import spawn_testproc +from psutil.tests import terminate +from psutil.tests import which + + +if POSIX: + import mmap + import resource + + from psutil._psutil_posix import getpagesize + + +def ps(fmt, pid=None): + """Wrapper for calling the ps command with a little bit of cross-platform + support for a narrow range of features. + """ + + cmd = ['ps'] + + if LINUX: + cmd.append('--no-headers') + + if pid is not None: + cmd.extend(['-p', str(pid)]) + else: + if SUNOS or AIX: + cmd.append('-A') + else: + cmd.append('ax') + + if SUNOS: + fmt = fmt.replace("start", "stime") + + cmd.extend(['-o', fmt]) + + output = sh(cmd) + + output = output.splitlines() if LINUX else output.splitlines()[1:] + + all_output = [] + for line in output: + line = line.strip() + + try: + line = int(line) + except ValueError: + pass + + all_output.append(line) + + if pid is None: + return all_output + else: + return all_output[0] + + +# ps "-o" field names differ wildly between platforms. +# "comm" means "only executable name" but is not available on BSD platforms. +# "args" means "command with all its arguments", and is also not available +# on BSD platforms. +# "command" is like "args" on most platforms, but like "comm" on AIX, +# and not available on SUNOS. +# so for the executable name we can use "comm" on Solaris and split "command" +# on other platforms. +# to get the cmdline (with args) we have to use "args" on AIX and +# Solaris, and can use "command" on all others. + + +def ps_name(pid): + field = "command" + if SUNOS: + field = "comm" + return ps(field, pid).split()[0] + + +def ps_args(pid): + field = "command" + if AIX or SUNOS: + field = "args" + out = ps(field, pid) + # observed on BSD + Github CI: '/usr/local/bin/python3 -E -O (python3.9)' + out = re.sub(r"\(python.*?\)$", "", out) + return out.strip() + + +def ps_rss(pid): + field = "rss" + if AIX: + field = "rssize" + return ps(field, pid) + + +def ps_vsz(pid): + field = "vsz" + if AIX: + field = "vsize" + return ps(field, pid) + + +@unittest.skipIf(not POSIX, "POSIX only") +class TestProcess(PsutilTestCase): + """Compare psutil results against 'ps' command line utility (mainly).""" + + @classmethod + def setUpClass(cls): + cls.pid = spawn_testproc( + [PYTHON_EXE, "-E", "-O"], stdin=subprocess.PIPE + ).pid + + @classmethod + def tearDownClass(cls): + terminate(cls.pid) + + def test_ppid(self): + ppid_ps = ps('ppid', self.pid) + ppid_psutil = psutil.Process(self.pid).ppid() + self.assertEqual(ppid_ps, ppid_psutil) + + def test_uid(self): + uid_ps = ps('uid', self.pid) + uid_psutil = psutil.Process(self.pid).uids().real + self.assertEqual(uid_ps, uid_psutil) + + def test_gid(self): + gid_ps = ps('rgid', self.pid) + gid_psutil = psutil.Process(self.pid).gids().real + self.assertEqual(gid_ps, gid_psutil) + + def test_username(self): + username_ps = ps('user', self.pid) + username_psutil = psutil.Process(self.pid).username() + self.assertEqual(username_ps, username_psutil) + + def test_username_no_resolution(self): + # Emulate a case where the system can't resolve the uid to + # a username in which case psutil is supposed to return + # the stringified uid. + p = psutil.Process() + with mock.patch("psutil.pwd.getpwuid", side_effect=KeyError) as fun: + self.assertEqual(p.username(), str(p.uids().real)) + assert fun.called + + @skip_on_access_denied() + @retry_on_failure() + def test_rss_memory(self): + # give python interpreter some time to properly initialize + # so that the results are the same + time.sleep(0.1) + rss_ps = ps_rss(self.pid) + rss_psutil = psutil.Process(self.pid).memory_info()[0] / 1024 + self.assertEqual(rss_ps, rss_psutil) + + @skip_on_access_denied() + @retry_on_failure() + def test_vsz_memory(self): + # give python interpreter some time to properly initialize + # so that the results are the same + time.sleep(0.1) + vsz_ps = ps_vsz(self.pid) + vsz_psutil = psutil.Process(self.pid).memory_info()[1] / 1024 + self.assertEqual(vsz_ps, vsz_psutil) + + def test_name(self): + name_ps = ps_name(self.pid) + # remove path if there is any, from the command + name_ps = os.path.basename(name_ps).lower() + name_psutil = psutil.Process(self.pid).name().lower() + # ...because of how we calculate PYTHON_EXE; on MACOS this may + # be "pythonX.Y". + name_ps = re.sub(r"\d.\d", "", name_ps) + name_psutil = re.sub(r"\d.\d", "", name_psutil) + # ...may also be "python.X" + name_ps = re.sub(r"\d", "", name_ps) + name_psutil = re.sub(r"\d", "", name_psutil) + self.assertEqual(name_ps, name_psutil) + + def test_name_long(self): + # On UNIX the kernel truncates the name to the first 15 + # characters. In such a case psutil tries to determine the + # full name from the cmdline. + name = "long-program-name" + cmdline = ["long-program-name-extended", "foo", "bar"] + with mock.patch("psutil._psplatform.Process.name", return_value=name): + with mock.patch( + "psutil._psplatform.Process.cmdline", return_value=cmdline + ): + p = psutil.Process() + self.assertEqual(p.name(), "long-program-name-extended") + + def test_name_long_cmdline_ad_exc(self): + # Same as above but emulates a case where cmdline() raises + # AccessDenied in which case psutil is supposed to return + # the truncated name instead of crashing. + name = "long-program-name" + with mock.patch("psutil._psplatform.Process.name", return_value=name): + with mock.patch( + "psutil._psplatform.Process.cmdline", + side_effect=psutil.AccessDenied(0, ""), + ): + p = psutil.Process() + self.assertEqual(p.name(), "long-program-name") + + def test_name_long_cmdline_nsp_exc(self): + # Same as above but emulates a case where cmdline() raises NSP + # which is supposed to propagate. + name = "long-program-name" + with mock.patch("psutil._psplatform.Process.name", return_value=name): + with mock.patch( + "psutil._psplatform.Process.cmdline", + side_effect=psutil.NoSuchProcess(0, ""), + ): + p = psutil.Process() + self.assertRaises(psutil.NoSuchProcess, p.name) + + @unittest.skipIf(MACOS or BSD, 'ps -o start not available') + def test_create_time(self): + time_ps = ps('start', self.pid) + time_psutil = psutil.Process(self.pid).create_time() + time_psutil_tstamp = datetime.datetime.fromtimestamp( + time_psutil + ).strftime("%H:%M:%S") + # sometimes ps shows the time rounded up instead of down, so we check + # for both possible values + round_time_psutil = round(time_psutil) + round_time_psutil_tstamp = datetime.datetime.fromtimestamp( + round_time_psutil + ).strftime("%H:%M:%S") + self.assertIn(time_ps, [time_psutil_tstamp, round_time_psutil_tstamp]) + + def test_exe(self): + ps_pathname = ps_name(self.pid) + psutil_pathname = psutil.Process(self.pid).exe() + try: + self.assertEqual(ps_pathname, psutil_pathname) + except AssertionError: + # certain platforms such as BSD are more accurate returning: + # "/usr/local/bin/python2.7" + # ...instead of: + # "/usr/local/bin/python" + # We do not want to consider this difference in accuracy + # an error. + adjusted_ps_pathname = ps_pathname[: len(ps_pathname)] + self.assertEqual(ps_pathname, adjusted_ps_pathname) + + # On macOS the official python installer exposes a python wrapper that + # executes a python executable hidden inside an application bundle inside + # the Python framework. + # There's a race condition between the ps call & the psutil call below + # depending on the completion of the execve call so let's retry on failure + @retry_on_failure() + def test_cmdline(self): + ps_cmdline = ps_args(self.pid) + psutil_cmdline = " ".join(psutil.Process(self.pid).cmdline()) + self.assertEqual(ps_cmdline, psutil_cmdline) + + # On SUNOS "ps" reads niceness /proc/pid/psinfo which returns an + # incorrect value (20); the real deal is getpriority(2) which + # returns 0; psutil relies on it, see: + # https://github.com/giampaolo/psutil/issues/1082 + # AIX has the same issue + @unittest.skipIf(SUNOS, "not reliable on SUNOS") + @unittest.skipIf(AIX, "not reliable on AIX") + def test_nice(self): + ps_nice = ps('nice', self.pid) + psutil_nice = psutil.Process().nice() + self.assertEqual(ps_nice, psutil_nice) + + +@unittest.skipIf(not POSIX, "POSIX only") +class TestSystemAPIs(PsutilTestCase): + """Test some system APIs.""" + + @retry_on_failure() + def test_pids(self): + # Note: this test might fail if the OS is starting/killing + # other processes in the meantime + pids_ps = sorted(ps("pid")) + pids_psutil = psutil.pids() + + # on MACOS and OPENBSD ps doesn't show pid 0 + if MACOS or OPENBSD and 0 not in pids_ps: + pids_ps.insert(0, 0) + + # There will often be one more process in pids_ps for ps itself + if len(pids_ps) - len(pids_psutil) > 1: + difference = [x for x in pids_psutil if x not in pids_ps] + [ + x for x in pids_ps if x not in pids_psutil + ] + raise self.fail("difference: " + str(difference)) + + # for some reason ifconfig -a does not report all interfaces + # returned by psutil + @unittest.skipIf(SUNOS, "unreliable on SUNOS") + @unittest.skipIf(not which('ifconfig'), "no ifconfig cmd") + @unittest.skipIf(not HAS_NET_IO_COUNTERS, "not supported") + def test_nic_names(self): + output = sh("ifconfig -a") + for nic in psutil.net_io_counters(pernic=True): + for line in output.split(): + if line.startswith(nic): + break + else: + raise self.fail( + "couldn't find %s nic in 'ifconfig -a' output\n%s" + % (nic, output) + ) + + # @unittest.skipIf(CI_TESTING and not psutil.users(), "unreliable on CI") + @retry_on_failure() + def test_users(self): + out = sh("who -u") + if not out.strip(): + raise self.skipTest("no users on this system") + lines = out.split('\n') + users = [x.split()[0] for x in lines] + terminals = [x.split()[1] for x in lines] + self.assertEqual(len(users), len(psutil.users())) + with self.subTest(psutil=psutil.users(), who=out): + for idx, u in enumerate(psutil.users()): + self.assertEqual(u.name, users[idx]) + self.assertEqual(u.terminal, terminals[idx]) + if u.pid is not None: # None on OpenBSD + psutil.Process(u.pid) + + @retry_on_failure() + def test_users_started(self): + out = sh("who -u") + if not out.strip(): + raise self.skipTest("no users on this system") + tstamp = None + # '2023-04-11 09:31' (Linux) + started = re.findall(r"\d\d\d\d-\d\d-\d\d \d\d:\d\d", out) + if started: + tstamp = "%Y-%m-%d %H:%M" + else: + # 'Apr 10 22:27' (macOS) + started = re.findall(r"[A-Z][a-z][a-z] \d\d \d\d:\d\d", out) + if started: + tstamp = "%b %d %H:%M" + else: + # 'Apr 10' + started = re.findall(r"[A-Z][a-z][a-z] \d\d", out) + if started: + tstamp = "%b %d" + else: + # 'apr 10' (sunOS) + started = re.findall(r"[a-z][a-z][a-z] \d\d", out) + if started: + tstamp = "%b %d" + started = [x.capitalize() for x in started] + + if not tstamp: + raise unittest.SkipTest( + "cannot interpret tstamp in who output\n%s" % (out) + ) + + with self.subTest(psutil=psutil.users(), who=out): + for idx, u in enumerate(psutil.users()): + psutil_value = datetime.datetime.fromtimestamp( + u.started + ).strftime(tstamp) + self.assertEqual(psutil_value, started[idx]) + + def test_pid_exists_let_raise(self): + # According to "man 2 kill" possible error values for kill + # are (EINVAL, EPERM, ESRCH). Test that any other errno + # results in an exception. + with mock.patch( + "psutil._psposix.os.kill", side_effect=OSError(errno.EBADF, "") + ) as m: + self.assertRaises(OSError, psutil._psposix.pid_exists, os.getpid()) + assert m.called + + def test_os_waitpid_let_raise(self): + # os.waitpid() is supposed to catch EINTR and ECHILD only. + # Test that any other errno results in an exception. + with mock.patch( + "psutil._psposix.os.waitpid", side_effect=OSError(errno.EBADF, "") + ) as m: + self.assertRaises(OSError, psutil._psposix.wait_pid, os.getpid()) + assert m.called + + def test_os_waitpid_eintr(self): + # os.waitpid() is supposed to "retry" on EINTR. + with mock.patch( + "psutil._psposix.os.waitpid", side_effect=OSError(errno.EINTR, "") + ) as m: + self.assertRaises( + psutil._psposix.TimeoutExpired, + psutil._psposix.wait_pid, + os.getpid(), + timeout=0.01, + ) + assert m.called + + def test_os_waitpid_bad_ret_status(self): + # Simulate os.waitpid() returning a bad status. + with mock.patch( + "psutil._psposix.os.waitpid", return_value=(1, -1) + ) as m: + self.assertRaises( + ValueError, psutil._psposix.wait_pid, os.getpid() + ) + assert m.called + + # AIX can return '-' in df output instead of numbers, e.g. for /proc + @unittest.skipIf(AIX, "unreliable on AIX") + @retry_on_failure() + def test_disk_usage(self): + def df(device): + try: + out = sh("df -k %s" % device).strip() + except RuntimeError as err: + if "device busy" in str(err).lower(): + raise self.skipTest("df returned EBUSY") + raise + line = out.split('\n')[1] + fields = line.split() + total = int(fields[1]) * 1024 + used = int(fields[2]) * 1024 + free = int(fields[3]) * 1024 + percent = float(fields[4].replace('%', '')) + return (total, used, free, percent) + + tolerance = 4 * 1024 * 1024 # 4MB + for part in psutil.disk_partitions(all=False): + usage = psutil.disk_usage(part.mountpoint) + try: + total, used, free, percent = df(part.device) + except RuntimeError as err: + # see: + # https://travis-ci.org/giampaolo/psutil/jobs/138338464 + # https://travis-ci.org/giampaolo/psutil/jobs/138343361 + err = str(err).lower() + if ( + "no such file or directory" in err + or "raw devices not supported" in err + or "permission denied" in err + ): + continue + raise + else: + self.assertAlmostEqual(usage.total, total, delta=tolerance) + self.assertAlmostEqual(usage.used, used, delta=tolerance) + self.assertAlmostEqual(usage.free, free, delta=tolerance) + self.assertAlmostEqual(usage.percent, percent, delta=1) + + +@unittest.skipIf(not POSIX, "POSIX only") +class TestMisc(PsutilTestCase): + def test_getpagesize(self): + pagesize = getpagesize() + self.assertGreater(pagesize, 0) + self.assertEqual(pagesize, resource.getpagesize()) + self.assertEqual(pagesize, mmap.PAGESIZE) + + +if __name__ == '__main__': + from psutil.tests.runner import run_from_name + + run_from_name(__file__) diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_posix.pyc b/addon/globalPlugins/soundmanager/psutil/tests/test_posix.pyc deleted file mode 100644 index 9468e3a..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/tests/test_posix.pyc and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_process.py b/addon/globalPlugins/soundmanager/psutil/tests/test_process.py index a4b738e..d74e2a8 100644 --- a/addon/globalPlugins/soundmanager/psutil/tests/test_process.py +++ b/addon/globalPlugins/soundmanager/psutil/tests/test_process.py @@ -1,1612 +1,1636 @@ -#!/usr/bin/env python - -# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -"""Tests for psutil.Process class.""" - -import collections -import errno -import getpass -import itertools -import os -import signal -import socket -import subprocess -import sys -import tempfile -import textwrap -import time -import types - -import psutil - -from psutil import AIX -from psutil import BSD -from psutil import LINUX -from psutil import MACOS -from psutil import NETBSD -from psutil import OPENBSD -from psutil import OSX -from psutil import POSIX -from psutil import SUNOS -from psutil import WINDOWS -from psutil._common import open_text -from psutil._compat import long -from psutil._compat import PY3 -from psutil.tests import APPVEYOR -from psutil.tests import call_until -from psutil.tests import copyload_shared_lib -from psutil.tests import create_exe -from psutil.tests import create_proc_children_pair -from psutil.tests import create_zombie_proc -from psutil.tests import enum -from psutil.tests import get_test_subprocess -from psutil.tests import HAS_CPU_AFFINITY -from psutil.tests import HAS_ENVIRON -from psutil.tests import HAS_IONICE -from psutil.tests import HAS_MEMORY_MAPS -from psutil.tests import HAS_PROC_CPU_NUM -from psutil.tests import HAS_PROC_IO_COUNTERS -from psutil.tests import HAS_RLIMIT -from psutil.tests import HAS_THREADS -from psutil.tests import mock -from psutil.tests import PYPY -from psutil.tests import PYTHON_EXE -from psutil.tests import reap_children -from psutil.tests import retry_on_failure -from psutil.tests import safe_rmpath -from psutil.tests import sh -from psutil.tests import skip_on_access_denied -from psutil.tests import skip_on_not_implemented -from psutil.tests import TESTFILE_PREFIX -from psutil.tests import TESTFN -from psutil.tests import ThreadTask -from psutil.tests import TRAVIS -from psutil.tests import unittest -from psutil.tests import wait_for_pid - - -# =================================================================== -# --- psutil.Process class tests -# =================================================================== - -class TestProcess(unittest.TestCase): - """Tests for psutil.Process class.""" - - def setUp(self): - safe_rmpath(TESTFN) - - def tearDown(self): - reap_children() - - def test_pid(self): - p = psutil.Process() - self.assertEqual(p.pid, os.getpid()) - sproc = get_test_subprocess() - self.assertEqual(psutil.Process(sproc.pid).pid, sproc.pid) - with self.assertRaises(AttributeError): - p.pid = 33 - - def test_kill(self): - sproc = get_test_subprocess() - test_pid = sproc.pid - p = psutil.Process(test_pid) - p.kill() - sig = p.wait() - self.assertFalse(psutil.pid_exists(test_pid)) - if POSIX: - self.assertEqual(sig, -signal.SIGKILL) - - def test_terminate(self): - sproc = get_test_subprocess() - test_pid = sproc.pid - p = psutil.Process(test_pid) - p.terminate() - sig = p.wait() - self.assertFalse(psutil.pid_exists(test_pid)) - if POSIX: - self.assertEqual(sig, -signal.SIGTERM) - - def test_send_signal(self): - sig = signal.SIGKILL if POSIX else signal.SIGTERM - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - p.send_signal(sig) - exit_sig = p.wait() - self.assertFalse(psutil.pid_exists(p.pid)) - if POSIX: - self.assertEqual(exit_sig, -sig) - # - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - p.send_signal(sig) - with mock.patch('psutil.os.kill', - side_effect=OSError(errno.ESRCH, "")): - with self.assertRaises(psutil.NoSuchProcess): - p.send_signal(sig) - # - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - p.send_signal(sig) - with mock.patch('psutil.os.kill', - side_effect=OSError(errno.EPERM, "")): - with self.assertRaises(psutil.AccessDenied): - psutil.Process().send_signal(sig) - # Sending a signal to process with PID 0 is not allowed as - # it would affect every process in the process group of - # the calling process (os.getpid()) instead of PID 0"). - if 0 in psutil.pids(): - p = psutil.Process(0) - self.assertRaises(ValueError, p.send_signal, signal.SIGTERM) - - def test_wait(self): - # check exit code signal - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - p.kill() - code = p.wait() - if POSIX: - self.assertEqual(code, -signal.SIGKILL) - else: - self.assertEqual(code, signal.SIGTERM) - self.assertFalse(p.is_running()) - - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - p.terminate() - code = p.wait() - if POSIX: - self.assertEqual(code, -signal.SIGTERM) - else: - self.assertEqual(code, signal.SIGTERM) - self.assertFalse(p.is_running()) - - # check sys.exit() code - code = "import time, sys; time.sleep(0.01); sys.exit(5);" - sproc = get_test_subprocess([PYTHON_EXE, "-c", code]) - p = psutil.Process(sproc.pid) - self.assertEqual(p.wait(), 5) - self.assertFalse(p.is_running()) - - # Test wait() issued twice. - # It is not supposed to raise NSP when the process is gone. - # On UNIX this should return None, on Windows it should keep - # returning the exit code. - sproc = get_test_subprocess([PYTHON_EXE, "-c", code]) - p = psutil.Process(sproc.pid) - self.assertEqual(p.wait(), 5) - self.assertIn(p.wait(), (5, None)) - - # test timeout - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - p.name() - self.assertRaises(psutil.TimeoutExpired, p.wait, 0.01) - - # timeout < 0 not allowed - self.assertRaises(ValueError, p.wait, -1) - - def test_wait_non_children(self): - # Test wait() against a process which is not our direct - # child. - p1, p2 = create_proc_children_pair() - self.assertRaises(psutil.TimeoutExpired, p1.wait, 0.01) - self.assertRaises(psutil.TimeoutExpired, p2.wait, 0.01) - # We also terminate the direct child otherwise the - # grandchild will hang until the parent is gone. - p1.terminate() - p2.terminate() - ret1 = p1.wait() - ret2 = p2.wait() - if POSIX: - self.assertEqual(ret1, -signal.SIGTERM) - # For processes which are not our children we're supposed - # to get None. - self.assertEqual(ret2, None) - else: - self.assertEqual(ret1, signal.SIGTERM) - self.assertEqual(ret1, signal.SIGTERM) - - def test_wait_timeout_0(self): - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - self.assertRaises(psutil.TimeoutExpired, p.wait, 0) - p.kill() - stop_at = time.time() + 2 - while True: - try: - code = p.wait(0) - except psutil.TimeoutExpired: - if time.time() >= stop_at: - raise - else: - break - if POSIX: - self.assertEqual(code, -signal.SIGKILL) - else: - self.assertEqual(code, signal.SIGTERM) - self.assertFalse(p.is_running()) - - def test_cpu_percent(self): - p = psutil.Process() - p.cpu_percent(interval=0.001) - p.cpu_percent(interval=0.001) - for x in range(100): - percent = p.cpu_percent(interval=None) - self.assertIsInstance(percent, float) - self.assertGreaterEqual(percent, 0.0) - with self.assertRaises(ValueError): - p.cpu_percent(interval=-1) - - def test_cpu_percent_numcpus_none(self): - # See: https://github.com/giampaolo/psutil/issues/1087 - with mock.patch('psutil.cpu_count', return_value=None) as m: - psutil.Process().cpu_percent() - assert m.called - - def test_cpu_times(self): - times = psutil.Process().cpu_times() - assert (times.user > 0.0) or (times.system > 0.0), times - assert (times.children_user >= 0.0), times - assert (times.children_system >= 0.0), times - # make sure returned values can be pretty printed with strftime - for name in times._fields: - time.strftime("%H:%M:%S", time.localtime(getattr(times, name))) - - def test_cpu_times_2(self): - user_time, kernel_time = psutil.Process().cpu_times()[:2] - utime, ktime = os.times()[:2] - - # Use os.times()[:2] as base values to compare our results - # using a tolerance of +/- 0.1 seconds. - # It will fail if the difference between the values is > 0.1s. - if (max([user_time, utime]) - min([user_time, utime])) > 0.1: - self.fail("expected: %s, found: %s" % (utime, user_time)) - - if (max([kernel_time, ktime]) - min([kernel_time, ktime])) > 0.1: - self.fail("expected: %s, found: %s" % (ktime, kernel_time)) - - @unittest.skipIf(not HAS_PROC_CPU_NUM, "not supported") - def test_cpu_num(self): - p = psutil.Process() - num = p.cpu_num() - self.assertGreaterEqual(num, 0) - if psutil.cpu_count() == 1: - self.assertEqual(num, 0) - self.assertIn(p.cpu_num(), range(psutil.cpu_count())) - - def test_create_time(self): - sproc = get_test_subprocess() - now = time.time() - p = psutil.Process(sproc.pid) - create_time = p.create_time() - - # Use time.time() as base value to compare our result using a - # tolerance of +/- 1 second. - # It will fail if the difference between the values is > 2s. - difference = abs(create_time - now) - if difference > 2: - self.fail("expected: %s, found: %s, difference: %s" - % (now, create_time, difference)) - - # make sure returned value can be pretty printed with strftime - time.strftime("%Y %m %d %H:%M:%S", time.localtime(p.create_time())) - - @unittest.skipIf(not POSIX, 'POSIX only') - @unittest.skipIf(TRAVIS, 'not reliable on TRAVIS') - def test_terminal(self): - terminal = psutil.Process().terminal() - if sys.stdin.isatty() or sys.stdout.isatty(): - tty = os.path.realpath(sh('tty')) - self.assertEqual(terminal, tty) - else: - self.assertIsNone(terminal) - - @unittest.skipIf(not HAS_PROC_IO_COUNTERS, 'not supported') - @skip_on_not_implemented(only_if=LINUX) - def test_io_counters(self): - p = psutil.Process() - - # test reads - io1 = p.io_counters() - with open(PYTHON_EXE, 'rb') as f: - f.read() - io2 = p.io_counters() - if not BSD and not AIX: - self.assertGreater(io2.read_count, io1.read_count) - self.assertEqual(io2.write_count, io1.write_count) - if LINUX: - self.assertGreater(io2.read_chars, io1.read_chars) - self.assertEqual(io2.write_chars, io1.write_chars) - else: - self.assertGreaterEqual(io2.read_bytes, io1.read_bytes) - self.assertGreaterEqual(io2.write_bytes, io1.write_bytes) - - # test writes - io1 = p.io_counters() - with tempfile.TemporaryFile(prefix=TESTFILE_PREFIX) as f: - if PY3: - f.write(bytes("x" * 1000000, 'ascii')) - else: - f.write("x" * 1000000) - io2 = p.io_counters() - self.assertGreaterEqual(io2.write_count, io1.write_count) - self.assertGreaterEqual(io2.write_bytes, io1.write_bytes) - self.assertGreaterEqual(io2.read_count, io1.read_count) - self.assertGreaterEqual(io2.read_bytes, io1.read_bytes) - if LINUX: - self.assertGreater(io2.write_chars, io1.write_chars) - self.assertGreaterEqual(io2.read_chars, io1.read_chars) - - # sanity check - for i in range(len(io2)): - if BSD and i >= 2: - # On BSD read_bytes and write_bytes are always set to -1. - continue - self.assertGreaterEqual(io2[i], 0) - self.assertGreaterEqual(io2[i], 0) - - @unittest.skipIf(not HAS_IONICE, "not supported") - @unittest.skipIf(not LINUX, "linux only") - def test_ionice_linux(self): - p = psutil.Process() - self.assertEqual(p.ionice()[0], psutil.IOPRIO_CLASS_NONE) - self.assertEqual(psutil.IOPRIO_CLASS_NONE, 0) - self.assertEqual(psutil.IOPRIO_CLASS_RT, 1) # high - self.assertEqual(psutil.IOPRIO_CLASS_BE, 2) # normal - self.assertEqual(psutil.IOPRIO_CLASS_IDLE, 3) # low - try: - # low - p.ionice(psutil.IOPRIO_CLASS_IDLE) - self.assertEqual(tuple(p.ionice()), (psutil.IOPRIO_CLASS_IDLE, 0)) - with self.assertRaises(ValueError): # accepts no value - p.ionice(psutil.IOPRIO_CLASS_IDLE, value=7) - # normal - p.ionice(psutil.IOPRIO_CLASS_BE) - self.assertEqual(tuple(p.ionice()), (psutil.IOPRIO_CLASS_BE, 0)) - p.ionice(psutil.IOPRIO_CLASS_BE, value=7) - self.assertEqual(tuple(p.ionice()), (psutil.IOPRIO_CLASS_BE, 7)) - with self.assertRaises(ValueError): - p.ionice(psutil.IOPRIO_CLASS_BE, value=8) - # high - if os.getuid() == 0: # root - p.ionice(psutil.IOPRIO_CLASS_RT) - self.assertEqual(tuple(p.ionice()), - (psutil.IOPRIO_CLASS_RT, 0)) - p.ionice(psutil.IOPRIO_CLASS_RT, value=7) - self.assertEqual(tuple(p.ionice()), - (psutil.IOPRIO_CLASS_RT, 7)) - with self.assertRaises(ValueError): - p.ionice(psutil.IOPRIO_CLASS_IDLE, value=8) - # errs - self.assertRaisesRegex( - ValueError, "ioclass accepts no value", - p.ionice, psutil.IOPRIO_CLASS_NONE, 1) - self.assertRaisesRegex( - ValueError, "ioclass accepts no value", - p.ionice, psutil.IOPRIO_CLASS_IDLE, 1) - self.assertRaisesRegex( - ValueError, "'ioclass' argument must be specified", - p.ionice, value=1) - finally: - p.ionice(psutil.IOPRIO_CLASS_BE) - - @unittest.skipIf(not HAS_IONICE, "not supported") - @unittest.skipIf(not WINDOWS, 'not supported on this win version') - def test_ionice_win(self): - p = psutil.Process() - self.assertEqual(p.ionice(), psutil.IOPRIO_NORMAL) - try: - # base - p.ionice(psutil.IOPRIO_VERYLOW) - self.assertEqual(p.ionice(), psutil.IOPRIO_VERYLOW) - p.ionice(psutil.IOPRIO_LOW) - self.assertEqual(p.ionice(), psutil.IOPRIO_LOW) - try: - p.ionice(psutil.IOPRIO_HIGH) - except psutil.AccessDenied: - pass - else: - self.assertEqual(p.ionice(), psutil.IOPRIO_HIGH) - # errs - self.assertRaisesRegex( - TypeError, "value argument not accepted on Windows", - p.ionice, psutil.IOPRIO_NORMAL, value=1) - self.assertRaisesRegex( - ValueError, "is not a valid priority", - p.ionice, psutil.IOPRIO_HIGH + 1) - finally: - p.ionice(psutil.IOPRIO_NORMAL) - self.assertEqual(p.ionice(), psutil.IOPRIO_NORMAL) - - @unittest.skipIf(not HAS_RLIMIT, "not supported") - def test_rlimit_get(self): - import resource - p = psutil.Process(os.getpid()) - names = [x for x in dir(psutil) if x.startswith('RLIMIT')] - assert names, names - for name in names: - value = getattr(psutil, name) - self.assertGreaterEqual(value, 0) - if name in dir(resource): - self.assertEqual(value, getattr(resource, name)) - # XXX - On PyPy RLIMIT_INFINITY returned by - # resource.getrlimit() is reported as a very big long - # number instead of -1. It looks like a bug with PyPy. - if PYPY: - continue - self.assertEqual(p.rlimit(value), resource.getrlimit(value)) - else: - ret = p.rlimit(value) - self.assertEqual(len(ret), 2) - self.assertGreaterEqual(ret[0], -1) - self.assertGreaterEqual(ret[1], -1) - - @unittest.skipIf(not HAS_RLIMIT, "not supported") - def test_rlimit_set(self): - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - p.rlimit(psutil.RLIMIT_NOFILE, (5, 5)) - self.assertEqual(p.rlimit(psutil.RLIMIT_NOFILE), (5, 5)) - # If pid is 0 prlimit() applies to the calling process and - # we don't want that. - with self.assertRaises(ValueError): - psutil._psplatform.Process(0).rlimit(0) - with self.assertRaises(ValueError): - p.rlimit(psutil.RLIMIT_NOFILE, (5, 5, 5)) - - @unittest.skipIf(not HAS_RLIMIT, "not supported") - def test_rlimit(self): - p = psutil.Process() - soft, hard = p.rlimit(psutil.RLIMIT_FSIZE) - try: - p.rlimit(psutil.RLIMIT_FSIZE, (1024, hard)) - with open(TESTFN, "wb") as f: - f.write(b"X" * 1024) - # write() or flush() doesn't always cause the exception - # but close() will. - with self.assertRaises(IOError) as exc: - with open(TESTFN, "wb") as f: - f.write(b"X" * 1025) - self.assertEqual(exc.exception.errno if PY3 else exc.exception[0], - errno.EFBIG) - finally: - p.rlimit(psutil.RLIMIT_FSIZE, (soft, hard)) - self.assertEqual(p.rlimit(psutil.RLIMIT_FSIZE), (soft, hard)) - - @unittest.skipIf(not HAS_RLIMIT, "not supported") - def test_rlimit_infinity(self): - # First set a limit, then re-set it by specifying INFINITY - # and assume we overridden the previous limit. - p = psutil.Process() - soft, hard = p.rlimit(psutil.RLIMIT_FSIZE) - try: - p.rlimit(psutil.RLIMIT_FSIZE, (1024, hard)) - p.rlimit(psutil.RLIMIT_FSIZE, (psutil.RLIM_INFINITY, hard)) - with open(TESTFN, "wb") as f: - f.write(b"X" * 2048) - finally: - p.rlimit(psutil.RLIMIT_FSIZE, (soft, hard)) - self.assertEqual(p.rlimit(psutil.RLIMIT_FSIZE), (soft, hard)) - - @unittest.skipIf(not HAS_RLIMIT, "not supported") - def test_rlimit_infinity_value(self): - # RLIMIT_FSIZE should be RLIM_INFINITY, which will be a really - # big number on a platform with large file support. On these - # platforms we need to test that the get/setrlimit functions - # properly convert the number to a C long long and that the - # conversion doesn't raise an error. - p = psutil.Process() - soft, hard = p.rlimit(psutil.RLIMIT_FSIZE) - self.assertEqual(psutil.RLIM_INFINITY, hard) - p.rlimit(psutil.RLIMIT_FSIZE, (soft, hard)) - - def test_num_threads(self): - # on certain platforms such as Linux we might test for exact - # thread number, since we always have with 1 thread per process, - # but this does not apply across all platforms (MACOS, Windows) - p = psutil.Process() - if OPENBSD: - try: - step1 = p.num_threads() - except psutil.AccessDenied: - raise unittest.SkipTest("on OpenBSD this requires root access") - else: - step1 = p.num_threads() - - with ThreadTask(): - step2 = p.num_threads() - self.assertEqual(step2, step1 + 1) - - @unittest.skipIf(not WINDOWS, 'WINDOWS only') - def test_num_handles(self): - # a better test is done later into test/_windows.py - p = psutil.Process() - self.assertGreater(p.num_handles(), 0) - - @unittest.skipIf(not HAS_THREADS, 'not supported') - def test_threads(self): - p = psutil.Process() - if OPENBSD: - try: - step1 = p.threads() - except psutil.AccessDenied: - raise unittest.SkipTest("on OpenBSD this requires root access") - else: - step1 = p.threads() - - with ThreadTask(): - step2 = p.threads() - self.assertEqual(len(step2), len(step1) + 1) - athread = step2[0] - # test named tuple - self.assertEqual(athread.id, athread[0]) - self.assertEqual(athread.user_time, athread[1]) - self.assertEqual(athread.system_time, athread[2]) - - @retry_on_failure() - @skip_on_access_denied(only_if=MACOS) - @unittest.skipIf(not HAS_THREADS, 'not supported') - def test_threads_2(self): - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - if OPENBSD: - try: - p.threads() - except psutil.AccessDenied: - raise unittest.SkipTest( - "on OpenBSD this requires root access") - self.assertAlmostEqual( - p.cpu_times().user, - sum([x.user_time for x in p.threads()]), delta=0.1) - self.assertAlmostEqual( - p.cpu_times().system, - sum([x.system_time for x in p.threads()]), delta=0.1) - - def test_memory_info(self): - p = psutil.Process() - - # step 1 - get a base value to compare our results - rss1, vms1 = p.memory_info()[:2] - percent1 = p.memory_percent() - self.assertGreater(rss1, 0) - self.assertGreater(vms1, 0) - - # step 2 - allocate some memory - memarr = [None] * 1500000 - - rss2, vms2 = p.memory_info()[:2] - percent2 = p.memory_percent() - - # step 3 - make sure that the memory usage bumped up - self.assertGreater(rss2, rss1) - self.assertGreaterEqual(vms2, vms1) # vms might be equal - self.assertGreater(percent2, percent1) - del memarr - - if WINDOWS: - mem = p.memory_info() - self.assertEqual(mem.rss, mem.wset) - self.assertEqual(mem.vms, mem.pagefile) - - mem = p.memory_info() - for name in mem._fields: - self.assertGreaterEqual(getattr(mem, name), 0) - - def test_memory_full_info(self): - total = psutil.virtual_memory().total - mem = psutil.Process().memory_full_info() - for name in mem._fields: - value = getattr(mem, name) - self.assertGreaterEqual(value, 0, msg=(name, value)) - if name == 'vms' and OSX or LINUX: - continue - self.assertLessEqual(value, total, msg=(name, value, total)) - if LINUX or WINDOWS or MACOS: - self.assertGreaterEqual(mem.uss, 0) - if LINUX: - self.assertGreaterEqual(mem.pss, 0) - self.assertGreaterEqual(mem.swap, 0) - - @unittest.skipIf(not HAS_MEMORY_MAPS, "not supported") - def test_memory_maps(self): - p = psutil.Process() - maps = p.memory_maps() - paths = [x for x in maps] - self.assertEqual(len(paths), len(set(paths))) - ext_maps = p.memory_maps(grouped=False) - - for nt in maps: - if not nt.path.startswith('['): - assert os.path.isabs(nt.path), nt.path - if POSIX: - try: - assert os.path.exists(nt.path) or \ - os.path.islink(nt.path), nt.path - except AssertionError: - if not LINUX: - raise - else: - # https://github.com/giampaolo/psutil/issues/759 - with open_text('/proc/self/smaps') as f: - data = f.read() - if "%s (deleted)" % nt.path not in data: - raise - else: - # XXX - On Windows we have this strange behavior with - # 64 bit dlls: they are visible via explorer but cannot - # be accessed via os.stat() (wtf?). - if '64' not in os.path.basename(nt.path): - assert os.path.exists(nt.path), nt.path - for nt in ext_maps: - for fname in nt._fields: - value = getattr(nt, fname) - if fname == 'path': - continue - elif fname in ('addr', 'perms'): - assert value, value - else: - self.assertIsInstance(value, (int, long)) - assert value >= 0, value - - @unittest.skipIf(not HAS_MEMORY_MAPS, "not supported") - def test_memory_maps_lists_lib(self): - # Make sure a newly loaded shared lib is listed. - with copyload_shared_lib() as path: - def normpath(p): - return os.path.realpath(os.path.normcase(p)) - libpaths = [normpath(x.path) - for x in psutil.Process().memory_maps()] - self.assertIn(normpath(path), libpaths) - - def test_memory_percent(self): - p = psutil.Process() - p.memory_percent() - self.assertRaises(ValueError, p.memory_percent, memtype="?!?") - if LINUX or MACOS or WINDOWS: - p.memory_percent(memtype='uss') - - def test_is_running(self): - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - assert p.is_running() - assert p.is_running() - p.kill() - p.wait() - assert not p.is_running() - assert not p.is_running() - - def test_exe(self): - sproc = get_test_subprocess() - exe = psutil.Process(sproc.pid).exe() - try: - self.assertEqual(exe, PYTHON_EXE) - except AssertionError: - if WINDOWS and len(exe) == len(PYTHON_EXE): - # on Windows we don't care about case sensitivity - normcase = os.path.normcase - self.assertEqual(normcase(exe), normcase(PYTHON_EXE)) - else: - # certain platforms such as BSD are more accurate returning: - # "/usr/local/bin/python2.7" - # ...instead of: - # "/usr/local/bin/python" - # We do not want to consider this difference in accuracy - # an error. - ver = "%s.%s" % (sys.version_info[0], sys.version_info[1]) - try: - self.assertEqual(exe.replace(ver, ''), - PYTHON_EXE.replace(ver, '')) - except AssertionError: - # Tipically MACOS. Really not sure what to do here. - pass - - out = sh([exe, "-c", "import os; print('hey')"]) - self.assertEqual(out, 'hey') - - def test_cmdline(self): - cmdline = [PYTHON_EXE, "-c", "import time; time.sleep(60)"] - sproc = get_test_subprocess(cmdline) - try: - self.assertEqual(' '.join(psutil.Process(sproc.pid).cmdline()), - ' '.join(cmdline)) - except AssertionError: - # XXX - most of the times the underlying sysctl() call on Net - # and Open BSD returns a truncated string. - # Also /proc/pid/cmdline behaves the same so it looks - # like this is a kernel bug. - # XXX - AIX truncates long arguments in /proc/pid/cmdline - if NETBSD or OPENBSD or AIX: - self.assertEqual( - psutil.Process(sproc.pid).cmdline()[0], PYTHON_EXE) - else: - raise - - def test_name(self): - sproc = get_test_subprocess(PYTHON_EXE) - name = psutil.Process(sproc.pid).name().lower() - pyexe = os.path.basename(os.path.realpath(sys.executable)).lower() - assert pyexe.startswith(name), (pyexe, name) - - # XXX - @unittest.skipIf(SUNOS, "broken on SUNOS") - @unittest.skipIf(AIX, "broken on AIX") - def test_prog_w_funky_name(self): - # Test that name(), exe() and cmdline() correctly handle programs - # with funky chars such as spaces and ")", see: - # https://github.com/giampaolo/psutil/issues/628 - - def rm(): - # Try to limit occasional failures on Appveyor: - # https://ci.appveyor.com/project/giampaolo/psutil/build/1350/ - # job/lbo3bkju55le850n - try: - safe_rmpath(funky_path) - except OSError: - pass - - funky_path = TESTFN + 'foo bar )' - create_exe(funky_path) - self.addCleanup(rm) - cmdline = [funky_path, "-c", - "import time; [time.sleep(0.01) for x in range(3000)];" - "arg1", "arg2", "", "arg3", ""] - sproc = get_test_subprocess(cmdline) - p = psutil.Process(sproc.pid) - # ...in order to try to prevent occasional failures on travis - if TRAVIS: - wait_for_pid(p.pid) - self.assertEqual(p.cmdline(), cmdline) - self.assertEqual(p.name(), os.path.basename(funky_path)) - self.assertEqual(os.path.normcase(p.exe()), - os.path.normcase(funky_path)) - - @unittest.skipIf(not POSIX, 'POSIX only') - def test_uids(self): - p = psutil.Process() - real, effective, saved = p.uids() - # os.getuid() refers to "real" uid - self.assertEqual(real, os.getuid()) - # os.geteuid() refers to "effective" uid - self.assertEqual(effective, os.geteuid()) - # No such thing as os.getsuid() ("saved" uid), but starting - # from python 2.7 we have os.getresuid() which returns all - # of them. - if hasattr(os, "getresuid"): - self.assertEqual(os.getresuid(), p.uids()) - - @unittest.skipIf(not POSIX, 'POSIX only') - def test_gids(self): - p = psutil.Process() - real, effective, saved = p.gids() - # os.getuid() refers to "real" uid - self.assertEqual(real, os.getgid()) - # os.geteuid() refers to "effective" uid - self.assertEqual(effective, os.getegid()) - # No such thing as os.getsgid() ("saved" gid), but starting - # from python 2.7 we have os.getresgid() which returns all - # of them. - if hasattr(os, "getresuid"): - self.assertEqual(os.getresgid(), p.gids()) - - def test_nice(self): - p = psutil.Process() - self.assertRaises(TypeError, p.nice, "str") - if WINDOWS: - try: - init = p.nice() - if sys.version_info > (3, 4): - self.assertIsInstance(init, enum.IntEnum) - else: - self.assertIsInstance(init, int) - self.assertEqual(init, psutil.NORMAL_PRIORITY_CLASS) - p.nice(psutil.HIGH_PRIORITY_CLASS) - self.assertEqual(p.nice(), psutil.HIGH_PRIORITY_CLASS) - p.nice(psutil.NORMAL_PRIORITY_CLASS) - self.assertEqual(p.nice(), psutil.NORMAL_PRIORITY_CLASS) - finally: - p.nice(psutil.NORMAL_PRIORITY_CLASS) - else: - first_nice = p.nice() - try: - if hasattr(os, "getpriority"): - self.assertEqual( - os.getpriority(os.PRIO_PROCESS, os.getpid()), p.nice()) - p.nice(1) - self.assertEqual(p.nice(), 1) - if hasattr(os, "getpriority"): - self.assertEqual( - os.getpriority(os.PRIO_PROCESS, os.getpid()), p.nice()) - # XXX - going back to previous nice value raises - # AccessDenied on MACOS - if not MACOS: - p.nice(0) - self.assertEqual(p.nice(), 0) - except psutil.AccessDenied: - pass - finally: - try: - p.nice(first_nice) - except psutil.AccessDenied: - pass - - def test_status(self): - p = psutil.Process() - self.assertEqual(p.status(), psutil.STATUS_RUNNING) - - def test_username(self): - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - username = p.username() - if WINDOWS: - domain, username = username.split('\\') - self.assertEqual(username, getpass.getuser()) - if 'USERDOMAIN' in os.environ: - self.assertEqual(domain, os.environ['USERDOMAIN']) - else: - self.assertEqual(username, getpass.getuser()) - - def test_cwd(self): - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - self.assertEqual(p.cwd(), os.getcwd()) - - def test_cwd_2(self): - cmd = [PYTHON_EXE, "-c", - "import os, time; os.chdir('..'); time.sleep(60)"] - sproc = get_test_subprocess(cmd) - p = psutil.Process(sproc.pid) - call_until(p.cwd, "ret == os.path.dirname(os.getcwd())") - - @unittest.skipIf(not HAS_CPU_AFFINITY, 'not supported') - def test_cpu_affinity(self): - p = psutil.Process() - initial = p.cpu_affinity() - assert initial, initial - self.addCleanup(p.cpu_affinity, initial) - - if hasattr(os, "sched_getaffinity"): - self.assertEqual(initial, list(os.sched_getaffinity(p.pid))) - self.assertEqual(len(initial), len(set(initial))) - - all_cpus = list(range(len(psutil.cpu_percent(percpu=True)))) - # Work around travis failure: - # https://travis-ci.org/giampaolo/psutil/builds/284173194 - for n in all_cpus if not TRAVIS else initial: - p.cpu_affinity([n]) - self.assertEqual(p.cpu_affinity(), [n]) - if hasattr(os, "sched_getaffinity"): - self.assertEqual(p.cpu_affinity(), - list(os.sched_getaffinity(p.pid))) - # also test num_cpu() - if hasattr(p, "num_cpu"): - self.assertEqual(p.cpu_affinity()[0], p.num_cpu()) - - # [] is an alias for "all eligible CPUs"; on Linux this may - # not be equal to all available CPUs, see: - # https://github.com/giampaolo/psutil/issues/956 - p.cpu_affinity([]) - if LINUX: - self.assertEqual(p.cpu_affinity(), p._proc._get_eligible_cpus()) - else: - self.assertEqual(p.cpu_affinity(), all_cpus) - if hasattr(os, "sched_getaffinity"): - self.assertEqual(p.cpu_affinity(), - list(os.sched_getaffinity(p.pid))) - # - self.assertRaises(TypeError, p.cpu_affinity, 1) - p.cpu_affinity(initial) - # it should work with all iterables, not only lists - if not TRAVIS: - p.cpu_affinity(set(all_cpus)) - p.cpu_affinity(tuple(all_cpus)) - - @unittest.skipIf(not HAS_CPU_AFFINITY, 'not supported') - def test_cpu_affinity_errs(self): - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - invalid_cpu = [len(psutil.cpu_times(percpu=True)) + 10] - self.assertRaises(ValueError, p.cpu_affinity, invalid_cpu) - self.assertRaises(ValueError, p.cpu_affinity, range(10000, 11000)) - self.assertRaises(TypeError, p.cpu_affinity, [0, "1"]) - self.assertRaises(ValueError, p.cpu_affinity, [0, -1]) - - @unittest.skipIf(not HAS_CPU_AFFINITY, 'not supported') - def test_cpu_affinity_all_combinations(self): - p = psutil.Process() - initial = p.cpu_affinity() - assert initial, initial - self.addCleanup(p.cpu_affinity, initial) - - # All possible CPU set combinations. - combos = [] - for l in range(0, len(initial) + 1): - for subset in itertools.combinations(initial, l): - if subset: - combos.append(list(subset)) - - for combo in combos: - p.cpu_affinity(combo) - self.assertEqual(p.cpu_affinity(), combo) - - # TODO: #595 - @unittest.skipIf(BSD, "broken on BSD") - # can't find any process file on Appveyor - @unittest.skipIf(APPVEYOR, "unreliable on APPVEYOR") - def test_open_files(self): - # current process - p = psutil.Process() - files = p.open_files() - self.assertFalse(TESTFN in files) - with open(TESTFN, 'wb') as f: - f.write(b'x' * 1024) - f.flush() - # give the kernel some time to see the new file - files = call_until(p.open_files, "len(ret) != %i" % len(files)) - for file in files: - if file.path == TESTFN: - if LINUX: - self.assertEqual(file.position, 1024) - break - else: - self.fail("no file found; files=%s" % repr(files)) - for file in files: - assert os.path.isfile(file.path), file - - # another process - cmdline = "import time; f = open(r'%s', 'r'); time.sleep(60);" % TESTFN - sproc = get_test_subprocess([PYTHON_EXE, "-c", cmdline]) - p = psutil.Process(sproc.pid) - - for x in range(100): - filenames = [x.path for x in p.open_files()] - if TESTFN in filenames: - break - time.sleep(.01) - else: - self.assertIn(TESTFN, filenames) - for file in filenames: - assert os.path.isfile(file), file - - # TODO: #595 - @unittest.skipIf(BSD, "broken on BSD") - # can't find any process file on Appveyor - @unittest.skipIf(APPVEYOR, "unreliable on APPVEYOR") - def test_open_files_2(self): - # test fd and path fields - with open(TESTFN, 'w') as fileobj: - p = psutil.Process() - for file in p.open_files(): - if file.path == fileobj.name or file.fd == fileobj.fileno(): - break - else: - self.fail("no file found; files=%s" % repr(p.open_files())) - self.assertEqual(file.path, fileobj.name) - if WINDOWS: - self.assertEqual(file.fd, -1) - else: - self.assertEqual(file.fd, fileobj.fileno()) - # test positions - ntuple = p.open_files()[0] - self.assertEqual(ntuple[0], ntuple.path) - self.assertEqual(ntuple[1], ntuple.fd) - # test file is gone - self.assertNotIn(fileobj.name, p.open_files()) - - @unittest.skipIf(not POSIX, 'POSIX only') - def test_num_fds(self): - p = psutil.Process() - start = p.num_fds() - file = open(TESTFN, 'w') - self.addCleanup(file.close) - self.assertEqual(p.num_fds(), start + 1) - sock = socket.socket() - self.addCleanup(sock.close) - self.assertEqual(p.num_fds(), start + 2) - file.close() - sock.close() - self.assertEqual(p.num_fds(), start) - - @skip_on_not_implemented(only_if=LINUX) - @unittest.skipIf(OPENBSD or NETBSD, "not reliable on OPENBSD & NETBSD") - def test_num_ctx_switches(self): - p = psutil.Process() - before = sum(p.num_ctx_switches()) - for x in range(500000): - after = sum(p.num_ctx_switches()) - if after > before: - return - self.fail("num ctx switches still the same after 50.000 iterations") - - def test_ppid(self): - if hasattr(os, 'getppid'): - self.assertEqual(psutil.Process().ppid(), os.getppid()) - this_parent = os.getpid() - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - self.assertEqual(p.ppid(), this_parent) - # no other process is supposed to have us as parent - reap_children(recursive=True) - if APPVEYOR: - # Occasional failures, see: - # https://ci.appveyor.com/project/giampaolo/psutil/build/ - # job/0hs623nenj7w4m33 - return - for p in psutil.process_iter(): - if p.pid == sproc.pid: - continue - # XXX: sometimes this fails on Windows; not sure why. - self.assertNotEqual(p.ppid(), this_parent, msg=p) - - def test_parent(self): - this_parent = os.getpid() - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - self.assertEqual(p.parent().pid, this_parent) - - lowest_pid = psutil.pids()[0] - self.assertIsNone(psutil.Process(lowest_pid).parent()) - - def test_parent_multi(self): - p1, p2 = create_proc_children_pair() - self.assertEqual(p2.parent(), p1) - self.assertEqual(p1.parent(), psutil.Process()) - - def test_parent_disappeared(self): - # Emulate a case where the parent process disappeared. - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - with mock.patch("psutil.Process", - side_effect=psutil.NoSuchProcess(0, 'foo')): - self.assertIsNone(p.parent()) - - def test_parents(self): - assert psutil.Process().parents() - p1, p2 = create_proc_children_pair() - self.assertEqual(p1.parents()[0], psutil.Process()) - self.assertEqual(p2.parents()[0], p1) - self.assertEqual(p2.parents()[1], psutil.Process()) - if POSIX: - lowest_pid = psutil.pids()[0] - self.assertEqual(p1.parents()[-1].pid, lowest_pid) - self.assertEqual(p2.parents()[-1].pid, lowest_pid) - - def test_children(self): - reap_children(recursive=True) - p = psutil.Process() - self.assertEqual(p.children(), []) - self.assertEqual(p.children(recursive=True), []) - # On Windows we set the flag to 0 in order to cancel out the - # CREATE_NO_WINDOW flag (enabled by default) which creates - # an extra "conhost.exe" child. - sproc = get_test_subprocess(creationflags=0) - children1 = p.children() - children2 = p.children(recursive=True) - for children in (children1, children2): - self.assertEqual(len(children), 1) - self.assertEqual(children[0].pid, sproc.pid) - self.assertEqual(children[0].ppid(), os.getpid()) - - def test_children_recursive(self): - # Test children() against two sub processes, p1 and p2, where - # p1 (our child) spawned p2 (our grandchild). - p1, p2 = create_proc_children_pair() - p = psutil.Process() - self.assertEqual(p.children(), [p1]) - self.assertEqual(p.children(recursive=True), [p1, p2]) - # If the intermediate process is gone there's no way for - # children() to recursively find it. - p1.terminate() - p1.wait() - self.assertEqual(p.children(recursive=True), []) - - def test_children_duplicates(self): - # find the process which has the highest number of children - table = collections.defaultdict(int) - for p in psutil.process_iter(): - try: - table[p.ppid()] += 1 - except psutil.Error: - pass - # this is the one, now let's make sure there are no duplicates - pid = sorted(table.items(), key=lambda x: x[1])[-1][0] - p = psutil.Process(pid) - try: - c = p.children(recursive=True) - except psutil.AccessDenied: # windows - pass - else: - self.assertEqual(len(c), len(set(c))) - - def test_parents_and_children(self): - p1, p2 = create_proc_children_pair() - me = psutil.Process() - # forward - children = me.children(recursive=True) - self.assertEqual(len(children), 2) - self.assertEqual(children[0], p1) - self.assertEqual(children[1], p2) - # backward - parents = p2.parents() - self.assertEqual(parents[0], p1) - self.assertEqual(parents[1], me) - - def test_suspend_resume(self): - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - p.suspend() - for x in range(100): - if p.status() == psutil.STATUS_STOPPED: - break - time.sleep(0.01) - p.resume() - self.assertNotEqual(p.status(), psutil.STATUS_STOPPED) - - def test_invalid_pid(self): - self.assertRaises(TypeError, psutil.Process, "1") - self.assertRaises(ValueError, psutil.Process, -1) - - def test_as_dict(self): - p = psutil.Process() - d = p.as_dict(attrs=['exe', 'name']) - self.assertEqual(sorted(d.keys()), ['exe', 'name']) - - p = psutil.Process(min(psutil.pids())) - d = p.as_dict(attrs=['connections'], ad_value='foo') - if not isinstance(d['connections'], list): - self.assertEqual(d['connections'], 'foo') - - # Test ad_value is set on AccessDenied. - with mock.patch('psutil.Process.nice', create=True, - side_effect=psutil.AccessDenied): - self.assertEqual( - p.as_dict(attrs=["nice"], ad_value=1), {"nice": 1}) - - # Test that NoSuchProcess bubbles up. - with mock.patch('psutil.Process.nice', create=True, - side_effect=psutil.NoSuchProcess(p.pid, "name")): - self.assertRaises( - psutil.NoSuchProcess, p.as_dict, attrs=["nice"]) - - # Test that ZombieProcess is swallowed. - with mock.patch('psutil.Process.nice', create=True, - side_effect=psutil.ZombieProcess(p.pid, "name")): - self.assertEqual( - p.as_dict(attrs=["nice"], ad_value="foo"), {"nice": "foo"}) - - # By default APIs raising NotImplementedError are - # supposed to be skipped. - with mock.patch('psutil.Process.nice', create=True, - side_effect=NotImplementedError): - d = p.as_dict() - self.assertNotIn('nice', list(d.keys())) - # ...unless the user explicitly asked for some attr. - with self.assertRaises(NotImplementedError): - p.as_dict(attrs=["nice"]) - - # errors - with self.assertRaises(TypeError): - p.as_dict('name') - with self.assertRaises(ValueError): - p.as_dict(['foo']) - with self.assertRaises(ValueError): - p.as_dict(['foo', 'bar']) - - def test_oneshot(self): - with mock.patch("psutil._psplatform.Process.cpu_times") as m: - p = psutil.Process() - with p.oneshot(): - p.cpu_times() - p.cpu_times() - self.assertEqual(m.call_count, 1) - - with mock.patch("psutil._psplatform.Process.cpu_times") as m: - p.cpu_times() - p.cpu_times() - self.assertEqual(m.call_count, 2) - - def test_oneshot_twice(self): - # Test the case where the ctx manager is __enter__ed twice. - # The second __enter__ is supposed to resut in a NOOP. - with mock.patch("psutil._psplatform.Process.cpu_times") as m1: - with mock.patch("psutil._psplatform.Process.oneshot_enter") as m2: - p = psutil.Process() - with p.oneshot(): - p.cpu_times() - p.cpu_times() - with p.oneshot(): - p.cpu_times() - p.cpu_times() - self.assertEqual(m1.call_count, 1) - self.assertEqual(m2.call_count, 1) - - with mock.patch("psutil._psplatform.Process.cpu_times") as m: - p.cpu_times() - p.cpu_times() - self.assertEqual(m.call_count, 2) - - def test_oneshot_cache(self): - # Make sure oneshot() cache is nonglobal. Instead it's - # supposed to be bound to the Process instance, see: - # https://github.com/giampaolo/psutil/issues/1373 - p1, p2 = create_proc_children_pair() - p1_ppid = p1.ppid() - p2_ppid = p2.ppid() - self.assertNotEqual(p1_ppid, p2_ppid) - with p1.oneshot(): - self.assertEqual(p1.ppid(), p1_ppid) - self.assertEqual(p2.ppid(), p2_ppid) - with p2.oneshot(): - self.assertEqual(p1.ppid(), p1_ppid) - self.assertEqual(p2.ppid(), p2_ppid) - - def test_halfway_terminated_process(self): - # Test that NoSuchProcess exception gets raised in case the - # process dies after we create the Process object. - # Example: - # >>> proc = Process(1234) - # >>> time.sleep(2) # time-consuming task, process dies in meantime - # >>> proc.name() - # Refers to Issue #15 - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - p.terminate() - p.wait() - if WINDOWS: - call_until(psutil.pids, "%s not in ret" % p.pid) - assert not p.is_running() - - if WINDOWS: - with self.assertRaises(psutil.NoSuchProcess): - p.send_signal(signal.CTRL_C_EVENT) - with self.assertRaises(psutil.NoSuchProcess): - p.send_signal(signal.CTRL_BREAK_EVENT) - - excluded_names = ['pid', 'is_running', 'wait', 'create_time', - 'oneshot', 'memory_info_ex'] - if LINUX and not HAS_RLIMIT: - excluded_names.append('rlimit') - for name in dir(p): - if (name.startswith('_') or - name in excluded_names): - continue - try: - meth = getattr(p, name) - # get/set methods - if name == 'nice': - if POSIX: - ret = meth(1) - else: - ret = meth(psutil.NORMAL_PRIORITY_CLASS) - elif name == 'ionice': - ret = meth() - ret = meth(2) - elif name == 'rlimit': - ret = meth(psutil.RLIMIT_NOFILE) - ret = meth(psutil.RLIMIT_NOFILE, (5, 5)) - elif name == 'cpu_affinity': - ret = meth() - ret = meth([0]) - elif name == 'send_signal': - ret = meth(signal.SIGTERM) - else: - ret = meth() - except psutil.ZombieProcess: - self.fail("ZombieProcess for %r was not supposed to happen" % - name) - except psutil.NoSuchProcess: - pass - except psutil.AccessDenied: - if OPENBSD and name in ('threads', 'num_threads'): - pass - else: - raise - except NotImplementedError: - pass - else: - self.fail( - "NoSuchProcess exception not raised for %r, retval=%s" % ( - name, ret)) - - @unittest.skipIf(not POSIX, 'POSIX only') - def test_zombie_process(self): - def succeed_or_zombie_p_exc(fun, *args, **kwargs): - try: - return fun(*args, **kwargs) - except (psutil.ZombieProcess, psutil.AccessDenied): - pass - - zpid = create_zombie_proc() - self.addCleanup(reap_children, recursive=True) - # A zombie process should always be instantiable - zproc = psutil.Process(zpid) - # ...and at least its status always be querable - self.assertEqual(zproc.status(), psutil.STATUS_ZOMBIE) - # ...and it should be considered 'running' - self.assertTrue(zproc.is_running()) - # ...and as_dict() shouldn't crash - zproc.as_dict() - # if cmdline succeeds it should be an empty list - ret = succeed_or_zombie_p_exc(zproc.suspend) - if ret is not None: - self.assertEqual(ret, []) - - if hasattr(zproc, "rlimit"): - succeed_or_zombie_p_exc(zproc.rlimit, psutil.RLIMIT_NOFILE) - succeed_or_zombie_p_exc(zproc.rlimit, psutil.RLIMIT_NOFILE, - (5, 5)) - # set methods - succeed_or_zombie_p_exc(zproc.parent) - if hasattr(zproc, 'cpu_affinity'): - try: - succeed_or_zombie_p_exc(zproc.cpu_affinity, [0]) - except ValueError as err: - if TRAVIS and LINUX and "not eligible" in str(err): - # https://travis-ci.org/giampaolo/psutil/jobs/279890461 - pass - else: - raise - - succeed_or_zombie_p_exc(zproc.nice, 0) - if hasattr(zproc, 'ionice'): - if LINUX: - succeed_or_zombie_p_exc(zproc.ionice, 2, 0) - else: - succeed_or_zombie_p_exc(zproc.ionice, 0) # Windows - if hasattr(zproc, 'rlimit'): - succeed_or_zombie_p_exc(zproc.rlimit, - psutil.RLIMIT_NOFILE, (5, 5)) - succeed_or_zombie_p_exc(zproc.suspend) - succeed_or_zombie_p_exc(zproc.resume) - succeed_or_zombie_p_exc(zproc.terminate) - succeed_or_zombie_p_exc(zproc.kill) - - # ...its parent should 'see' it - # edit: not true on BSD and MACOS - # descendants = [x.pid for x in psutil.Process().children( - # recursive=True)] - # self.assertIn(zpid, descendants) - # XXX should we also assume ppid be usable? Note: this - # would be an important use case as the only way to get - # rid of a zombie is to kill its parent. - # self.assertEqual(zpid.ppid(), os.getpid()) - # ...and all other APIs should be able to deal with it - self.assertTrue(psutil.pid_exists(zpid)) - if not TRAVIS and MACOS: - # For some reason this started failing all of the sudden. - # Maybe they upgraded MACOS version? - # https://travis-ci.org/giampaolo/psutil/jobs/310896404 - self.assertIn(zpid, psutil.pids()) - self.assertIn(zpid, [x.pid for x in psutil.process_iter()]) - psutil._pmap = {} - self.assertIn(zpid, [x.pid for x in psutil.process_iter()]) - - @unittest.skipIf(not POSIX, 'POSIX only') - def test_zombie_process_is_running_w_exc(self): - # Emulate a case where internally is_running() raises - # ZombieProcess. - p = psutil.Process() - with mock.patch("psutil.Process", - side_effect=psutil.ZombieProcess(0)) as m: - assert p.is_running() - assert m.called - - @unittest.skipIf(not POSIX, 'POSIX only') - def test_zombie_process_status_w_exc(self): - # Emulate a case where internally status() raises - # ZombieProcess. - p = psutil.Process() - with mock.patch("psutil._psplatform.Process.status", - side_effect=psutil.ZombieProcess(0)) as m: - self.assertEqual(p.status(), psutil.STATUS_ZOMBIE) - assert m.called - - def test_pid_0(self): - # Process(0) is supposed to work on all platforms except Linux - if 0 not in psutil.pids(): - self.assertRaises(psutil.NoSuchProcess, psutil.Process, 0) - return - - # test all methods - p = psutil.Process(0) - for name in psutil._as_dict_attrnames: - if name == 'pid': - continue - meth = getattr(p, name) - try: - ret = meth() - except psutil.AccessDenied: - pass - else: - if name in ("uids", "gids"): - self.assertEqual(ret.real, 0) - elif name == "username": - if POSIX: - self.assertEqual(p.username(), 'root') - elif WINDOWS: - self.assertEqual(p.username(), 'NT AUTHORITY\\SYSTEM') - elif name == "name": - assert name, name - - if hasattr(p, 'rlimit'): - try: - p.rlimit(psutil.RLIMIT_FSIZE) - except psutil.AccessDenied: - pass - - p.as_dict() - - if not OPENBSD: - self.assertIn(0, psutil.pids()) - self.assertTrue(psutil.pid_exists(0)) - - @unittest.skipIf(not HAS_ENVIRON, "not supported") - def test_environ(self): - def clean_dict(d): - # Most of these are problematic on Travis. - d.pop("PSUTIL_TESTING", None) - d.pop("PLAT", None) - d.pop("HOME", None) - if MACOS: - d.pop("__CF_USER_TEXT_ENCODING", None) - d.pop("VERSIONER_PYTHON_PREFER_32_BIT", None) - d.pop("VERSIONER_PYTHON_VERSION", None) - return dict( - [(k.replace("\r", "").replace("\n", ""), - v.replace("\r", "").replace("\n", "")) - for k, v in d.items()]) - - self.maxDiff = None - p = psutil.Process() - d1 = clean_dict(p.environ()) - d2 = clean_dict(os.environ.copy()) - self.assertEqual(d1, d2) - - @unittest.skipIf(not HAS_ENVIRON, "not supported") - @unittest.skipIf(not POSIX, "POSIX only") - def test_weird_environ(self): - # environment variables can contain values without an equals sign - code = textwrap.dedent(""" - #include - #include - char * const argv[] = {"cat", 0}; - char * const envp[] = {"A=1", "X", "C=3", 0}; - int main(void) { - /* Close stderr on exec so parent can wait for the execve to - * finish. */ - if (fcntl(2, F_SETFD, FD_CLOEXEC) != 0) - return 0; - return execve("/bin/cat", argv, envp); - } - """) - path = TESTFN - create_exe(path, c_code=code) - self.addCleanup(safe_rmpath, path) - sproc = get_test_subprocess([path], - stdin=subprocess.PIPE, - stderr=subprocess.PIPE) - p = psutil.Process(sproc.pid) - wait_for_pid(p.pid) - self.assertTrue(p.is_running()) - # Wait for process to exec or exit. - self.assertEqual(sproc.stderr.read(), b"") - self.assertEqual(p.environ(), {"A": "1", "C": "3"}) - sproc.communicate() - self.assertEqual(sproc.returncode, 0) - - -# =================================================================== -# --- Limited user tests -# =================================================================== - - -if POSIX and os.getuid() == 0: - class LimitedUserTestCase(TestProcess): - """Repeat the previous tests by using a limited user. - Executed only on UNIX and only if the user who run the test script - is root. - """ - # the uid/gid the test suite runs under - if hasattr(os, 'getuid'): - PROCESS_UID = os.getuid() - PROCESS_GID = os.getgid() - - def __init__(self, *args, **kwargs): - TestProcess.__init__(self, *args, **kwargs) - # re-define all existent test methods in order to - # ignore AccessDenied exceptions - for attr in [x for x in dir(self) if x.startswith('test')]: - meth = getattr(self, attr) - - def test_(self): - try: - meth() - except psutil.AccessDenied: - pass - setattr(self, attr, types.MethodType(test_, self)) - - def setUp(self): - safe_rmpath(TESTFN) - TestProcess.setUp(self) - os.setegid(1000) - os.seteuid(1000) - - def tearDown(self): - os.setegid(self.PROCESS_UID) - os.seteuid(self.PROCESS_GID) - TestProcess.tearDown(self) - - def test_nice(self): - try: - psutil.Process().nice(-1) - except psutil.AccessDenied: - pass - else: - self.fail("exception not raised") - - def test_zombie_process(self): - # causes problems if test test suite is run as root - pass - - -# =================================================================== -# --- psutil.Popen tests -# =================================================================== - - -class TestPopen(unittest.TestCase): - """Tests for psutil.Popen class.""" - - def tearDown(self): - reap_children() - - def test_misc(self): - # XXX this test causes a ResourceWarning on Python 3 because - # psutil.__subproc instance doesn't get propertly freed. - # Not sure what to do though. - cmd = [PYTHON_EXE, "-c", "import time; time.sleep(60);"] - with psutil.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) as proc: - proc.name() - proc.cpu_times() - proc.stdin - self.assertTrue(dir(proc)) - self.assertRaises(AttributeError, getattr, proc, 'foo') - proc.terminate() - - def test_ctx_manager(self): - with psutil.Popen([PYTHON_EXE, "-V"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - stdin=subprocess.PIPE) as proc: - proc.communicate() - assert proc.stdout.closed - assert proc.stderr.closed - assert proc.stdin.closed - self.assertEqual(proc.returncode, 0) - - def test_kill_terminate(self): - # subprocess.Popen()'s terminate(), kill() and send_signal() do - # not raise exception after the process is gone. psutil.Popen - # diverges from that. - cmd = [PYTHON_EXE, "-c", "import time; time.sleep(60);"] - with psutil.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) as proc: - proc.terminate() - proc.wait() - self.assertRaises(psutil.NoSuchProcess, proc.terminate) - self.assertRaises(psutil.NoSuchProcess, proc.kill) - self.assertRaises(psutil.NoSuchProcess, proc.send_signal, - signal.SIGTERM) - if WINDOWS and sys.version_info >= (2, 7): - self.assertRaises(psutil.NoSuchProcess, proc.send_signal, - signal.CTRL_C_EVENT) - self.assertRaises(psutil.NoSuchProcess, proc.send_signal, - signal.CTRL_BREAK_EVENT) - - -if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Tests for psutil.Process class.""" + +import collections +import errno +import getpass +import itertools +import os +import signal +import socket +import stat +import subprocess +import sys +import textwrap +import time +import types +import unittest + +import psutil +from psutil import AIX +from psutil import BSD +from psutil import LINUX +from psutil import MACOS +from psutil import NETBSD +from psutil import OPENBSD +from psutil import OSX +from psutil import POSIX +from psutil import SUNOS +from psutil import WINDOWS +from psutil._common import open_text +from psutil._compat import PY3 +from psutil._compat import FileNotFoundError +from psutil._compat import long +from psutil._compat import super +from psutil.tests import APPVEYOR +from psutil.tests import CI_TESTING +from psutil.tests import GITHUB_ACTIONS +from psutil.tests import GLOBAL_TIMEOUT +from psutil.tests import HAS_CPU_AFFINITY +from psutil.tests import HAS_ENVIRON +from psutil.tests import HAS_IONICE +from psutil.tests import HAS_MEMORY_MAPS +from psutil.tests import HAS_PROC_CPU_NUM +from psutil.tests import HAS_PROC_IO_COUNTERS +from psutil.tests import HAS_RLIMIT +from psutil.tests import HAS_THREADS +from psutil.tests import MACOS_11PLUS +from psutil.tests import PYPY +from psutil.tests import PYTHON_EXE +from psutil.tests import PYTHON_EXE_ENV +from psutil.tests import PsutilTestCase +from psutil.tests import ThreadTask +from psutil.tests import call_until +from psutil.tests import copyload_shared_lib +from psutil.tests import create_c_exe +from psutil.tests import create_py_exe +from psutil.tests import mock +from psutil.tests import process_namespace +from psutil.tests import reap_children +from psutil.tests import retry_on_failure +from psutil.tests import sh +from psutil.tests import skip_on_access_denied +from psutil.tests import skip_on_not_implemented +from psutil.tests import wait_for_pid + + +# =================================================================== +# --- psutil.Process class tests +# =================================================================== + + +class TestProcess(PsutilTestCase): + """Tests for psutil.Process class.""" + + def spawn_psproc(self, *args, **kwargs): + sproc = self.spawn_testproc(*args, **kwargs) + try: + return psutil.Process(sproc.pid) + except psutil.NoSuchProcess: + self.assertPidGone(sproc.pid) + raise + + # --- + + def test_pid(self): + p = psutil.Process() + self.assertEqual(p.pid, os.getpid()) + with self.assertRaises(AttributeError): + p.pid = 33 + + def test_kill(self): + p = self.spawn_psproc() + p.kill() + code = p.wait() + if WINDOWS: + self.assertEqual(code, signal.SIGTERM) + else: + self.assertEqual(code, -signal.SIGKILL) + self.assertProcessGone(p) + + def test_terminate(self): + p = self.spawn_psproc() + p.terminate() + code = p.wait() + if WINDOWS: + self.assertEqual(code, signal.SIGTERM) + else: + self.assertEqual(code, -signal.SIGTERM) + self.assertProcessGone(p) + + def test_send_signal(self): + sig = signal.SIGKILL if POSIX else signal.SIGTERM + p = self.spawn_psproc() + p.send_signal(sig) + code = p.wait() + if WINDOWS: + self.assertEqual(code, sig) + else: + self.assertEqual(code, -sig) + self.assertProcessGone(p) + + @unittest.skipIf(not POSIX, "not POSIX") + def test_send_signal_mocked(self): + sig = signal.SIGTERM + p = self.spawn_psproc() + with mock.patch( + 'psutil.os.kill', side_effect=OSError(errno.ESRCH, "") + ): + self.assertRaises(psutil.NoSuchProcess, p.send_signal, sig) + + p = self.spawn_psproc() + with mock.patch( + 'psutil.os.kill', side_effect=OSError(errno.EPERM, "") + ): + self.assertRaises(psutil.AccessDenied, p.send_signal, sig) + + def test_wait_exited(self): + # Test waitpid() + WIFEXITED -> WEXITSTATUS. + # normal return, same as exit(0) + cmd = [PYTHON_EXE, "-c", "pass"] + p = self.spawn_psproc(cmd) + code = p.wait() + self.assertEqual(code, 0) + self.assertProcessGone(p) + # exit(1), implicit in case of error + cmd = [PYTHON_EXE, "-c", "1 / 0"] + p = self.spawn_psproc(cmd, stderr=subprocess.PIPE) + code = p.wait() + self.assertEqual(code, 1) + self.assertProcessGone(p) + # via sys.exit() + cmd = [PYTHON_EXE, "-c", "import sys; sys.exit(5);"] + p = self.spawn_psproc(cmd) + code = p.wait() + self.assertEqual(code, 5) + self.assertProcessGone(p) + # via os._exit() + cmd = [PYTHON_EXE, "-c", "import os; os._exit(5);"] + p = self.spawn_psproc(cmd) + code = p.wait() + self.assertEqual(code, 5) + self.assertProcessGone(p) + + @unittest.skipIf(NETBSD, "fails on NETBSD") + def test_wait_stopped(self): + p = self.spawn_psproc() + if POSIX: + # Test waitpid() + WIFSTOPPED and WIFCONTINUED. + # Note: if a process is stopped it ignores SIGTERM. + p.send_signal(signal.SIGSTOP) + self.assertRaises(psutil.TimeoutExpired, p.wait, timeout=0.001) + p.send_signal(signal.SIGCONT) + self.assertRaises(psutil.TimeoutExpired, p.wait, timeout=0.001) + p.send_signal(signal.SIGTERM) + self.assertEqual(p.wait(), -signal.SIGTERM) + self.assertEqual(p.wait(), -signal.SIGTERM) + else: + p.suspend() + self.assertRaises(psutil.TimeoutExpired, p.wait, timeout=0.001) + p.resume() + self.assertRaises(psutil.TimeoutExpired, p.wait, timeout=0.001) + p.terminate() + self.assertEqual(p.wait(), signal.SIGTERM) + self.assertEqual(p.wait(), signal.SIGTERM) + + def test_wait_non_children(self): + # Test wait() against a process which is not our direct + # child. + child, grandchild = self.spawn_children_pair() + self.assertRaises(psutil.TimeoutExpired, child.wait, 0.01) + self.assertRaises(psutil.TimeoutExpired, grandchild.wait, 0.01) + # We also terminate the direct child otherwise the + # grandchild will hang until the parent is gone. + child.terminate() + grandchild.terminate() + child_ret = child.wait() + grandchild_ret = grandchild.wait() + if POSIX: + self.assertEqual(child_ret, -signal.SIGTERM) + # For processes which are not our children we're supposed + # to get None. + self.assertEqual(grandchild_ret, None) + else: + self.assertEqual(child_ret, signal.SIGTERM) + self.assertEqual(child_ret, signal.SIGTERM) + + def test_wait_timeout(self): + p = self.spawn_psproc() + p.name() + self.assertRaises(psutil.TimeoutExpired, p.wait, 0.01) + self.assertRaises(psutil.TimeoutExpired, p.wait, 0) + self.assertRaises(ValueError, p.wait, -1) + + def test_wait_timeout_nonblocking(self): + p = self.spawn_psproc() + self.assertRaises(psutil.TimeoutExpired, p.wait, 0) + p.kill() + stop_at = time.time() + GLOBAL_TIMEOUT + while time.time() < stop_at: + try: + code = p.wait(0) + break + except psutil.TimeoutExpired: + pass + else: + raise self.fail('timeout') + if POSIX: + self.assertEqual(code, -signal.SIGKILL) + else: + self.assertEqual(code, signal.SIGTERM) + self.assertProcessGone(p) + + def test_cpu_percent(self): + p = psutil.Process() + p.cpu_percent(interval=0.001) + p.cpu_percent(interval=0.001) + for _ in range(100): + percent = p.cpu_percent(interval=None) + self.assertIsInstance(percent, float) + self.assertGreaterEqual(percent, 0.0) + with self.assertRaises(ValueError): + p.cpu_percent(interval=-1) + + def test_cpu_percent_numcpus_none(self): + # See: https://github.com/giampaolo/psutil/issues/1087 + with mock.patch('psutil.cpu_count', return_value=None) as m: + psutil.Process().cpu_percent() + assert m.called + + def test_cpu_times(self): + times = psutil.Process().cpu_times() + assert (times.user > 0.0) or (times.system > 0.0), times + assert times.children_user >= 0.0, times + assert times.children_system >= 0.0, times + if LINUX: + assert times.iowait >= 0.0, times + # make sure returned values can be pretty printed with strftime + for name in times._fields: + time.strftime("%H:%M:%S", time.localtime(getattr(times, name))) + + def test_cpu_times_2(self): + user_time, kernel_time = psutil.Process().cpu_times()[:2] + utime, ktime = os.times()[:2] + + # Use os.times()[:2] as base values to compare our results + # using a tolerance of +/- 0.1 seconds. + # It will fail if the difference between the values is > 0.1s. + if (max([user_time, utime]) - min([user_time, utime])) > 0.1: + raise self.fail("expected: %s, found: %s" % (utime, user_time)) + + if (max([kernel_time, ktime]) - min([kernel_time, ktime])) > 0.1: + raise self.fail("expected: %s, found: %s" % (ktime, kernel_time)) + + @unittest.skipIf(not HAS_PROC_CPU_NUM, "not supported") + def test_cpu_num(self): + p = psutil.Process() + num = p.cpu_num() + self.assertGreaterEqual(num, 0) + if psutil.cpu_count() == 1: + self.assertEqual(num, 0) + self.assertIn(p.cpu_num(), range(psutil.cpu_count())) + + def test_create_time(self): + p = self.spawn_psproc() + now = time.time() + create_time = p.create_time() + + # Use time.time() as base value to compare our result using a + # tolerance of +/- 1 second. + # It will fail if the difference between the values is > 2s. + difference = abs(create_time - now) + if difference > 2: + raise self.fail( + "expected: %s, found: %s, difference: %s" + % (now, create_time, difference) + ) + + # make sure returned value can be pretty printed with strftime + time.strftime("%Y %m %d %H:%M:%S", time.localtime(p.create_time())) + + @unittest.skipIf(not POSIX, 'POSIX only') + def test_terminal(self): + terminal = psutil.Process().terminal() + if terminal is not None: + tty = os.path.realpath(sh('tty')) + self.assertEqual(terminal, tty) + + @unittest.skipIf(not HAS_PROC_IO_COUNTERS, 'not supported') + @skip_on_not_implemented(only_if=LINUX) + def test_io_counters(self): + p = psutil.Process() + # test reads + io1 = p.io_counters() + with open(PYTHON_EXE, 'rb') as f: + f.read() + io2 = p.io_counters() + if not BSD and not AIX: + self.assertGreater(io2.read_count, io1.read_count) + self.assertEqual(io2.write_count, io1.write_count) + if LINUX: + self.assertGreater(io2.read_chars, io1.read_chars) + self.assertEqual(io2.write_chars, io1.write_chars) + else: + self.assertGreaterEqual(io2.read_bytes, io1.read_bytes) + self.assertGreaterEqual(io2.write_bytes, io1.write_bytes) + + # test writes + io1 = p.io_counters() + with open(self.get_testfn(), 'wb') as f: + if PY3: + f.write(bytes("x" * 1000000, 'ascii')) + else: + f.write("x" * 1000000) + io2 = p.io_counters() + self.assertGreaterEqual(io2.write_count, io1.write_count) + self.assertGreaterEqual(io2.write_bytes, io1.write_bytes) + self.assertGreaterEqual(io2.read_count, io1.read_count) + self.assertGreaterEqual(io2.read_bytes, io1.read_bytes) + if LINUX: + self.assertGreater(io2.write_chars, io1.write_chars) + self.assertGreaterEqual(io2.read_chars, io1.read_chars) + + # sanity check + for i in range(len(io2)): + if BSD and i >= 2: + # On BSD read_bytes and write_bytes are always set to -1. + continue + self.assertGreaterEqual(io2[i], 0) + self.assertGreaterEqual(io2[i], 0) + + @unittest.skipIf(not HAS_IONICE, "not supported") + @unittest.skipIf(not LINUX, "linux only") + def test_ionice_linux(self): + def cleanup(init): + ioclass, value = init + if ioclass == psutil.IOPRIO_CLASS_NONE: + value = 0 + p.ionice(ioclass, value) + + p = psutil.Process() + if not CI_TESTING: + self.assertEqual(p.ionice()[0], psutil.IOPRIO_CLASS_NONE) + self.assertEqual(psutil.IOPRIO_CLASS_NONE, 0) + self.assertEqual(psutil.IOPRIO_CLASS_RT, 1) # high + self.assertEqual(psutil.IOPRIO_CLASS_BE, 2) # normal + self.assertEqual(psutil.IOPRIO_CLASS_IDLE, 3) # low + init = p.ionice() + self.addCleanup(cleanup, init) + + # low + p.ionice(psutil.IOPRIO_CLASS_IDLE) + self.assertEqual(tuple(p.ionice()), (psutil.IOPRIO_CLASS_IDLE, 0)) + with self.assertRaises(ValueError): # accepts no value + p.ionice(psutil.IOPRIO_CLASS_IDLE, value=7) + # normal + p.ionice(psutil.IOPRIO_CLASS_BE) + self.assertEqual(tuple(p.ionice()), (psutil.IOPRIO_CLASS_BE, 0)) + p.ionice(psutil.IOPRIO_CLASS_BE, value=7) + self.assertEqual(tuple(p.ionice()), (psutil.IOPRIO_CLASS_BE, 7)) + with self.assertRaises(ValueError): + p.ionice(psutil.IOPRIO_CLASS_BE, value=8) + try: + p.ionice(psutil.IOPRIO_CLASS_RT, value=7) + except psutil.AccessDenied: + pass + # errs + with self.assertRaisesRegex(ValueError, "ioclass accepts no value"): + p.ionice(psutil.IOPRIO_CLASS_NONE, 1) + with self.assertRaisesRegex(ValueError, "ioclass accepts no value"): + p.ionice(psutil.IOPRIO_CLASS_IDLE, 1) + with self.assertRaisesRegex( + ValueError, "'ioclass' argument must be specified" + ): + p.ionice(value=1) + + @unittest.skipIf(not HAS_IONICE, "not supported") + @unittest.skipIf(not WINDOWS, 'not supported on this win version') + def test_ionice_win(self): + p = psutil.Process() + if not CI_TESTING: + self.assertEqual(p.ionice(), psutil.IOPRIO_NORMAL) + init = p.ionice() + self.addCleanup(p.ionice, init) + + # base + p.ionice(psutil.IOPRIO_VERYLOW) + self.assertEqual(p.ionice(), psutil.IOPRIO_VERYLOW) + p.ionice(psutil.IOPRIO_LOW) + self.assertEqual(p.ionice(), psutil.IOPRIO_LOW) + try: + p.ionice(psutil.IOPRIO_HIGH) + except psutil.AccessDenied: + pass + else: + self.assertEqual(p.ionice(), psutil.IOPRIO_HIGH) + # errs + with self.assertRaisesRegex( + TypeError, "value argument not accepted on Windows" + ): + p.ionice(psutil.IOPRIO_NORMAL, value=1) + with self.assertRaisesRegex(ValueError, "is not a valid priority"): + p.ionice(psutil.IOPRIO_HIGH + 1) + + @unittest.skipIf(not HAS_RLIMIT, "not supported") + def test_rlimit_get(self): + import resource + + p = psutil.Process(os.getpid()) + names = [x for x in dir(psutil) if x.startswith('RLIMIT')] + assert names, names + for name in names: + value = getattr(psutil, name) + self.assertGreaterEqual(value, 0) + if name in dir(resource): + self.assertEqual(value, getattr(resource, name)) + # XXX - On PyPy RLIMIT_INFINITY returned by + # resource.getrlimit() is reported as a very big long + # number instead of -1. It looks like a bug with PyPy. + if PYPY: + continue + self.assertEqual(p.rlimit(value), resource.getrlimit(value)) + else: + ret = p.rlimit(value) + self.assertEqual(len(ret), 2) + self.assertGreaterEqual(ret[0], -1) + self.assertGreaterEqual(ret[1], -1) + + @unittest.skipIf(not HAS_RLIMIT, "not supported") + def test_rlimit_set(self): + p = self.spawn_psproc() + p.rlimit(psutil.RLIMIT_NOFILE, (5, 5)) + self.assertEqual(p.rlimit(psutil.RLIMIT_NOFILE), (5, 5)) + # If pid is 0 prlimit() applies to the calling process and + # we don't want that. + if LINUX: + with self.assertRaisesRegex(ValueError, "can't use prlimit"): + psutil._psplatform.Process(0).rlimit(0) + with self.assertRaises(ValueError): + p.rlimit(psutil.RLIMIT_NOFILE, (5, 5, 5)) + + @unittest.skipIf(not HAS_RLIMIT, "not supported") + def test_rlimit(self): + p = psutil.Process() + testfn = self.get_testfn() + soft, hard = p.rlimit(psutil.RLIMIT_FSIZE) + try: + p.rlimit(psutil.RLIMIT_FSIZE, (1024, hard)) + with open(testfn, "wb") as f: + f.write(b"X" * 1024) + # write() or flush() doesn't always cause the exception + # but close() will. + with self.assertRaises(IOError) as exc: + with open(testfn, "wb") as f: + f.write(b"X" * 1025) + self.assertEqual( + exc.exception.errno if PY3 else exc.exception[0], errno.EFBIG + ) + finally: + p.rlimit(psutil.RLIMIT_FSIZE, (soft, hard)) + self.assertEqual(p.rlimit(psutil.RLIMIT_FSIZE), (soft, hard)) + + @unittest.skipIf(not HAS_RLIMIT, "not supported") + def test_rlimit_infinity(self): + # First set a limit, then re-set it by specifying INFINITY + # and assume we overridden the previous limit. + p = psutil.Process() + soft, hard = p.rlimit(psutil.RLIMIT_FSIZE) + try: + p.rlimit(psutil.RLIMIT_FSIZE, (1024, hard)) + p.rlimit(psutil.RLIMIT_FSIZE, (psutil.RLIM_INFINITY, hard)) + with open(self.get_testfn(), "wb") as f: + f.write(b"X" * 2048) + finally: + p.rlimit(psutil.RLIMIT_FSIZE, (soft, hard)) + self.assertEqual(p.rlimit(psutil.RLIMIT_FSIZE), (soft, hard)) + + @unittest.skipIf(not HAS_RLIMIT, "not supported") + def test_rlimit_infinity_value(self): + # RLIMIT_FSIZE should be RLIM_INFINITY, which will be a really + # big number on a platform with large file support. On these + # platforms we need to test that the get/setrlimit functions + # properly convert the number to a C long long and that the + # conversion doesn't raise an error. + p = psutil.Process() + soft, hard = p.rlimit(psutil.RLIMIT_FSIZE) + self.assertEqual(psutil.RLIM_INFINITY, hard) + p.rlimit(psutil.RLIMIT_FSIZE, (soft, hard)) + + def test_num_threads(self): + # on certain platforms such as Linux we might test for exact + # thread number, since we always have with 1 thread per process, + # but this does not apply across all platforms (MACOS, Windows) + p = psutil.Process() + if OPENBSD: + try: + step1 = p.num_threads() + except psutil.AccessDenied: + raise unittest.SkipTest("on OpenBSD this requires root access") + else: + step1 = p.num_threads() + + with ThreadTask(): + step2 = p.num_threads() + self.assertEqual(step2, step1 + 1) + + @unittest.skipIf(not WINDOWS, 'WINDOWS only') + def test_num_handles(self): + # a better test is done later into test/_windows.py + p = psutil.Process() + self.assertGreater(p.num_handles(), 0) + + @unittest.skipIf(not HAS_THREADS, 'not supported') + def test_threads(self): + p = psutil.Process() + if OPENBSD: + try: + step1 = p.threads() + except psutil.AccessDenied: + raise unittest.SkipTest("on OpenBSD this requires root access") + else: + step1 = p.threads() + + with ThreadTask(): + step2 = p.threads() + self.assertEqual(len(step2), len(step1) + 1) + athread = step2[0] + # test named tuple + self.assertEqual(athread.id, athread[0]) + self.assertEqual(athread.user_time, athread[1]) + self.assertEqual(athread.system_time, athread[2]) + + @retry_on_failure() + @skip_on_access_denied(only_if=MACOS) + @unittest.skipIf(not HAS_THREADS, 'not supported') + def test_threads_2(self): + p = self.spawn_psproc() + if OPENBSD: + try: + p.threads() + except psutil.AccessDenied: + raise unittest.SkipTest("on OpenBSD this requires root access") + self.assertAlmostEqual( + p.cpu_times().user, + sum([x.user_time for x in p.threads()]), + delta=0.1, + ) + self.assertAlmostEqual( + p.cpu_times().system, + sum([x.system_time for x in p.threads()]), + delta=0.1, + ) + + @retry_on_failure() + def test_memory_info(self): + p = psutil.Process() + + # step 1 - get a base value to compare our results + rss1, vms1 = p.memory_info()[:2] + percent1 = p.memory_percent() + self.assertGreater(rss1, 0) + self.assertGreater(vms1, 0) + + # step 2 - allocate some memory + memarr = [None] * 1500000 + + rss2, vms2 = p.memory_info()[:2] + percent2 = p.memory_percent() + + # step 3 - make sure that the memory usage bumped up + self.assertGreater(rss2, rss1) + self.assertGreaterEqual(vms2, vms1) # vms might be equal + self.assertGreater(percent2, percent1) + del memarr + + if WINDOWS: + mem = p.memory_info() + self.assertEqual(mem.rss, mem.wset) + self.assertEqual(mem.vms, mem.pagefile) + + mem = p.memory_info() + for name in mem._fields: + self.assertGreaterEqual(getattr(mem, name), 0) + + def test_memory_full_info(self): + p = psutil.Process() + total = psutil.virtual_memory().total + mem = p.memory_full_info() + for name in mem._fields: + value = getattr(mem, name) + self.assertGreaterEqual(value, 0, msg=(name, value)) + if name == 'vms' and OSX or LINUX: + continue + self.assertLessEqual(value, total, msg=(name, value, total)) + if LINUX or WINDOWS or MACOS: + self.assertGreaterEqual(mem.uss, 0) + if LINUX: + self.assertGreaterEqual(mem.pss, 0) + self.assertGreaterEqual(mem.swap, 0) + + @unittest.skipIf(not HAS_MEMORY_MAPS, "not supported") + def test_memory_maps(self): + p = psutil.Process() + maps = p.memory_maps() + self.assertEqual(len(maps), len(set(maps))) + ext_maps = p.memory_maps(grouped=False) + + for nt in maps: + if not nt.path.startswith('['): + assert os.path.isabs(nt.path), nt.path + if POSIX: + try: + assert os.path.exists(nt.path) or os.path.islink( + nt.path + ), nt.path + except AssertionError: + if not LINUX: + raise + else: + # https://github.com/giampaolo/psutil/issues/759 + with open_text('/proc/self/smaps') as f: + data = f.read() + if "%s (deleted)" % nt.path not in data: + raise + else: + # XXX - On Windows we have this strange behavior with + # 64 bit dlls: they are visible via explorer but cannot + # be accessed via os.stat() (wtf?). + if '64' not in os.path.basename(nt.path): + try: + st = os.stat(nt.path) + except FileNotFoundError: + pass + else: + assert stat.S_ISREG(st.st_mode), nt.path + for nt in ext_maps: + for fname in nt._fields: + value = getattr(nt, fname) + if fname == 'path': + continue + if fname in ('addr', 'perms'): + assert value, value + else: + self.assertIsInstance(value, (int, long)) + assert value >= 0, value + + @unittest.skipIf(not HAS_MEMORY_MAPS, "not supported") + def test_memory_maps_lists_lib(self): + # Make sure a newly loaded shared lib is listed. + p = psutil.Process() + with copyload_shared_lib() as path: + + def normpath(p): + return os.path.realpath(os.path.normcase(p)) + + libpaths = [normpath(x.path) for x in p.memory_maps()] + self.assertIn(normpath(path), libpaths) + + def test_memory_percent(self): + p = psutil.Process() + p.memory_percent() + self.assertRaises(ValueError, p.memory_percent, memtype="?!?") + if LINUX or MACOS or WINDOWS: + p.memory_percent(memtype='uss') + + def test_is_running(self): + p = self.spawn_psproc() + assert p.is_running() + assert p.is_running() + p.kill() + p.wait() + assert not p.is_running() + assert not p.is_running() + + def test_exe(self): + p = self.spawn_psproc() + exe = p.exe() + try: + self.assertEqual(exe, PYTHON_EXE) + except AssertionError: + if WINDOWS and len(exe) == len(PYTHON_EXE): + # on Windows we don't care about case sensitivity + normcase = os.path.normcase + self.assertEqual(normcase(exe), normcase(PYTHON_EXE)) + else: + # certain platforms such as BSD are more accurate returning: + # "/usr/local/bin/python2.7" + # ...instead of: + # "/usr/local/bin/python" + # We do not want to consider this difference in accuracy + # an error. + ver = "%s.%s" % (sys.version_info[0], sys.version_info[1]) + try: + self.assertEqual( + exe.replace(ver, ''), PYTHON_EXE.replace(ver, '') + ) + except AssertionError: + # Typically MACOS. Really not sure what to do here. + pass + + out = sh([exe, "-c", "import os; print('hey')"]) + self.assertEqual(out, 'hey') + + def test_cmdline(self): + cmdline = [PYTHON_EXE, "-c", "import time; time.sleep(60)"] + p = self.spawn_psproc(cmdline) + # XXX - most of the times the underlying sysctl() call on Net + # and Open BSD returns a truncated string. + # Also /proc/pid/cmdline behaves the same so it looks + # like this is a kernel bug. + # XXX - AIX truncates long arguments in /proc/pid/cmdline + if NETBSD or OPENBSD or AIX: + self.assertEqual(p.cmdline()[0], PYTHON_EXE) + else: + if MACOS and CI_TESTING: + pyexe = p.cmdline()[0] + if pyexe != PYTHON_EXE: + self.assertEqual( + ' '.join(p.cmdline()[1:]), ' '.join(cmdline[1:]) + ) + return + self.assertEqual(' '.join(p.cmdline()), ' '.join(cmdline)) + + @unittest.skipIf(PYPY, "broken on PYPY") + def test_long_cmdline(self): + cmdline = [PYTHON_EXE] + cmdline.extend(["-v"] * 50) + cmdline.extend(["-c", "import time; time.sleep(10)"]) + p = self.spawn_psproc(cmdline) + if OPENBSD: + # XXX: for some reason the test process may turn into a + # zombie (don't know why). + try: + self.assertEqual(p.cmdline(), cmdline) + except psutil.ZombieProcess: + raise self.skipTest("OPENBSD: process turned into zombie") + else: + self.assertEqual(p.cmdline(), cmdline) + + def test_name(self): + p = self.spawn_psproc() + name = p.name().lower() + pyexe = os.path.basename(os.path.realpath(sys.executable)).lower() + assert pyexe.startswith(name), (pyexe, name) + + @unittest.skipIf(PYPY, "unreliable on PYPY") + def test_long_name(self): + pyexe = create_py_exe(self.get_testfn(suffix="0123456789" * 2)) + cmdline = [pyexe, "-c", "import time; time.sleep(10)"] + p = self.spawn_psproc(cmdline) + if OPENBSD: + # XXX: for some reason the test process may turn into a + # zombie (don't know why). Because the name() is long, all + # UNIX kernels truncate it to 15 chars, so internally psutil + # tries to guess the full name() from the cmdline(). But the + # cmdline() of a zombie on OpenBSD fails (internally), so we + # just compare the first 15 chars. Full explanation: + # https://github.com/giampaolo/psutil/issues/2239 + try: + self.assertEqual(p.name(), os.path.basename(pyexe)) + except AssertionError: + if p.status() == psutil.STATUS_ZOMBIE: + assert os.path.basename(pyexe).startswith(p.name()) + else: + raise + else: + self.assertEqual(p.name(), os.path.basename(pyexe)) + + # XXX + @unittest.skipIf(SUNOS, "broken on SUNOS") + @unittest.skipIf(AIX, "broken on AIX") + @unittest.skipIf(PYPY, "broken on PYPY") + def test_prog_w_funky_name(self): + # Test that name(), exe() and cmdline() correctly handle programs + # with funky chars such as spaces and ")", see: + # https://github.com/giampaolo/psutil/issues/628 + pyexe = create_py_exe(self.get_testfn(suffix='foo bar )')) + cmdline = [pyexe, "-c", "import time; time.sleep(10)"] + p = self.spawn_psproc(cmdline) + self.assertEqual(p.cmdline(), cmdline) + self.assertEqual(p.name(), os.path.basename(pyexe)) + self.assertEqual(os.path.normcase(p.exe()), os.path.normcase(pyexe)) + + @unittest.skipIf(not POSIX, 'POSIX only') + def test_uids(self): + p = psutil.Process() + real, effective, saved = p.uids() + # os.getuid() refers to "real" uid + self.assertEqual(real, os.getuid()) + # os.geteuid() refers to "effective" uid + self.assertEqual(effective, os.geteuid()) + # No such thing as os.getsuid() ("saved" uid), but starting + # from python 2.7 we have os.getresuid() which returns all + # of them. + if hasattr(os, "getresuid"): + self.assertEqual(os.getresuid(), p.uids()) + + @unittest.skipIf(not POSIX, 'POSIX only') + def test_gids(self): + p = psutil.Process() + real, effective, saved = p.gids() + # os.getuid() refers to "real" uid + self.assertEqual(real, os.getgid()) + # os.geteuid() refers to "effective" uid + self.assertEqual(effective, os.getegid()) + # No such thing as os.getsgid() ("saved" gid), but starting + # from python 2.7 we have os.getresgid() which returns all + # of them. + if hasattr(os, "getresuid"): + self.assertEqual(os.getresgid(), p.gids()) + + def test_nice(self): + def cleanup(init): + try: + p.nice(init) + except psutil.AccessDenied: + pass + + p = psutil.Process() + self.assertRaises(TypeError, p.nice, "str") + init = p.nice() + self.addCleanup(cleanup, init) + + if WINDOWS: + highest_prio = None + for prio in [ + psutil.IDLE_PRIORITY_CLASS, + psutil.BELOW_NORMAL_PRIORITY_CLASS, + psutil.NORMAL_PRIORITY_CLASS, + psutil.ABOVE_NORMAL_PRIORITY_CLASS, + psutil.HIGH_PRIORITY_CLASS, + psutil.REALTIME_PRIORITY_CLASS, + ]: + with self.subTest(prio=prio): + try: + p.nice(prio) + except psutil.AccessDenied: + pass + else: + new_prio = p.nice() + # The OS may limit our maximum priority, + # even if the function succeeds. For higher + # priorities, we match either the expected + # value or the highest so far. + if prio in ( + psutil.ABOVE_NORMAL_PRIORITY_CLASS, + psutil.HIGH_PRIORITY_CLASS, + psutil.REALTIME_PRIORITY_CLASS, + ): + if new_prio == prio or highest_prio is None: + highest_prio = prio + self.assertEqual(new_prio, highest_prio) + else: + self.assertEqual(new_prio, prio) + else: + try: + if hasattr(os, "getpriority"): + self.assertEqual( + os.getpriority(os.PRIO_PROCESS, os.getpid()), p.nice() + ) + p.nice(1) + self.assertEqual(p.nice(), 1) + if hasattr(os, "getpriority"): + self.assertEqual( + os.getpriority(os.PRIO_PROCESS, os.getpid()), p.nice() + ) + # XXX - going back to previous nice value raises + # AccessDenied on MACOS + if not MACOS: + p.nice(0) + self.assertEqual(p.nice(), 0) + except psutil.AccessDenied: + pass + + def test_status(self): + p = psutil.Process() + self.assertEqual(p.status(), psutil.STATUS_RUNNING) + + def test_username(self): + p = self.spawn_psproc() + username = p.username() + if WINDOWS: + domain, username = username.split('\\') + getpass_user = getpass.getuser() + if getpass_user.endswith('$'): + # When running as a service account (most likely to be + # NetworkService), these user name calculations don't produce + # the same result, causing the test to fail. + raise unittest.SkipTest('running as service account') + self.assertEqual(username, getpass_user) + if 'USERDOMAIN' in os.environ: + self.assertEqual(domain, os.environ['USERDOMAIN']) + else: + self.assertEqual(username, getpass.getuser()) + + def test_cwd(self): + p = self.spawn_psproc() + self.assertEqual(p.cwd(), os.getcwd()) + + def test_cwd_2(self): + cmd = [ + PYTHON_EXE, + "-c", + "import os, time; os.chdir('..'); time.sleep(60)", + ] + p = self.spawn_psproc(cmd) + call_until(p.cwd, "ret == os.path.dirname(os.getcwd())") + + @unittest.skipIf(not HAS_CPU_AFFINITY, 'not supported') + def test_cpu_affinity(self): + p = psutil.Process() + initial = p.cpu_affinity() + assert initial, initial + self.addCleanup(p.cpu_affinity, initial) + + if hasattr(os, "sched_getaffinity"): + self.assertEqual(initial, list(os.sched_getaffinity(p.pid))) + self.assertEqual(len(initial), len(set(initial))) + + all_cpus = list(range(len(psutil.cpu_percent(percpu=True)))) + for n in all_cpus: + p.cpu_affinity([n]) + self.assertEqual(p.cpu_affinity(), [n]) + if hasattr(os, "sched_getaffinity"): + self.assertEqual( + p.cpu_affinity(), list(os.sched_getaffinity(p.pid)) + ) + # also test num_cpu() + if hasattr(p, "num_cpu"): + self.assertEqual(p.cpu_affinity()[0], p.num_cpu()) + + # [] is an alias for "all eligible CPUs"; on Linux this may + # not be equal to all available CPUs, see: + # https://github.com/giampaolo/psutil/issues/956 + p.cpu_affinity([]) + if LINUX: + self.assertEqual(p.cpu_affinity(), p._proc._get_eligible_cpus()) + else: + self.assertEqual(p.cpu_affinity(), all_cpus) + if hasattr(os, "sched_getaffinity"): + self.assertEqual( + p.cpu_affinity(), list(os.sched_getaffinity(p.pid)) + ) + # + self.assertRaises(TypeError, p.cpu_affinity, 1) + p.cpu_affinity(initial) + # it should work with all iterables, not only lists + p.cpu_affinity(set(all_cpus)) + p.cpu_affinity(tuple(all_cpus)) + + @unittest.skipIf(not HAS_CPU_AFFINITY, 'not supported') + def test_cpu_affinity_errs(self): + p = self.spawn_psproc() + invalid_cpu = [len(psutil.cpu_times(percpu=True)) + 10] + self.assertRaises(ValueError, p.cpu_affinity, invalid_cpu) + self.assertRaises(ValueError, p.cpu_affinity, range(10000, 11000)) + self.assertRaises(TypeError, p.cpu_affinity, [0, "1"]) + self.assertRaises(ValueError, p.cpu_affinity, [0, -1]) + + @unittest.skipIf(not HAS_CPU_AFFINITY, 'not supported') + def test_cpu_affinity_all_combinations(self): + p = psutil.Process() + initial = p.cpu_affinity() + assert initial, initial + self.addCleanup(p.cpu_affinity, initial) + + # All possible CPU set combinations. + if len(initial) > 12: + initial = initial[:12] # ...otherwise it will take forever + combos = [] + for i in range(len(initial) + 1): + for subset in itertools.combinations(initial, i): + if subset: + combos.append(list(subset)) + + for combo in combos: + p.cpu_affinity(combo) + self.assertEqual(sorted(p.cpu_affinity()), sorted(combo)) + + # TODO: #595 + @unittest.skipIf(BSD, "broken on BSD") + # can't find any process file on Appveyor + @unittest.skipIf(APPVEYOR, "unreliable on APPVEYOR") + def test_open_files(self): + p = psutil.Process() + testfn = self.get_testfn() + files = p.open_files() + self.assertNotIn(testfn, files) + with open(testfn, 'wb') as f: + f.write(b'x' * 1024) + f.flush() + # give the kernel some time to see the new file + files = call_until(p.open_files, "len(ret) != %i" % len(files)) + filenames = [os.path.normcase(x.path) for x in files] + self.assertIn(os.path.normcase(testfn), filenames) + if LINUX: + for file in files: + if file.path == testfn: + self.assertEqual(file.position, 1024) + for file in files: + assert os.path.isfile(file.path), file + + # another process + cmdline = "import time; f = open(r'%s', 'r'); time.sleep(60);" % testfn + p = self.spawn_psproc([PYTHON_EXE, "-c", cmdline]) + + for x in range(100): + filenames = [os.path.normcase(x.path) for x in p.open_files()] + if testfn in filenames: + break + time.sleep(0.01) + else: + self.assertIn(os.path.normcase(testfn), filenames) + for file in filenames: + assert os.path.isfile(file), file + + # TODO: #595 + @unittest.skipIf(BSD, "broken on BSD") + # can't find any process file on Appveyor + @unittest.skipIf(APPVEYOR, "unreliable on APPVEYOR") + def test_open_files_2(self): + # test fd and path fields + p = psutil.Process() + normcase = os.path.normcase + testfn = self.get_testfn() + with open(testfn, 'w') as fileobj: + for file in p.open_files(): + if ( + normcase(file.path) == normcase(fileobj.name) + or file.fd == fileobj.fileno() + ): + break + else: + raise self.fail( + "no file found; files=%s" % (repr(p.open_files())) + ) + self.assertEqual(normcase(file.path), normcase(fileobj.name)) + if WINDOWS: + self.assertEqual(file.fd, -1) + else: + self.assertEqual(file.fd, fileobj.fileno()) + # test positions + ntuple = p.open_files()[0] + self.assertEqual(ntuple[0], ntuple.path) + self.assertEqual(ntuple[1], ntuple.fd) + # test file is gone + self.assertNotIn(fileobj.name, p.open_files()) + + @unittest.skipIf(not POSIX, 'POSIX only') + def test_num_fds(self): + p = psutil.Process() + testfn = self.get_testfn() + start = p.num_fds() + file = open(testfn, 'w') + self.addCleanup(file.close) + self.assertEqual(p.num_fds(), start + 1) + sock = socket.socket() + self.addCleanup(sock.close) + self.assertEqual(p.num_fds(), start + 2) + file.close() + sock.close() + self.assertEqual(p.num_fds(), start) + + @skip_on_not_implemented(only_if=LINUX) + @unittest.skipIf(OPENBSD or NETBSD, "not reliable on OPENBSD & NETBSD") + def test_num_ctx_switches(self): + p = psutil.Process() + before = sum(p.num_ctx_switches()) + for _ in range(2): + time.sleep(0.05) # this shall ensure a context switch happens + after = sum(p.num_ctx_switches()) + if after > before: + return + raise self.fail("num ctx switches still the same after 2 iterations") + + def test_ppid(self): + p = psutil.Process() + if hasattr(os, 'getppid'): + self.assertEqual(p.ppid(), os.getppid()) + p = self.spawn_psproc() + self.assertEqual(p.ppid(), os.getpid()) + + def test_parent(self): + p = self.spawn_psproc() + self.assertEqual(p.parent().pid, os.getpid()) + + lowest_pid = psutil.pids()[0] + self.assertIsNone(psutil.Process(lowest_pid).parent()) + + def test_parent_multi(self): + parent = psutil.Process() + child, grandchild = self.spawn_children_pair() + self.assertEqual(grandchild.parent(), child) + self.assertEqual(child.parent(), parent) + + @retry_on_failure() + def test_parents(self): + parent = psutil.Process() + assert parent.parents() + child, grandchild = self.spawn_children_pair() + self.assertEqual(child.parents()[0], parent) + self.assertEqual(grandchild.parents()[0], child) + self.assertEqual(grandchild.parents()[1], parent) + + def test_children(self): + parent = psutil.Process() + self.assertEqual(parent.children(), []) + self.assertEqual(parent.children(recursive=True), []) + # On Windows we set the flag to 0 in order to cancel out the + # CREATE_NO_WINDOW flag (enabled by default) which creates + # an extra "conhost.exe" child. + child = self.spawn_psproc(creationflags=0) + children1 = parent.children() + children2 = parent.children(recursive=True) + for children in (children1, children2): + self.assertEqual(len(children), 1) + self.assertEqual(children[0].pid, child.pid) + self.assertEqual(children[0].ppid(), parent.pid) + + def test_children_recursive(self): + # Test children() against two sub processes, p1 and p2, where + # p1 (our child) spawned p2 (our grandchild). + parent = psutil.Process() + child, grandchild = self.spawn_children_pair() + self.assertEqual(parent.children(), [child]) + self.assertEqual(parent.children(recursive=True), [child, grandchild]) + # If the intermediate process is gone there's no way for + # children() to recursively find it. + child.terminate() + child.wait() + self.assertEqual(parent.children(recursive=True), []) + + def test_children_duplicates(self): + # find the process which has the highest number of children + table = collections.defaultdict(int) + for p in psutil.process_iter(): + try: + table[p.ppid()] += 1 + except psutil.Error: + pass + # this is the one, now let's make sure there are no duplicates + pid = sorted(table.items(), key=lambda x: x[1])[-1][0] + if LINUX and pid == 0: + raise self.skipTest("PID 0") + p = psutil.Process(pid) + try: + c = p.children(recursive=True) + except psutil.AccessDenied: # windows + pass + else: + self.assertEqual(len(c), len(set(c))) + + def test_parents_and_children(self): + parent = psutil.Process() + child, grandchild = self.spawn_children_pair() + # forward + children = parent.children(recursive=True) + self.assertEqual(len(children), 2) + self.assertEqual(children[0], child) + self.assertEqual(children[1], grandchild) + # backward + parents = grandchild.parents() + self.assertEqual(parents[0], child) + self.assertEqual(parents[1], parent) + + def test_suspend_resume(self): + p = self.spawn_psproc() + p.suspend() + for _ in range(100): + if p.status() == psutil.STATUS_STOPPED: + break + time.sleep(0.01) + p.resume() + self.assertNotEqual(p.status(), psutil.STATUS_STOPPED) + + def test_invalid_pid(self): + self.assertRaises(TypeError, psutil.Process, "1") + self.assertRaises(ValueError, psutil.Process, -1) + + def test_as_dict(self): + p = psutil.Process() + d = p.as_dict(attrs=['exe', 'name']) + self.assertEqual(sorted(d.keys()), ['exe', 'name']) + + p = psutil.Process(min(psutil.pids())) + d = p.as_dict(attrs=['connections'], ad_value='foo') + if not isinstance(d['connections'], list): + self.assertEqual(d['connections'], 'foo') + + # Test ad_value is set on AccessDenied. + with mock.patch( + 'psutil.Process.nice', create=True, side_effect=psutil.AccessDenied + ): + self.assertEqual( + p.as_dict(attrs=["nice"], ad_value=1), {"nice": 1} + ) + + # Test that NoSuchProcess bubbles up. + with mock.patch( + 'psutil.Process.nice', + create=True, + side_effect=psutil.NoSuchProcess(p.pid, "name"), + ): + self.assertRaises(psutil.NoSuchProcess, p.as_dict, attrs=["nice"]) + + # Test that ZombieProcess is swallowed. + with mock.patch( + 'psutil.Process.nice', + create=True, + side_effect=psutil.ZombieProcess(p.pid, "name"), + ): + self.assertEqual( + p.as_dict(attrs=["nice"], ad_value="foo"), {"nice": "foo"} + ) + + # By default APIs raising NotImplementedError are + # supposed to be skipped. + with mock.patch( + 'psutil.Process.nice', create=True, side_effect=NotImplementedError + ): + d = p.as_dict() + self.assertNotIn('nice', list(d.keys())) + # ...unless the user explicitly asked for some attr. + with self.assertRaises(NotImplementedError): + p.as_dict(attrs=["nice"]) + + # errors + with self.assertRaises(TypeError): + p.as_dict('name') + with self.assertRaises(ValueError): + p.as_dict(['foo']) + with self.assertRaises(ValueError): + p.as_dict(['foo', 'bar']) + + def test_oneshot(self): + p = psutil.Process() + with mock.patch("psutil._psplatform.Process.cpu_times") as m: + with p.oneshot(): + p.cpu_times() + p.cpu_times() + self.assertEqual(m.call_count, 1) + + with mock.patch("psutil._psplatform.Process.cpu_times") as m: + p.cpu_times() + p.cpu_times() + self.assertEqual(m.call_count, 2) + + def test_oneshot_twice(self): + # Test the case where the ctx manager is __enter__ed twice. + # The second __enter__ is supposed to resut in a NOOP. + p = psutil.Process() + with mock.patch("psutil._psplatform.Process.cpu_times") as m1: + with mock.patch("psutil._psplatform.Process.oneshot_enter") as m2: + with p.oneshot(): + p.cpu_times() + p.cpu_times() + with p.oneshot(): + p.cpu_times() + p.cpu_times() + self.assertEqual(m1.call_count, 1) + self.assertEqual(m2.call_count, 1) + + with mock.patch("psutil._psplatform.Process.cpu_times") as m: + p.cpu_times() + p.cpu_times() + self.assertEqual(m.call_count, 2) + + def test_oneshot_cache(self): + # Make sure oneshot() cache is nonglobal. Instead it's + # supposed to be bound to the Process instance, see: + # https://github.com/giampaolo/psutil/issues/1373 + p1, p2 = self.spawn_children_pair() + p1_ppid = p1.ppid() + p2_ppid = p2.ppid() + self.assertNotEqual(p1_ppid, p2_ppid) + with p1.oneshot(): + self.assertEqual(p1.ppid(), p1_ppid) + self.assertEqual(p2.ppid(), p2_ppid) + with p2.oneshot(): + self.assertEqual(p1.ppid(), p1_ppid) + self.assertEqual(p2.ppid(), p2_ppid) + + def test_halfway_terminated_process(self): + # Test that NoSuchProcess exception gets raised in case the + # process dies after we create the Process object. + # Example: + # >>> proc = Process(1234) + # >>> time.sleep(2) # time-consuming task, process dies in meantime + # >>> proc.name() + # Refers to Issue #15 + def assert_raises_nsp(fun, fun_name): + try: + ret = fun() + except psutil.ZombieProcess: # differentiate from NSP + raise + except psutil.NoSuchProcess: + pass + except psutil.AccessDenied: + if OPENBSD and fun_name in ('threads', 'num_threads'): + return + raise + else: + # NtQuerySystemInformation succeeds even if process is gone. + if WINDOWS and fun_name in ('exe', 'name'): + return + raise self.fail( + "%r didn't raise NSP and returned %r instead" % (fun, ret) + ) + + p = self.spawn_psproc() + p.terminate() + p.wait() + if WINDOWS: # XXX + call_until(psutil.pids, "%s not in ret" % p.pid) + self.assertProcessGone(p) + + ns = process_namespace(p) + for fun, name in ns.iter(ns.all): + assert_raises_nsp(fun, name) + + @unittest.skipIf(not POSIX, 'POSIX only') + def test_zombie_process(self): + parent, zombie = self.spawn_zombie() + self.assertProcessZombie(zombie) + + @unittest.skipIf(not POSIX, 'POSIX only') + def test_zombie_process_is_running_w_exc(self): + # Emulate a case where internally is_running() raises + # ZombieProcess. + p = psutil.Process() + with mock.patch( + "psutil.Process", side_effect=psutil.ZombieProcess(0) + ) as m: + assert p.is_running() + assert m.called + + @unittest.skipIf(not POSIX, 'POSIX only') + def test_zombie_process_status_w_exc(self): + # Emulate a case where internally status() raises + # ZombieProcess. + p = psutil.Process() + with mock.patch( + "psutil._psplatform.Process.status", + side_effect=psutil.ZombieProcess(0), + ) as m: + self.assertEqual(p.status(), psutil.STATUS_ZOMBIE) + assert m.called + + def test_reused_pid(self): + # Emulate a case where PID has been reused by another process. + subp = self.spawn_testproc() + p = psutil.Process(subp.pid) + p._ident = (p.pid, p.create_time() + 100) + assert not p.is_running() + assert p != psutil.Process(subp.pid) + msg = "process no longer exists and its PID has been reused" + ns = process_namespace(p) + for fun, name in ns.iter(ns.setters + ns.killers, clear_cache=False): + with self.subTest(name=name): + self.assertRaisesRegex(psutil.NoSuchProcess, msg, fun) + self.assertRaisesRegex(psutil.NoSuchProcess, msg, p.ppid) + self.assertRaisesRegex(psutil.NoSuchProcess, msg, p.parent) + self.assertRaisesRegex(psutil.NoSuchProcess, msg, p.parents) + self.assertRaisesRegex(psutil.NoSuchProcess, msg, p.children) + + def test_pid_0(self): + # Process(0) is supposed to work on all platforms except Linux + if 0 not in psutil.pids(): + self.assertRaises(psutil.NoSuchProcess, psutil.Process, 0) + # These 2 are a contradiction, but "ps" says PID 1's parent + # is PID 0. + assert not psutil.pid_exists(0) + self.assertEqual(psutil.Process(1).ppid(), 0) + return + + p = psutil.Process(0) + exc = psutil.AccessDenied if WINDOWS else ValueError + self.assertRaises(exc, p.wait) + self.assertRaises(exc, p.terminate) + self.assertRaises(exc, p.suspend) + self.assertRaises(exc, p.resume) + self.assertRaises(exc, p.kill) + self.assertRaises(exc, p.send_signal, signal.SIGTERM) + + # test all methods + ns = process_namespace(p) + for fun, name in ns.iter(ns.getters + ns.setters): + try: + ret = fun() + except psutil.AccessDenied: + pass + else: + if name in ("uids", "gids"): + self.assertEqual(ret.real, 0) + elif name == "username": + user = 'NT AUTHORITY\\SYSTEM' if WINDOWS else 'root' + self.assertEqual(p.username(), user) + elif name == "name": + assert name, name + + if not OPENBSD: + self.assertIn(0, psutil.pids()) + assert psutil.pid_exists(0) + + @unittest.skipIf(not HAS_ENVIRON, "not supported") + def test_environ(self): + def clean_dict(d): + # Most of these are problematic on Travis. + d.pop("PLAT", None) + d.pop("HOME", None) + if MACOS: + d.pop("__CF_USER_TEXT_ENCODING", None) + d.pop("VERSIONER_PYTHON_PREFER_32_BIT", None) + d.pop("VERSIONER_PYTHON_VERSION", None) + return dict([ + ( + k.replace("\r", "").replace("\n", ""), + v.replace("\r", "").replace("\n", ""), + ) + for k, v in d.items() + ]) + + self.maxDiff = None + p = psutil.Process() + d1 = clean_dict(p.environ()) + d2 = clean_dict(os.environ.copy()) + if not OSX and GITHUB_ACTIONS: + self.assertEqual(d1, d2) + + @unittest.skipIf(not HAS_ENVIRON, "not supported") + @unittest.skipIf(not POSIX, "POSIX only") + @unittest.skipIf( + MACOS_11PLUS, + "macOS 11+ can't get another process environment, issue #2084", + ) + def test_weird_environ(self): + # environment variables can contain values without an equals sign + code = textwrap.dedent(""" + #include + #include + + char * const argv[] = {"cat", 0}; + char * const envp[] = {"A=1", "X", "C=3", 0}; + + int main(void) { + // Close stderr on exec so parent can wait for the + // execve to finish. + if (fcntl(2, F_SETFD, FD_CLOEXEC) != 0) + return 0; + return execve("/bin/cat", argv, envp); + } + """) + cexe = create_c_exe(self.get_testfn(), c_code=code) + sproc = self.spawn_testproc( + [cexe], stdin=subprocess.PIPE, stderr=subprocess.PIPE + ) + p = psutil.Process(sproc.pid) + wait_for_pid(p.pid) + assert p.is_running() + # Wait for process to exec or exit. + self.assertEqual(sproc.stderr.read(), b"") + if MACOS and CI_TESTING: + try: + env = p.environ() + except psutil.AccessDenied: + # XXX: fails sometimes with: + # PermissionError from 'sysctl(KERN_PROCARGS2) -> EIO' + return + else: + env = p.environ() + self.assertEqual(env, {"A": "1", "C": "3"}) + sproc.communicate() + self.assertEqual(sproc.returncode, 0) + + +# =================================================================== +# --- Limited user tests +# =================================================================== + + +if POSIX and os.getuid() == 0: + + class LimitedUserTestCase(TestProcess): + """Repeat the previous tests by using a limited user. + Executed only on UNIX and only if the user who run the test script + is root. + """ + + # the uid/gid the test suite runs under + if hasattr(os, 'getuid'): + PROCESS_UID = os.getuid() + PROCESS_GID = os.getgid() + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # re-define all existent test methods in order to + # ignore AccessDenied exceptions + for attr in [x for x in dir(self) if x.startswith('test')]: + meth = getattr(self, attr) + + def test_(self): + try: + meth() # noqa + except psutil.AccessDenied: + pass + + setattr(self, attr, types.MethodType(test_, self)) + + def setUp(self): + super().setUp() + os.setegid(1000) + os.seteuid(1000) + + def tearDown(self): + os.setegid(self.PROCESS_UID) + os.seteuid(self.PROCESS_GID) + super().tearDown() + + def test_nice(self): + try: + psutil.Process().nice(-1) + except psutil.AccessDenied: + pass + else: + raise self.fail("exception not raised") + + @unittest.skipIf(1, "causes problem as root") + def test_zombie_process(self): + pass + + +# =================================================================== +# --- psutil.Popen tests +# =================================================================== + + +class TestPopen(PsutilTestCase): + """Tests for psutil.Popen class.""" + + @classmethod + def tearDownClass(cls): + reap_children() + + def test_misc(self): + # XXX this test causes a ResourceWarning on Python 3 because + # psutil.__subproc instance doesn't get properly freed. + # Not sure what to do though. + cmd = [PYTHON_EXE, "-c", "import time; time.sleep(60);"] + with psutil.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=PYTHON_EXE_ENV, + ) as proc: + proc.name() + proc.cpu_times() + proc.stdin # noqa + self.assertTrue(dir(proc)) + self.assertRaises(AttributeError, getattr, proc, 'foo') + proc.terminate() + if POSIX: + self.assertEqual(proc.wait(5), -signal.SIGTERM) + else: + self.assertEqual(proc.wait(5), signal.SIGTERM) + + def test_ctx_manager(self): + with psutil.Popen( + [PYTHON_EXE, "-V"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + stdin=subprocess.PIPE, + env=PYTHON_EXE_ENV, + ) as proc: + proc.communicate() + assert proc.stdout.closed + assert proc.stderr.closed + assert proc.stdin.closed + self.assertEqual(proc.returncode, 0) + + def test_kill_terminate(self): + # subprocess.Popen()'s terminate(), kill() and send_signal() do + # not raise exception after the process is gone. psutil.Popen + # diverges from that. + cmd = [PYTHON_EXE, "-c", "import time; time.sleep(60);"] + with psutil.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=PYTHON_EXE_ENV, + ) as proc: + proc.terminate() + proc.wait() + self.assertRaises(psutil.NoSuchProcess, proc.terminate) + self.assertRaises(psutil.NoSuchProcess, proc.kill) + self.assertRaises( + psutil.NoSuchProcess, proc.send_signal, signal.SIGTERM + ) + if WINDOWS: + self.assertRaises( + psutil.NoSuchProcess, proc.send_signal, signal.CTRL_C_EVENT + ) + self.assertRaises( + psutil.NoSuchProcess, + proc.send_signal, + signal.CTRL_BREAK_EVENT, + ) + + +if __name__ == '__main__': + from psutil.tests.runner import run_from_name + + run_from_name(__file__) diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_process.pyc b/addon/globalPlugins/soundmanager/psutil/tests/test_process.pyc deleted file mode 100644 index bb73a5c..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/tests/test_process.pyc and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_process_all.py b/addon/globalPlugins/soundmanager/psutil/tests/test_process_all.py new file mode 100644 index 0000000..1e4eceb --- /dev/null +++ b/addon/globalPlugins/soundmanager/psutil/tests/test_process_all.py @@ -0,0 +1,464 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Iterate over all process PIDs and for each one of them invoke and +test all psutil.Process() methods. +""" + +import enum +import errno +import multiprocessing +import os +import stat +import time +import traceback + +import psutil +from psutil import AIX +from psutil import BSD +from psutil import FREEBSD +from psutil import LINUX +from psutil import MACOS +from psutil import NETBSD +from psutil import OPENBSD +from psutil import OSX +from psutil import POSIX +from psutil import WINDOWS +from psutil._compat import PY3 +from psutil._compat import long +from psutil._compat import unicode +from psutil.tests import CI_TESTING +from psutil.tests import VALID_PROC_STATUSES +from psutil.tests import PsutilTestCase +from psutil.tests import check_connection_ntuple +from psutil.tests import create_sockets +from psutil.tests import is_namedtuple +from psutil.tests import is_win_secure_system_proc +from psutil.tests import process_namespace +from psutil.tests import serialrun + + +# Cuts the time in half, but (e.g.) on macOS the process pool stays +# alive after join() (multiprocessing bug?), messing up other tests. +USE_PROC_POOL = LINUX and not CI_TESTING + + +def proc_info(pid): + tcase = PsutilTestCase() + + def check_exception(exc, proc, name, ppid): + tcase.assertEqual(exc.pid, pid) + if exc.name is not None: + tcase.assertEqual(exc.name, name) + if isinstance(exc, psutil.ZombieProcess): + tcase.assertProcessZombie(proc) + if exc.ppid is not None: + tcase.assertGreaterEqual(exc.ppid, 0) + tcase.assertEqual(exc.ppid, ppid) + elif isinstance(exc, psutil.NoSuchProcess): + tcase.assertProcessGone(proc) + str(exc) + repr(exc) + + def do_wait(): + if pid != 0: + try: + proc.wait(0) + except psutil.Error as exc: + check_exception(exc, proc, name, ppid) + + try: + proc = psutil.Process(pid) + except psutil.NoSuchProcess: + tcase.assertPidGone(pid) + return {} + try: + d = proc.as_dict(['ppid', 'name']) + except psutil.NoSuchProcess: + tcase.assertProcessGone(proc) + else: + name, ppid = d['name'], d['ppid'] + info = {'pid': proc.pid} + ns = process_namespace(proc) + # We don't use oneshot() because in order not to fool + # check_exception() in case of NSP. + for fun, fun_name in ns.iter(ns.getters, clear_cache=False): + try: + info[fun_name] = fun() + except psutil.Error as exc: + check_exception(exc, proc, name, ppid) + continue + do_wait() + return info + + +@serialrun +class TestFetchAllProcesses(PsutilTestCase): + """Test which iterates over all running processes and performs + some sanity checks against Process API's returned values. + Uses a process pool to get info about all processes. + """ + + def setUp(self): + # Using a pool in a CI env may result in deadlock, see: + # https://github.com/giampaolo/psutil/issues/2104 + if USE_PROC_POOL: + self.pool = multiprocessing.Pool() + + def tearDown(self): + if USE_PROC_POOL: + self.pool.terminate() + self.pool.join() + + def iter_proc_info(self): + # Fixes "can't pickle : it's not the + # same object as test_process_all.proc_info". + from psutil.tests.test_process_all import proc_info + + if USE_PROC_POOL: + return self.pool.imap_unordered(proc_info, psutil.pids()) + else: + ls = [] + for pid in psutil.pids(): + ls.append(proc_info(pid)) + return ls + + def test_all(self): + failures = [] + for info in self.iter_proc_info(): + for name, value in info.items(): + meth = getattr(self, name) + try: + meth(value, info) + except Exception: # noqa: BLE001 + s = '\n' + '=' * 70 + '\n' + s += "FAIL: name=test_%s, pid=%s, ret=%s\ninfo=%s\n" % ( + name, + info['pid'], + repr(value), + info, + ) + s += '-' * 70 + s += "\n%s" % traceback.format_exc() + s = "\n".join((" " * 4) + i for i in s.splitlines()) + "\n" + failures.append(s) + else: + if value not in (0, 0.0, [], None, '', {}): + assert value, value + if failures: + raise self.fail(''.join(failures)) + + def cmdline(self, ret, info): + self.assertIsInstance(ret, list) + for part in ret: + self.assertIsInstance(part, str) + + def exe(self, ret, info): + self.assertIsInstance(ret, (str, unicode)) + self.assertEqual(ret.strip(), ret) + if ret: + if WINDOWS and not ret.endswith('.exe'): + return # May be "Registry", "MemCompression", ... + assert os.path.isabs(ret), ret + # Note: os.stat() may return False even if the file is there + # hence we skip the test, see: + # http://stackoverflow.com/questions/3112546/os-path-exists-lies + if POSIX and os.path.isfile(ret): + if hasattr(os, 'access') and hasattr(os, "X_OK"): + # XXX: may fail on MACOS + try: + assert os.access(ret, os.X_OK) + except AssertionError: + if os.path.exists(ret) and not CI_TESTING: + raise + + def pid(self, ret, info): + self.assertIsInstance(ret, int) + self.assertGreaterEqual(ret, 0) + + def ppid(self, ret, info): + self.assertIsInstance(ret, (int, long)) + self.assertGreaterEqual(ret, 0) + proc_info(ret) + + def name(self, ret, info): + self.assertIsInstance(ret, (str, unicode)) + if WINDOWS and not ret and is_win_secure_system_proc(info['pid']): + # https://github.com/giampaolo/psutil/issues/2338 + return + # on AIX, "" processes don't have names + if not AIX: + assert ret, repr(ret) + + def create_time(self, ret, info): + self.assertIsInstance(ret, float) + try: + self.assertGreaterEqual(ret, 0) + except AssertionError: + # XXX + if OPENBSD and info['status'] == psutil.STATUS_ZOMBIE: + pass + else: + raise + # this can't be taken for granted on all platforms + # self.assertGreaterEqual(ret, psutil.boot_time()) + # make sure returned value can be pretty printed + # with strftime + time.strftime("%Y %m %d %H:%M:%S", time.localtime(ret)) + + def uids(self, ret, info): + assert is_namedtuple(ret) + for uid in ret: + self.assertIsInstance(uid, int) + self.assertGreaterEqual(uid, 0) + + def gids(self, ret, info): + assert is_namedtuple(ret) + # note: testing all gids as above seems not to be reliable for + # gid == 30 (nodoby); not sure why. + for gid in ret: + self.assertIsInstance(gid, int) + if not MACOS and not NETBSD: + self.assertGreaterEqual(gid, 0) + + def username(self, ret, info): + self.assertIsInstance(ret, str) + self.assertEqual(ret.strip(), ret) + assert ret.strip() + + def status(self, ret, info): + self.assertIsInstance(ret, str) + assert ret, ret + self.assertNotEqual(ret, '?') # XXX + self.assertIn(ret, VALID_PROC_STATUSES) + + def io_counters(self, ret, info): + assert is_namedtuple(ret) + for field in ret: + self.assertIsInstance(field, (int, long)) + if field != -1: + self.assertGreaterEqual(field, 0) + + def ionice(self, ret, info): + if LINUX: + self.assertIsInstance(ret.ioclass, int) + self.assertIsInstance(ret.value, int) + self.assertGreaterEqual(ret.ioclass, 0) + self.assertGreaterEqual(ret.value, 0) + else: # Windows, Cygwin + choices = [ + psutil.IOPRIO_VERYLOW, + psutil.IOPRIO_LOW, + psutil.IOPRIO_NORMAL, + psutil.IOPRIO_HIGH, + ] + self.assertIsInstance(ret, int) + self.assertGreaterEqual(ret, 0) + self.assertIn(ret, choices) + + def num_threads(self, ret, info): + self.assertIsInstance(ret, int) + if WINDOWS and ret == 0 and is_win_secure_system_proc(info['pid']): + # https://github.com/giampaolo/psutil/issues/2338 + return + self.assertGreaterEqual(ret, 1) + + def threads(self, ret, info): + self.assertIsInstance(ret, list) + for t in ret: + assert is_namedtuple(t) + self.assertGreaterEqual(t.id, 0) + self.assertGreaterEqual(t.user_time, 0) + self.assertGreaterEqual(t.system_time, 0) + for field in t: + self.assertIsInstance(field, (int, float)) + + def cpu_times(self, ret, info): + assert is_namedtuple(ret) + for n in ret: + self.assertIsInstance(n, float) + self.assertGreaterEqual(n, 0) + # TODO: check ntuple fields + + def cpu_percent(self, ret, info): + self.assertIsInstance(ret, float) + assert 0.0 <= ret <= 100.0, ret + + def cpu_num(self, ret, info): + self.assertIsInstance(ret, int) + if FREEBSD and ret == -1: + return + self.assertGreaterEqual(ret, 0) + if psutil.cpu_count() == 1: + self.assertEqual(ret, 0) + self.assertIn(ret, list(range(psutil.cpu_count()))) + + def memory_info(self, ret, info): + assert is_namedtuple(ret) + for value in ret: + self.assertIsInstance(value, (int, long)) + self.assertGreaterEqual(value, 0) + if WINDOWS: + self.assertGreaterEqual(ret.peak_wset, ret.wset) + self.assertGreaterEqual(ret.peak_paged_pool, ret.paged_pool) + self.assertGreaterEqual(ret.peak_nonpaged_pool, ret.nonpaged_pool) + self.assertGreaterEqual(ret.peak_pagefile, ret.pagefile) + + def memory_full_info(self, ret, info): + assert is_namedtuple(ret) + total = psutil.virtual_memory().total + for name in ret._fields: + value = getattr(ret, name) + self.assertIsInstance(value, (int, long)) + self.assertGreaterEqual(value, 0, msg=(name, value)) + if LINUX or OSX and name in ('vms', 'data'): + # On Linux there are processes (e.g. 'goa-daemon') whose + # VMS is incredibly high for some reason. + continue + self.assertLessEqual(value, total, msg=(name, value, total)) + + if LINUX: + self.assertGreaterEqual(ret.pss, ret.uss) + + def open_files(self, ret, info): + self.assertIsInstance(ret, list) + for f in ret: + self.assertIsInstance(f.fd, int) + self.assertIsInstance(f.path, str) + self.assertEqual(f.path.strip(), f.path) + if WINDOWS: + self.assertEqual(f.fd, -1) + elif LINUX: + self.assertIsInstance(f.position, int) + self.assertIsInstance(f.mode, str) + self.assertIsInstance(f.flags, int) + self.assertGreaterEqual(f.position, 0) + self.assertIn(f.mode, ('r', 'w', 'a', 'r+', 'a+')) + self.assertGreater(f.flags, 0) + elif BSD and not f.path: + # XXX see: https://github.com/giampaolo/psutil/issues/595 + continue + assert os.path.isabs(f.path), f + try: + st = os.stat(f.path) + except FileNotFoundError: + pass + else: + assert stat.S_ISREG(st.st_mode), f + + def num_fds(self, ret, info): + self.assertIsInstance(ret, int) + self.assertGreaterEqual(ret, 0) + + def connections(self, ret, info): + with create_sockets(): + self.assertEqual(len(ret), len(set(ret))) + for conn in ret: + assert is_namedtuple(conn) + check_connection_ntuple(conn) + + def cwd(self, ret, info): + self.assertIsInstance(ret, (str, unicode)) + self.assertEqual(ret.strip(), ret) + if ret: + assert os.path.isabs(ret), ret + try: + st = os.stat(ret) + except OSError as err: + if WINDOWS and psutil._psplatform.is_permission_err(err): + pass + # directory has been removed in mean time + elif err.errno != errno.ENOENT: + raise + else: + assert stat.S_ISDIR(st.st_mode) + + def memory_percent(self, ret, info): + self.assertIsInstance(ret, float) + assert 0 <= ret <= 100, ret + + def is_running(self, ret, info): + self.assertIsInstance(ret, bool) + + def cpu_affinity(self, ret, info): + self.assertIsInstance(ret, list) + self.assertNotEqual(ret, []) + cpus = list(range(psutil.cpu_count())) + for n in ret: + self.assertIsInstance(n, int) + self.assertIn(n, cpus) + + def terminal(self, ret, info): + self.assertIsInstance(ret, (str, type(None))) + if ret is not None: + assert os.path.isabs(ret), ret + assert os.path.exists(ret), ret + + def memory_maps(self, ret, info): + for nt in ret: + self.assertIsInstance(nt.addr, str) + self.assertIsInstance(nt.perms, str) + self.assertIsInstance(nt.path, str) + for fname in nt._fields: + value = getattr(nt, fname) + if fname == 'path': + if not value.startswith(("[", "anon_inode:")): + assert os.path.isabs(nt.path), nt.path + # commented as on Linux we might get + # '/foo/bar (deleted)' + # assert os.path.exists(nt.path), nt.path + elif fname == 'addr': + assert value, repr(value) + elif fname == 'perms': + if not WINDOWS: + assert value, repr(value) + else: + self.assertIsInstance(value, (int, long)) + self.assertGreaterEqual(value, 0) + + def num_handles(self, ret, info): + self.assertIsInstance(ret, int) + self.assertGreaterEqual(ret, 0) + + def nice(self, ret, info): + self.assertIsInstance(ret, int) + if POSIX: + assert -20 <= ret <= 20, ret + else: + priorities = [ + getattr(psutil, x) + for x in dir(psutil) + if x.endswith('_PRIORITY_CLASS') + ] + self.assertIn(ret, priorities) + if PY3: + self.assertIsInstance(ret, enum.IntEnum) + else: + self.assertIsInstance(ret, int) + + def num_ctx_switches(self, ret, info): + assert is_namedtuple(ret) + for value in ret: + self.assertIsInstance(value, (int, long)) + self.assertGreaterEqual(value, 0) + + def rlimit(self, ret, info): + self.assertIsInstance(ret, tuple) + self.assertEqual(len(ret), 2) + self.assertGreaterEqual(ret[0], -1) + self.assertGreaterEqual(ret[1], -1) + + def environ(self, ret, info): + self.assertIsInstance(ret, dict) + for k, v in ret.items(): + self.assertIsInstance(k, str) + self.assertIsInstance(v, str) + + +if __name__ == '__main__': + from psutil.tests.runner import run_from_name + + run_from_name(__file__) diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_sunos.py b/addon/globalPlugins/soundmanager/psutil/tests/test_sunos.py index 94405d4..93f01e9 100644 --- a/addon/globalPlugins/soundmanager/psutil/tests/test_sunos.py +++ b/addon/globalPlugins/soundmanager/psutil/tests/test_sunos.py @@ -1,45 +1,45 @@ -#!/usr/bin/env python - -# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -"""Sun OS specific tests.""" - -import os - -import psutil -from psutil import SUNOS -from psutil.tests import sh -from psutil.tests import unittest - - -@unittest.skipIf(not SUNOS, "SUNOS only") -class SunOSSpecificTestCase(unittest.TestCase): - - def test_swap_memory(self): - out = sh('env PATH=/usr/sbin:/sbin:%s swap -l' % os.environ['PATH']) - lines = out.strip().split('\n')[1:] - if not lines: - raise ValueError('no swap device(s) configured') - total = free = 0 - for line in lines: - line = line.split() - t, f = line[-2:] - total += int(int(t) * 512) - free += int(int(f) * 512) - used = total - free - - psutil_swap = psutil.swap_memory() - self.assertEqual(psutil_swap.total, total) - self.assertEqual(psutil_swap.used, used) - self.assertEqual(psutil_swap.free, free) - - def test_cpu_count(self): - out = sh("/usr/sbin/psrinfo") - self.assertEqual(psutil.cpu_count(), len(out.split('\n'))) - - -if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Sun OS specific tests.""" + +import os +import unittest + +import psutil +from psutil import SUNOS +from psutil.tests import PsutilTestCase +from psutil.tests import sh + + +@unittest.skipIf(not SUNOS, "SUNOS only") +class SunOSSpecificTestCase(PsutilTestCase): + def test_swap_memory(self): + out = sh('env PATH=/usr/sbin:/sbin:%s swap -l' % os.environ['PATH']) + lines = out.strip().split('\n')[1:] + if not lines: + raise ValueError('no swap device(s) configured') + total = free = 0 + for line in lines: + fields = line.split() + total = int(fields[3]) * 512 + free = int(fields[4]) * 512 + used = total - free + + psutil_swap = psutil.swap_memory() + self.assertEqual(psutil_swap.total, total) + self.assertEqual(psutil_swap.used, used) + self.assertEqual(psutil_swap.free, free) + + def test_cpu_count(self): + out = sh("/usr/sbin/psrinfo") + self.assertEqual(psutil.cpu_count(), len(out.split('\n'))) + + +if __name__ == '__main__': + from psutil.tests.runner import run_from_name + + run_from_name(__file__) diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_sunos.pyc b/addon/globalPlugins/soundmanager/psutil/tests/test_sunos.pyc deleted file mode 100644 index 26b673b..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/tests/test_sunos.pyc and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_system.py b/addon/globalPlugins/soundmanager/psutil/tests/test_system.py index c4c503a..07631fb 100644 --- a/addon/globalPlugins/soundmanager/psutil/tests/test_system.py +++ b/addon/globalPlugins/soundmanager/psutil/tests/test_system.py @@ -1,881 +1,967 @@ -#!/usr/bin/env python - -# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -"""Tests for system APIS.""" - -import contextlib -import datetime -import errno -import os -import pprint -import shutil -import signal -import socket -import sys -import tempfile -import time - -import psutil -from psutil import AIX -from psutil import BSD -from psutil import FREEBSD -from psutil import LINUX -from psutil import MACOS -from psutil import NETBSD -from psutil import OPENBSD -from psutil import POSIX -from psutil import SUNOS -from psutil import WINDOWS -from psutil._compat import long -from psutil.tests import APPVEYOR -from psutil.tests import ASCII_FS -from psutil.tests import check_net_address -from psutil.tests import DEVNULL -from psutil.tests import enum -from psutil.tests import get_test_subprocess -from psutil.tests import HAS_BATTERY -from psutil.tests import HAS_CPU_FREQ -from psutil.tests import HAS_GETLOADAVG -from psutil.tests import HAS_NET_IO_COUNTERS -from psutil.tests import HAS_SENSORS_BATTERY -from psutil.tests import HAS_SENSORS_FANS -from psutil.tests import HAS_SENSORS_TEMPERATURES -from psutil.tests import mock -from psutil.tests import reap_children -from psutil.tests import retry_on_failure -from psutil.tests import safe_rmpath -from psutil.tests import TESTFN -from psutil.tests import TESTFN_UNICODE -from psutil.tests import TRAVIS -from psutil.tests import unittest - - -# =================================================================== -# --- System-related API tests -# =================================================================== - - -class TestSystemAPIs(unittest.TestCase): - """Tests for system-related APIs.""" - - def setUp(self): - safe_rmpath(TESTFN) - - def tearDown(self): - reap_children() - - def test_process_iter(self): - self.assertIn(os.getpid(), [x.pid for x in psutil.process_iter()]) - sproc = get_test_subprocess() - self.assertIn(sproc.pid, [x.pid for x in psutil.process_iter()]) - p = psutil.Process(sproc.pid) - p.kill() - p.wait() - self.assertNotIn(sproc.pid, [x.pid for x in psutil.process_iter()]) - - with mock.patch('psutil.Process', - side_effect=psutil.NoSuchProcess(os.getpid())): - self.assertEqual(list(psutil.process_iter()), []) - with mock.patch('psutil.Process', - side_effect=psutil.AccessDenied(os.getpid())): - with self.assertRaises(psutil.AccessDenied): - list(psutil.process_iter()) - - def test_prcess_iter_w_params(self): - for p in psutil.process_iter(attrs=['pid']): - self.assertEqual(list(p.info.keys()), ['pid']) - with self.assertRaises(ValueError): - list(psutil.process_iter(attrs=['foo'])) - with mock.patch("psutil._psplatform.Process.cpu_times", - side_effect=psutil.AccessDenied(0, "")) as m: - for p in psutil.process_iter(attrs=["pid", "cpu_times"]): - self.assertIsNone(p.info['cpu_times']) - self.assertGreaterEqual(p.info['pid'], 0) - assert m.called - with mock.patch("psutil._psplatform.Process.cpu_times", - side_effect=psutil.AccessDenied(0, "")) as m: - flag = object() - for p in psutil.process_iter( - attrs=["pid", "cpu_times"], ad_value=flag): - self.assertIs(p.info['cpu_times'], flag) - self.assertGreaterEqual(p.info['pid'], 0) - assert m.called - - def test_wait_procs(self): - def callback(p): - pids.append(p.pid) - - pids = [] - sproc1 = get_test_subprocess() - sproc2 = get_test_subprocess() - sproc3 = get_test_subprocess() - procs = [psutil.Process(x.pid) for x in (sproc1, sproc2, sproc3)] - self.assertRaises(ValueError, psutil.wait_procs, procs, timeout=-1) - self.assertRaises(TypeError, psutil.wait_procs, procs, callback=1) - t = time.time() - gone, alive = psutil.wait_procs(procs, timeout=0.01, callback=callback) - - self.assertLess(time.time() - t, 0.5) - self.assertEqual(gone, []) - self.assertEqual(len(alive), 3) - self.assertEqual(pids, []) - for p in alive: - self.assertFalse(hasattr(p, 'returncode')) - - @retry_on_failure(30) - def test(procs, callback): - gone, alive = psutil.wait_procs(procs, timeout=0.03, - callback=callback) - self.assertEqual(len(gone), 1) - self.assertEqual(len(alive), 2) - return gone, alive - - sproc3.terminate() - gone, alive = test(procs, callback) - self.assertIn(sproc3.pid, [x.pid for x in gone]) - if POSIX: - self.assertEqual(gone.pop().returncode, -signal.SIGTERM) - else: - self.assertEqual(gone.pop().returncode, 1) - self.assertEqual(pids, [sproc3.pid]) - for p in alive: - self.assertFalse(hasattr(p, 'returncode')) - - @retry_on_failure(30) - def test(procs, callback): - gone, alive = psutil.wait_procs(procs, timeout=0.03, - callback=callback) - self.assertEqual(len(gone), 3) - self.assertEqual(len(alive), 0) - return gone, alive - - sproc1.terminate() - sproc2.terminate() - gone, alive = test(procs, callback) - self.assertEqual(set(pids), set([sproc1.pid, sproc2.pid, sproc3.pid])) - for p in gone: - self.assertTrue(hasattr(p, 'returncode')) - - def test_wait_procs_no_timeout(self): - sproc1 = get_test_subprocess() - sproc2 = get_test_subprocess() - sproc3 = get_test_subprocess() - procs = [psutil.Process(x.pid) for x in (sproc1, sproc2, sproc3)] - for p in procs: - p.terminate() - gone, alive = psutil.wait_procs(procs) - - def test_boot_time(self): - bt = psutil.boot_time() - self.assertIsInstance(bt, float) - self.assertGreater(bt, 0) - self.assertLess(bt, time.time()) - - @unittest.skipIf(not POSIX, 'POSIX only') - def test_PAGESIZE(self): - # pagesize is used internally to perform different calculations - # and it's determined by using SC_PAGE_SIZE; make sure - # getpagesize() returns the same value. - import resource - self.assertEqual(os.sysconf("SC_PAGE_SIZE"), resource.getpagesize()) - - def test_virtual_memory(self): - mem = psutil.virtual_memory() - assert mem.total > 0, mem - assert mem.available > 0, mem - assert 0 <= mem.percent <= 100, mem - assert mem.used > 0, mem - assert mem.free >= 0, mem - for name in mem._fields: - value = getattr(mem, name) - if name != 'percent': - self.assertIsInstance(value, (int, long)) - if name != 'total': - if not value >= 0: - self.fail("%r < 0 (%s)" % (name, value)) - if value > mem.total: - self.fail("%r > total (total=%s, %s=%s)" - % (name, mem.total, name, value)) - - def test_swap_memory(self): - mem = psutil.swap_memory() - self.assertEqual( - mem._fields, ('total', 'used', 'free', 'percent', 'sin', 'sout')) - - assert mem.total >= 0, mem - assert mem.used >= 0, mem - if mem.total > 0: - # likely a system with no swap partition - assert mem.free > 0, mem - else: - assert mem.free == 0, mem - assert 0 <= mem.percent <= 100, mem - assert mem.sin >= 0, mem - assert mem.sout >= 0, mem - - def test_pid_exists(self): - sproc = get_test_subprocess() - self.assertTrue(psutil.pid_exists(sproc.pid)) - p = psutil.Process(sproc.pid) - p.kill() - p.wait() - self.assertFalse(psutil.pid_exists(sproc.pid)) - self.assertFalse(psutil.pid_exists(-1)) - self.assertEqual(psutil.pid_exists(0), 0 in psutil.pids()) - - def test_pid_exists_2(self): - reap_children() - pids = psutil.pids() - for pid in pids: - try: - assert psutil.pid_exists(pid) - except AssertionError: - # in case the process disappeared in meantime fail only - # if it is no longer in psutil.pids() - time.sleep(.1) - if pid in psutil.pids(): - self.fail(pid) - pids = range(max(pids) + 5000, max(pids) + 6000) - for pid in pids: - self.assertFalse(psutil.pid_exists(pid), msg=pid) - - def test_pids(self): - pidslist = psutil.pids() - procslist = [x.pid for x in psutil.process_iter()] - # make sure every pid is unique - self.assertEqual(sorted(set(pidslist)), pidslist) - self.assertEqual(pidslist, procslist) - - def test_test(self): - # test for psutil.test() function - stdout = sys.stdout - sys.stdout = DEVNULL - try: - psutil.test() - finally: - sys.stdout = stdout - - def test_cpu_count(self): - logical = psutil.cpu_count() - self.assertEqual(logical, len(psutil.cpu_times(percpu=True))) - self.assertGreaterEqual(logical, 1) - # - if os.path.exists("/proc/cpuinfo"): - with open("/proc/cpuinfo") as fd: - cpuinfo_data = fd.read() - if "physical id" not in cpuinfo_data: - raise unittest.SkipTest("cpuinfo doesn't include physical id") - physical = psutil.cpu_count(logical=False) - if WINDOWS and sys.getwindowsversion()[:2] <= (6, 1): # <= Vista - self.assertIsNone(physical) - else: - self.assertGreaterEqual(physical, 1) - self.assertGreaterEqual(logical, physical) - - def test_cpu_count_none(self): - # https://github.com/giampaolo/psutil/issues/1085 - for val in (-1, 0, None): - with mock.patch('psutil._psplatform.cpu_count_logical', - return_value=val) as m: - self.assertIsNone(psutil.cpu_count()) - assert m.called - with mock.patch('psutil._psplatform.cpu_count_physical', - return_value=val) as m: - self.assertIsNone(psutil.cpu_count(logical=False)) - assert m.called - - def test_cpu_times(self): - # Check type, value >= 0, str(). - total = 0 - times = psutil.cpu_times() - sum(times) - for cp_time in times: - self.assertIsInstance(cp_time, float) - self.assertGreaterEqual(cp_time, 0.0) - total += cp_time - self.assertEqual(total, sum(times)) - str(times) - # CPU times are always supposed to increase over time - # or at least remain the same and that's because time - # cannot go backwards. - # Surprisingly sometimes this might not be the case (at - # least on Windows and Linux), see: - # https://github.com/giampaolo/psutil/issues/392 - # https://github.com/giampaolo/psutil/issues/645 - # if not WINDOWS: - # last = psutil.cpu_times() - # for x in range(100): - # new = psutil.cpu_times() - # for field in new._fields: - # new_t = getattr(new, field) - # last_t = getattr(last, field) - # self.assertGreaterEqual(new_t, last_t, - # msg="%s %s" % (new_t, last_t)) - # last = new - - def test_cpu_times_time_increases(self): - # Make sure time increases between calls. - t1 = sum(psutil.cpu_times()) - stop_at = time.time() + 1 - while time.time() < stop_at: - t2 = sum(psutil.cpu_times()) - if t2 > t1: - return - self.fail("time remained the same") - - def test_per_cpu_times(self): - # Check type, value >= 0, str(). - for times in psutil.cpu_times(percpu=True): - total = 0 - sum(times) - for cp_time in times: - self.assertIsInstance(cp_time, float) - self.assertGreaterEqual(cp_time, 0.0) - total += cp_time - self.assertEqual(total, sum(times)) - str(times) - self.assertEqual(len(psutil.cpu_times(percpu=True)[0]), - len(psutil.cpu_times(percpu=False))) - - # Note: in theory CPU times are always supposed to increase over - # time or remain the same but never go backwards. In practice - # sometimes this is not the case. - # This issue seemd to be afflict Windows: - # https://github.com/giampaolo/psutil/issues/392 - # ...but it turns out also Linux (rarely) behaves the same. - # last = psutil.cpu_times(percpu=True) - # for x in range(100): - # new = psutil.cpu_times(percpu=True) - # for index in range(len(new)): - # newcpu = new[index] - # lastcpu = last[index] - # for field in newcpu._fields: - # new_t = getattr(newcpu, field) - # last_t = getattr(lastcpu, field) - # self.assertGreaterEqual( - # new_t, last_t, msg="%s %s" % (lastcpu, newcpu)) - # last = new - - def test_per_cpu_times_2(self): - # Simulate some work load then make sure time have increased - # between calls. - tot1 = psutil.cpu_times(percpu=True) - stop_at = time.time() + 0.1 - while True: - if time.time() >= stop_at: - break - tot2 = psutil.cpu_times(percpu=True) - for t1, t2 in zip(tot1, tot2): - t1, t2 = sum(t1), sum(t2) - difference = t2 - t1 - if difference >= 0.05: - return - self.fail() - - def test_cpu_times_comparison(self): - # Make sure the sum of all per cpu times is almost equal to - # base "one cpu" times. - base = psutil.cpu_times() - per_cpu = psutil.cpu_times(percpu=True) - summed_values = base._make([sum(num) for num in zip(*per_cpu)]) - for field in base._fields: - self.assertAlmostEqual( - getattr(base, field), getattr(summed_values, field), delta=1) - - def _test_cpu_percent(self, percent, last_ret, new_ret): - try: - self.assertIsInstance(percent, float) - self.assertGreaterEqual(percent, 0.0) - self.assertIsNot(percent, -0.0) - self.assertLessEqual(percent, 100.0 * psutil.cpu_count()) - except AssertionError as err: - raise AssertionError("\n%s\nlast=%s\nnew=%s" % ( - err, pprint.pformat(last_ret), pprint.pformat(new_ret))) - - def test_cpu_percent(self): - last = psutil.cpu_percent(interval=0.001) - for x in range(100): - new = psutil.cpu_percent(interval=None) - self._test_cpu_percent(new, last, new) - last = new - with self.assertRaises(ValueError): - psutil.cpu_percent(interval=-1) - - def test_per_cpu_percent(self): - last = psutil.cpu_percent(interval=0.001, percpu=True) - self.assertEqual(len(last), psutil.cpu_count()) - for x in range(100): - new = psutil.cpu_percent(interval=None, percpu=True) - for percent in new: - self._test_cpu_percent(percent, last, new) - last = new - with self.assertRaises(ValueError): - psutil.cpu_percent(interval=-1, percpu=True) - - def test_cpu_times_percent(self): - last = psutil.cpu_times_percent(interval=0.001) - for x in range(100): - new = psutil.cpu_times_percent(interval=None) - for percent in new: - self._test_cpu_percent(percent, last, new) - self._test_cpu_percent(sum(new), last, new) - last = new - - def test_per_cpu_times_percent(self): - last = psutil.cpu_times_percent(interval=0.001, percpu=True) - self.assertEqual(len(last), psutil.cpu_count()) - for x in range(100): - new = psutil.cpu_times_percent(interval=None, percpu=True) - for cpu in new: - for percent in cpu: - self._test_cpu_percent(percent, last, new) - self._test_cpu_percent(sum(cpu), last, new) - last = new - - def test_per_cpu_times_percent_negative(self): - # see: https://github.com/giampaolo/psutil/issues/645 - psutil.cpu_times_percent(percpu=True) - zero_times = [x._make([0 for x in range(len(x._fields))]) - for x in psutil.cpu_times(percpu=True)] - with mock.patch('psutil.cpu_times', return_value=zero_times): - for cpu in psutil.cpu_times_percent(percpu=True): - for percent in cpu: - self._test_cpu_percent(percent, None, None) - - def test_disk_usage(self): - usage = psutil.disk_usage(os.getcwd()) - self.assertEqual(usage._fields, ('total', 'used', 'free', 'percent')) - - assert usage.total > 0, usage - assert usage.used > 0, usage - assert usage.free > 0, usage - assert usage.total > usage.used, usage - assert usage.total > usage.free, usage - assert 0 <= usage.percent <= 100, usage.percent - if hasattr(shutil, 'disk_usage'): - # py >= 3.3, see: http://bugs.python.org/issue12442 - shutil_usage = shutil.disk_usage(os.getcwd()) - tolerance = 5 * 1024 * 1024 # 5MB - self.assertEqual(usage.total, shutil_usage.total) - self.assertAlmostEqual(usage.free, shutil_usage.free, - delta=tolerance) - self.assertAlmostEqual(usage.used, shutil_usage.used, - delta=tolerance) - - # if path does not exist OSError ENOENT is expected across - # all platforms - fname = tempfile.mktemp() - with self.assertRaises(OSError) as exc: - psutil.disk_usage(fname) - self.assertEqual(exc.exception.errno, errno.ENOENT) - - def test_disk_usage_unicode(self): - # See: https://github.com/giampaolo/psutil/issues/416 - if ASCII_FS: - with self.assertRaises(UnicodeEncodeError): - psutil.disk_usage(TESTFN_UNICODE) - - def test_disk_usage_bytes(self): - psutil.disk_usage(b'.') - - def test_disk_partitions(self): - # all = False - ls = psutil.disk_partitions(all=False) - # on travis we get: - # self.assertEqual(p.cpu_affinity(), [n]) - # AssertionError: Lists differ: [0, 1, 2, 3, 4, 5, 6, 7,... != [0] - self.assertTrue(ls, msg=ls) - for disk in ls: - self.assertIsInstance(disk.device, str) - self.assertIsInstance(disk.mountpoint, str) - self.assertIsInstance(disk.fstype, str) - self.assertIsInstance(disk.opts, str) - if WINDOWS and 'cdrom' in disk.opts: - continue - if not POSIX: - assert os.path.exists(disk.device), disk - else: - # we cannot make any assumption about this, see: - # http://goo.gl/p9c43 - disk.device - if SUNOS or TRAVIS: - # on solaris apparently mount points can also be files - assert os.path.exists(disk.mountpoint), disk - else: - assert os.path.isdir(disk.mountpoint), disk - assert disk.fstype, disk - - # all = True - ls = psutil.disk_partitions(all=True) - self.assertTrue(ls, msg=ls) - for disk in psutil.disk_partitions(all=True): - if not WINDOWS: - try: - os.stat(disk.mountpoint) - except OSError as err: - if TRAVIS and MACOS and err.errno == errno.EIO: - continue - # http://mail.python.org/pipermail/python-dev/ - # 2012-June/120787.html - if err.errno not in (errno.EPERM, errno.EACCES): - raise - else: - assert os.path.exists(disk.mountpoint), disk - self.assertIsInstance(disk.fstype, str) - self.assertIsInstance(disk.opts, str) - - def find_mount_point(path): - path = os.path.abspath(path) - while not os.path.ismount(path): - path = os.path.dirname(path) - return path.lower() - - mount = find_mount_point(__file__) - mounts = [x.mountpoint.lower() for x in - psutil.disk_partitions(all=True)] - self.assertIn(mount, mounts) - psutil.disk_usage(mount) - - @unittest.skipIf(not HAS_NET_IO_COUNTERS, 'not supported') - def test_net_io_counters(self): - def check_ntuple(nt): - self.assertEqual(nt[0], nt.bytes_sent) - self.assertEqual(nt[1], nt.bytes_recv) - self.assertEqual(nt[2], nt.packets_sent) - self.assertEqual(nt[3], nt.packets_recv) - self.assertEqual(nt[4], nt.errin) - self.assertEqual(nt[5], nt.errout) - self.assertEqual(nt[6], nt.dropin) - self.assertEqual(nt[7], nt.dropout) - assert nt.bytes_sent >= 0, nt - assert nt.bytes_recv >= 0, nt - assert nt.packets_sent >= 0, nt - assert nt.packets_recv >= 0, nt - assert nt.errin >= 0, nt - assert nt.errout >= 0, nt - assert nt.dropin >= 0, nt - assert nt.dropout >= 0, nt - - ret = psutil.net_io_counters(pernic=False) - check_ntuple(ret) - ret = psutil.net_io_counters(pernic=True) - self.assertNotEqual(ret, []) - for key in ret: - self.assertTrue(key) - self.assertIsInstance(key, str) - check_ntuple(ret[key]) - - @unittest.skipIf(not HAS_NET_IO_COUNTERS, 'not supported') - def test_net_io_counters_no_nics(self): - # Emulate a case where no NICs are installed, see: - # https://github.com/giampaolo/psutil/issues/1062 - with mock.patch('psutil._psplatform.net_io_counters', - return_value={}) as m: - self.assertIsNone(psutil.net_io_counters(pernic=False)) - self.assertEqual(psutil.net_io_counters(pernic=True), {}) - assert m.called - - def test_net_if_addrs(self): - nics = psutil.net_if_addrs() - assert nics, nics - - nic_stats = psutil.net_if_stats() - - # Not reliable on all platforms (net_if_addrs() reports more - # interfaces). - # self.assertEqual(sorted(nics.keys()), - # sorted(psutil.net_io_counters(pernic=True).keys())) - - families = set([socket.AF_INET, socket.AF_INET6, psutil.AF_LINK]) - for nic, addrs in nics.items(): - self.assertIsInstance(nic, str) - self.assertEqual(len(set(addrs)), len(addrs)) - for addr in addrs: - self.assertIsInstance(addr.family, int) - self.assertIsInstance(addr.address, str) - self.assertIsInstance(addr.netmask, (str, type(None))) - self.assertIsInstance(addr.broadcast, (str, type(None))) - self.assertIn(addr.family, families) - if sys.version_info >= (3, 4): - self.assertIsInstance(addr.family, enum.IntEnum) - if nic_stats[nic].isup: - # Do not test binding to addresses of interfaces - # that are down - if addr.family == socket.AF_INET: - s = socket.socket(addr.family) - with contextlib.closing(s): - s.bind((addr.address, 0)) - elif addr.family == socket.AF_INET6: - info = socket.getaddrinfo( - addr.address, 0, socket.AF_INET6, - socket.SOCK_STREAM, 0, socket.AI_PASSIVE)[0] - af, socktype, proto, canonname, sa = info - s = socket.socket(af, socktype, proto) - with contextlib.closing(s): - s.bind(sa) - for ip in (addr.address, addr.netmask, addr.broadcast, - addr.ptp): - if ip is not None: - # TODO: skip AF_INET6 for now because I get: - # AddressValueError: Only hex digits permitted in - # u'c6f3%lxcbr0' in u'fe80::c8e0:fff:fe54:c6f3%lxcbr0' - if addr.family != socket.AF_INET6: - check_net_address(ip, addr.family) - # broadcast and ptp addresses are mutually exclusive - if addr.broadcast: - self.assertIsNone(addr.ptp) - elif addr.ptp: - self.assertIsNone(addr.broadcast) - - if BSD or MACOS or SUNOS: - if hasattr(socket, "AF_LINK"): - self.assertEqual(psutil.AF_LINK, socket.AF_LINK) - elif LINUX: - self.assertEqual(psutil.AF_LINK, socket.AF_PACKET) - elif WINDOWS: - self.assertEqual(psutil.AF_LINK, -1) - - def test_net_if_addrs_mac_null_bytes(self): - # Simulate that the underlying C function returns an incomplete - # MAC address. psutil is supposed to fill it with null bytes. - # https://github.com/giampaolo/psutil/issues/786 - if POSIX: - ret = [('em1', psutil.AF_LINK, '06:3d:29', None, None, None)] - else: - ret = [('em1', -1, '06-3d-29', None, None, None)] - with mock.patch('psutil._psplatform.net_if_addrs', - return_value=ret) as m: - addr = psutil.net_if_addrs()['em1'][0] - assert m.called - if POSIX: - self.assertEqual(addr.address, '06:3d:29:00:00:00') - else: - self.assertEqual(addr.address, '06-3d-29-00-00-00') - - @unittest.skipIf(TRAVIS, "unreliable on TRAVIS") # raises EPERM - def test_net_if_stats(self): - nics = psutil.net_if_stats() - assert nics, nics - all_duplexes = (psutil.NIC_DUPLEX_FULL, - psutil.NIC_DUPLEX_HALF, - psutil.NIC_DUPLEX_UNKNOWN) - for name, stats in nics.items(): - self.assertIsInstance(name, str) - isup, duplex, speed, mtu = stats - self.assertIsInstance(isup, bool) - self.assertIn(duplex, all_duplexes) - self.assertIn(duplex, all_duplexes) - self.assertGreaterEqual(speed, 0) - self.assertGreaterEqual(mtu, 0) - - @unittest.skipIf(not (LINUX or BSD or MACOS), - "LINUX or BSD or MACOS specific") - def test_net_if_stats_enodev(self): - # See: https://github.com/giampaolo/psutil/issues/1279 - with mock.patch('psutil._psutil_posix.net_if_mtu', - side_effect=OSError(errno.ENODEV, "")) as m: - ret = psutil.net_if_stats() - self.assertEqual(ret, {}) - assert m.called - - @unittest.skipIf(LINUX and not os.path.exists('/proc/diskstats'), - '/proc/diskstats not available on this linux version') - @unittest.skipIf(APPVEYOR and psutil.disk_io_counters() is None, - "unreliable on APPVEYOR") # no visible disks - def test_disk_io_counters(self): - def check_ntuple(nt): - self.assertEqual(nt[0], nt.read_count) - self.assertEqual(nt[1], nt.write_count) - self.assertEqual(nt[2], nt.read_bytes) - self.assertEqual(nt[3], nt.write_bytes) - if not (OPENBSD or NETBSD): - self.assertEqual(nt[4], nt.read_time) - self.assertEqual(nt[5], nt.write_time) - if LINUX: - self.assertEqual(nt[6], nt.read_merged_count) - self.assertEqual(nt[7], nt.write_merged_count) - self.assertEqual(nt[8], nt.busy_time) - elif FREEBSD: - self.assertEqual(nt[6], nt.busy_time) - for name in nt._fields: - assert getattr(nt, name) >= 0, nt - - ret = psutil.disk_io_counters(perdisk=False) - assert ret is not None, "no disks on this system?" - check_ntuple(ret) - ret = psutil.disk_io_counters(perdisk=True) - # make sure there are no duplicates - self.assertEqual(len(ret), len(set(ret))) - for key in ret: - assert key, key - check_ntuple(ret[key]) - - def test_disk_io_counters_no_disks(self): - # Emulate a case where no disks are installed, see: - # https://github.com/giampaolo/psutil/issues/1062 - with mock.patch('psutil._psplatform.disk_io_counters', - return_value={}) as m: - self.assertIsNone(psutil.disk_io_counters(perdisk=False)) - self.assertEqual(psutil.disk_io_counters(perdisk=True), {}) - assert m.called - - # can't find users on APPVEYOR or TRAVIS - @unittest.skipIf(APPVEYOR or TRAVIS and not psutil.users(), - "unreliable on APPVEYOR or TRAVIS") - def test_users(self): - users = psutil.users() - self.assertNotEqual(users, []) - for user in users: - assert user.name, user - self.assertIsInstance(user.name, str) - self.assertIsInstance(user.terminal, (str, type(None))) - if user.host is not None: - self.assertIsInstance(user.host, (str, type(None))) - user.terminal - user.host - assert user.started > 0.0, user - datetime.datetime.fromtimestamp(user.started) - if WINDOWS or OPENBSD: - self.assertIsNone(user.pid) - else: - psutil.Process(user.pid) - - def test_cpu_stats(self): - # Tested more extensively in per-platform test modules. - infos = psutil.cpu_stats() - self.assertEqual( - infos._fields, - ('ctx_switches', 'interrupts', 'soft_interrupts', 'syscalls')) - for name in infos._fields: - value = getattr(infos, name) - self.assertGreaterEqual(value, 0) - # on AIX, ctx_switches is always 0 - if not AIX and name in ('ctx_switches', 'interrupts'): - self.assertGreater(value, 0) - - @unittest.skipIf(not HAS_CPU_FREQ, "not suported") - def test_cpu_freq(self): - def check_ls(ls): - for nt in ls: - self.assertEqual(nt._fields, ('current', 'min', 'max')) - self.assertLessEqual(nt.current, nt.max) - for name in nt._fields: - value = getattr(nt, name) - self.assertIsInstance(value, (int, long, float)) - self.assertGreaterEqual(value, 0) - - ls = psutil.cpu_freq(percpu=True) - if TRAVIS and not ls: - raise self.skipTest("skipped on Travis") - if FREEBSD and not ls: - raise self.skipTest("returns empty list on FreeBSD") - - assert ls, ls - check_ls([psutil.cpu_freq(percpu=False)]) - - if LINUX: - self.assertEqual(len(ls), psutil.cpu_count()) - - @unittest.skipIf(not HAS_GETLOADAVG, "not supported") - def test_getloadavg(self): - loadavg = psutil.getloadavg() - assert len(loadavg) == 3 - - for load in loadavg: - self.assertIsInstance(load, float) - self.assertGreaterEqual(load, 0.0) - - def test_os_constants(self): - names = ["POSIX", "WINDOWS", "LINUX", "MACOS", "FREEBSD", "OPENBSD", - "NETBSD", "BSD", "SUNOS"] - for name in names: - self.assertIsInstance(getattr(psutil, name), bool, msg=name) - - if os.name == 'posix': - assert psutil.POSIX - assert not psutil.WINDOWS - names.remove("POSIX") - if "linux" in sys.platform.lower(): - assert psutil.LINUX - names.remove("LINUX") - elif "bsd" in sys.platform.lower(): - assert psutil.BSD - self.assertEqual([psutil.FREEBSD, psutil.OPENBSD, - psutil.NETBSD].count(True), 1) - names.remove("BSD") - names.remove("FREEBSD") - names.remove("OPENBSD") - names.remove("NETBSD") - elif "sunos" in sys.platform.lower() or \ - "solaris" in sys.platform.lower(): - assert psutil.SUNOS - names.remove("SUNOS") - elif "darwin" in sys.platform.lower(): - assert psutil.MACOS - names.remove("MACOS") - else: - assert psutil.WINDOWS - assert not psutil.POSIX - names.remove("WINDOWS") - - # assert all other constants are set to False - for name in names: - self.assertIs(getattr(psutil, name), False, msg=name) - - @unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported") - def test_sensors_temperatures(self): - temps = psutil.sensors_temperatures() - for name, entries in temps.items(): - self.assertIsInstance(name, str) - for entry in entries: - self.assertIsInstance(entry.label, str) - if entry.current is not None: - self.assertGreaterEqual(entry.current, 0) - if entry.high is not None: - self.assertGreaterEqual(entry.high, 0) - if entry.critical is not None: - self.assertGreaterEqual(entry.critical, 0) - - @unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported") - def test_sensors_temperatures_fahreneit(self): - d = {'coretemp': [('label', 50.0, 60.0, 70.0)]} - with mock.patch("psutil._psplatform.sensors_temperatures", - return_value=d) as m: - temps = psutil.sensors_temperatures( - fahrenheit=True)['coretemp'][0] - assert m.called - self.assertEqual(temps.current, 122.0) - self.assertEqual(temps.high, 140.0) - self.assertEqual(temps.critical, 158.0) - - @unittest.skipIf(not HAS_SENSORS_BATTERY, "not supported") - @unittest.skipIf(not HAS_BATTERY, "no battery") - def test_sensors_battery(self): - ret = psutil.sensors_battery() - self.assertGreaterEqual(ret.percent, 0) - self.assertLessEqual(ret.percent, 100) - if ret.secsleft not in (psutil.POWER_TIME_UNKNOWN, - psutil.POWER_TIME_UNLIMITED): - self.assertGreaterEqual(ret.secsleft, 0) - else: - if ret.secsleft == psutil.POWER_TIME_UNLIMITED: - self.assertTrue(ret.power_plugged) - self.assertIsInstance(ret.power_plugged, bool) - - @unittest.skipIf(not HAS_SENSORS_FANS, "not supported") - def test_sensors_fans(self): - fans = psutil.sensors_fans() - for name, entries in fans.items(): - self.assertIsInstance(name, str) - for entry in entries: - self.assertIsInstance(entry.label, str) - self.assertIsInstance(entry.current, (int, long)) - self.assertGreaterEqual(entry.current, 0) - - -if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Tests for system APIS.""" + +import contextlib +import datetime +import errno +import os +import platform +import pprint +import shutil +import signal +import socket +import sys +import time +import unittest + +import psutil +from psutil import AIX +from psutil import BSD +from psutil import FREEBSD +from psutil import LINUX +from psutil import MACOS +from psutil import NETBSD +from psutil import OPENBSD +from psutil import POSIX +from psutil import SUNOS +from psutil import WINDOWS +from psutil._compat import PY3 +from psutil._compat import FileNotFoundError +from psutil._compat import long +from psutil.tests import ASCII_FS +from psutil.tests import CI_TESTING +from psutil.tests import DEVNULL +from psutil.tests import GITHUB_ACTIONS +from psutil.tests import GLOBAL_TIMEOUT +from psutil.tests import HAS_BATTERY +from psutil.tests import HAS_CPU_FREQ +from psutil.tests import HAS_GETLOADAVG +from psutil.tests import HAS_NET_IO_COUNTERS +from psutil.tests import HAS_SENSORS_BATTERY +from psutil.tests import HAS_SENSORS_FANS +from psutil.tests import HAS_SENSORS_TEMPERATURES +from psutil.tests import IS_64BIT +from psutil.tests import MACOS_12PLUS +from psutil.tests import PYPY +from psutil.tests import UNICODE_SUFFIX +from psutil.tests import PsutilTestCase +from psutil.tests import check_net_address +from psutil.tests import enum +from psutil.tests import mock +from psutil.tests import retry_on_failure + + +# =================================================================== +# --- System-related API tests +# =================================================================== + + +class TestProcessAPIs(PsutilTestCase): + def test_process_iter(self): + self.assertIn(os.getpid(), [x.pid for x in psutil.process_iter()]) + sproc = self.spawn_testproc() + self.assertIn(sproc.pid, [x.pid for x in psutil.process_iter()]) + p = psutil.Process(sproc.pid) + p.kill() + p.wait() + self.assertNotIn(sproc.pid, [x.pid for x in psutil.process_iter()]) + + # assert there are no duplicates + ls = [x for x in psutil.process_iter()] + self.assertEqual( + sorted(ls, key=lambda x: x.pid), + sorted(set(ls), key=lambda x: x.pid), + ) + + with mock.patch( + 'psutil.Process', side_effect=psutil.NoSuchProcess(os.getpid()) + ): + self.assertEqual(list(psutil.process_iter()), []) + with mock.patch( + 'psutil.Process', side_effect=psutil.AccessDenied(os.getpid()) + ): + with self.assertRaises(psutil.AccessDenied): + list(psutil.process_iter()) + + def test_prcess_iter_w_attrs(self): + for p in psutil.process_iter(attrs=['pid']): + self.assertEqual(list(p.info.keys()), ['pid']) + with self.assertRaises(ValueError): + list(psutil.process_iter(attrs=['foo'])) + with mock.patch( + "psutil._psplatform.Process.cpu_times", + side_effect=psutil.AccessDenied(0, ""), + ) as m: + for p in psutil.process_iter(attrs=["pid", "cpu_times"]): + self.assertIsNone(p.info['cpu_times']) + self.assertGreaterEqual(p.info['pid'], 0) + assert m.called + with mock.patch( + "psutil._psplatform.Process.cpu_times", + side_effect=psutil.AccessDenied(0, ""), + ) as m: + flag = object() + for p in psutil.process_iter( + attrs=["pid", "cpu_times"], ad_value=flag + ): + self.assertIs(p.info['cpu_times'], flag) + self.assertGreaterEqual(p.info['pid'], 0) + assert m.called + + @unittest.skipIf( + PYPY and WINDOWS, "spawn_testproc() unreliable on PYPY + WINDOWS" + ) + def test_wait_procs(self): + def callback(p): + pids.append(p.pid) + + pids = [] + sproc1 = self.spawn_testproc() + sproc2 = self.spawn_testproc() + sproc3 = self.spawn_testproc() + procs = [psutil.Process(x.pid) for x in (sproc1, sproc2, sproc3)] + self.assertRaises(ValueError, psutil.wait_procs, procs, timeout=-1) + self.assertRaises(TypeError, psutil.wait_procs, procs, callback=1) + t = time.time() + gone, alive = psutil.wait_procs(procs, timeout=0.01, callback=callback) + + self.assertLess(time.time() - t, 0.5) + self.assertEqual(gone, []) + self.assertEqual(len(alive), 3) + self.assertEqual(pids, []) + for p in alive: + self.assertFalse(hasattr(p, 'returncode')) + + @retry_on_failure(30) + def test_1(procs, callback): + gone, alive = psutil.wait_procs( + procs, timeout=0.03, callback=callback + ) + self.assertEqual(len(gone), 1) + self.assertEqual(len(alive), 2) + return gone, alive + + sproc3.terminate() + gone, alive = test_1(procs, callback) + self.assertIn(sproc3.pid, [x.pid for x in gone]) + if POSIX: + self.assertEqual(gone.pop().returncode, -signal.SIGTERM) + else: + self.assertEqual(gone.pop().returncode, 1) + self.assertEqual(pids, [sproc3.pid]) + for p in alive: + self.assertFalse(hasattr(p, 'returncode')) + + @retry_on_failure(30) + def test_2(procs, callback): + gone, alive = psutil.wait_procs( + procs, timeout=0.03, callback=callback + ) + self.assertEqual(len(gone), 3) + self.assertEqual(len(alive), 0) + return gone, alive + + sproc1.terminate() + sproc2.terminate() + gone, alive = test_2(procs, callback) + self.assertEqual(set(pids), set([sproc1.pid, sproc2.pid, sproc3.pid])) + for p in gone: + self.assertTrue(hasattr(p, 'returncode')) + + @unittest.skipIf( + PYPY and WINDOWS, "spawn_testproc() unreliable on PYPY + WINDOWS" + ) + def test_wait_procs_no_timeout(self): + sproc1 = self.spawn_testproc() + sproc2 = self.spawn_testproc() + sproc3 = self.spawn_testproc() + procs = [psutil.Process(x.pid) for x in (sproc1, sproc2, sproc3)] + for p in procs: + p.terminate() + psutil.wait_procs(procs) + + def test_pid_exists(self): + sproc = self.spawn_testproc() + self.assertTrue(psutil.pid_exists(sproc.pid)) + p = psutil.Process(sproc.pid) + p.kill() + p.wait() + self.assertFalse(psutil.pid_exists(sproc.pid)) + self.assertFalse(psutil.pid_exists(-1)) + self.assertEqual(psutil.pid_exists(0), 0 in psutil.pids()) + + def test_pid_exists_2(self): + pids = psutil.pids() + for pid in pids: + try: + assert psutil.pid_exists(pid) + except AssertionError: + # in case the process disappeared in meantime fail only + # if it is no longer in psutil.pids() + time.sleep(0.1) + self.assertNotIn(pid, psutil.pids()) + pids = range(max(pids) + 15000, max(pids) + 16000) + for pid in pids: + self.assertFalse(psutil.pid_exists(pid), msg=pid) + + +class TestMiscAPIs(PsutilTestCase): + def test_boot_time(self): + bt = psutil.boot_time() + self.assertIsInstance(bt, float) + self.assertGreater(bt, 0) + self.assertLess(bt, time.time()) + + @unittest.skipIf(CI_TESTING and not psutil.users(), "unreliable on CI") + def test_users(self): + users = psutil.users() + self.assertNotEqual(users, []) + for user in users: + with self.subTest(user=user): + assert user.name + self.assertIsInstance(user.name, str) + self.assertIsInstance(user.terminal, (str, type(None))) + if user.host is not None: + self.assertIsInstance(user.host, (str, type(None))) + user.terminal # noqa + user.host # noqa + self.assertGreater(user.started, 0.0) + datetime.datetime.fromtimestamp(user.started) + if WINDOWS or OPENBSD: + self.assertIsNone(user.pid) + else: + psutil.Process(user.pid) + + def test_test(self): + # test for psutil.test() function + stdout = sys.stdout + sys.stdout = DEVNULL + try: + psutil.test() + finally: + sys.stdout = stdout + + def test_os_constants(self): + names = [ + "POSIX", + "WINDOWS", + "LINUX", + "MACOS", + "FREEBSD", + "OPENBSD", + "NETBSD", + "BSD", + "SUNOS", + ] + for name in names: + self.assertIsInstance(getattr(psutil, name), bool, msg=name) + + if os.name == 'posix': + assert psutil.POSIX + assert not psutil.WINDOWS + names.remove("POSIX") + if "linux" in sys.platform.lower(): + assert psutil.LINUX + names.remove("LINUX") + elif "bsd" in sys.platform.lower(): + assert psutil.BSD + self.assertEqual( + [psutil.FREEBSD, psutil.OPENBSD, psutil.NETBSD].count( + True + ), + 1, + ) + names.remove("BSD") + names.remove("FREEBSD") + names.remove("OPENBSD") + names.remove("NETBSD") + elif ( + "sunos" in sys.platform.lower() + or "solaris" in sys.platform.lower() + ): + assert psutil.SUNOS + names.remove("SUNOS") + elif "darwin" in sys.platform.lower(): + assert psutil.MACOS + names.remove("MACOS") + else: + assert psutil.WINDOWS + assert not psutil.POSIX + names.remove("WINDOWS") + + # assert all other constants are set to False + for name in names: + self.assertFalse(getattr(psutil, name), msg=name) + + +class TestMemoryAPIs(PsutilTestCase): + def test_virtual_memory(self): + mem = psutil.virtual_memory() + assert mem.total > 0, mem + assert mem.available > 0, mem + assert 0 <= mem.percent <= 100, mem + assert mem.used > 0, mem + assert mem.free >= 0, mem + for name in mem._fields: + value = getattr(mem, name) + if name != 'percent': + self.assertIsInstance(value, (int, long)) + if name != 'total': + if not value >= 0: + raise self.fail("%r < 0 (%s)" % (name, value)) + if value > mem.total: + raise self.fail( + "%r > total (total=%s, %s=%s)" + % (name, mem.total, name, value) + ) + + def test_swap_memory(self): + mem = psutil.swap_memory() + self.assertEqual( + mem._fields, ('total', 'used', 'free', 'percent', 'sin', 'sout') + ) + + assert mem.total >= 0, mem + assert mem.used >= 0, mem + if mem.total > 0: + # likely a system with no swap partition + assert mem.free > 0, mem + else: + assert mem.free == 0, mem + assert 0 <= mem.percent <= 100, mem + assert mem.sin >= 0, mem + assert mem.sout >= 0, mem + + +class TestCpuAPIs(PsutilTestCase): + def test_cpu_count_logical(self): + logical = psutil.cpu_count() + self.assertIsNotNone(logical) + self.assertEqual(logical, len(psutil.cpu_times(percpu=True))) + self.assertGreaterEqual(logical, 1) + # + if os.path.exists("/proc/cpuinfo"): + with open("/proc/cpuinfo") as fd: + cpuinfo_data = fd.read() + if "physical id" not in cpuinfo_data: + raise unittest.SkipTest("cpuinfo doesn't include physical id") + + def test_cpu_count_cores(self): + logical = psutil.cpu_count() + cores = psutil.cpu_count(logical=False) + if cores is None: + raise self.skipTest("cpu_count_cores() is None") + if WINDOWS and sys.getwindowsversion()[:2] <= (6, 1): # <= Vista + self.assertIsNone(cores) + else: + self.assertGreaterEqual(cores, 1) + self.assertGreaterEqual(logical, cores) + + def test_cpu_count_none(self): + # https://github.com/giampaolo/psutil/issues/1085 + for val in (-1, 0, None): + with mock.patch( + 'psutil._psplatform.cpu_count_logical', return_value=val + ) as m: + self.assertIsNone(psutil.cpu_count()) + assert m.called + with mock.patch( + 'psutil._psplatform.cpu_count_cores', return_value=val + ) as m: + self.assertIsNone(psutil.cpu_count(logical=False)) + assert m.called + + def test_cpu_times(self): + # Check type, value >= 0, str(). + total = 0 + times = psutil.cpu_times() + sum(times) + for cp_time in times: + self.assertIsInstance(cp_time, float) + self.assertGreaterEqual(cp_time, 0.0) + total += cp_time + self.assertAlmostEqual(total, sum(times)) + str(times) + # CPU times are always supposed to increase over time + # or at least remain the same and that's because time + # cannot go backwards. + # Surprisingly sometimes this might not be the case (at + # least on Windows and Linux), see: + # https://github.com/giampaolo/psutil/issues/392 + # https://github.com/giampaolo/psutil/issues/645 + # if not WINDOWS: + # last = psutil.cpu_times() + # for x in range(100): + # new = psutil.cpu_times() + # for field in new._fields: + # new_t = getattr(new, field) + # last_t = getattr(last, field) + # self.assertGreaterEqual(new_t, last_t, + # msg="%s %s" % (new_t, last_t)) + # last = new + + def test_cpu_times_time_increases(self): + # Make sure time increases between calls. + t1 = sum(psutil.cpu_times()) + stop_at = time.time() + GLOBAL_TIMEOUT + while time.time() < stop_at: + t2 = sum(psutil.cpu_times()) + if t2 > t1: + return + raise self.fail("time remained the same") + + def test_per_cpu_times(self): + # Check type, value >= 0, str(). + for times in psutil.cpu_times(percpu=True): + total = 0 + sum(times) + for cp_time in times: + self.assertIsInstance(cp_time, float) + self.assertGreaterEqual(cp_time, 0.0) + total += cp_time + self.assertAlmostEqual(total, sum(times)) + str(times) + self.assertEqual( + len(psutil.cpu_times(percpu=True)[0]), + len(psutil.cpu_times(percpu=False)), + ) + + # Note: in theory CPU times are always supposed to increase over + # time or remain the same but never go backwards. In practice + # sometimes this is not the case. + # This issue seemd to be afflict Windows: + # https://github.com/giampaolo/psutil/issues/392 + # ...but it turns out also Linux (rarely) behaves the same. + # last = psutil.cpu_times(percpu=True) + # for x in range(100): + # new = psutil.cpu_times(percpu=True) + # for index in range(len(new)): + # newcpu = new[index] + # lastcpu = last[index] + # for field in newcpu._fields: + # new_t = getattr(newcpu, field) + # last_t = getattr(lastcpu, field) + # self.assertGreaterEqual( + # new_t, last_t, msg="%s %s" % (lastcpu, newcpu)) + # last = new + + def test_per_cpu_times_2(self): + # Simulate some work load then make sure time have increased + # between calls. + tot1 = psutil.cpu_times(percpu=True) + giveup_at = time.time() + GLOBAL_TIMEOUT + while True: + if time.time() >= giveup_at: + return self.fail("timeout") + tot2 = psutil.cpu_times(percpu=True) + for t1, t2 in zip(tot1, tot2): + t1, t2 = psutil._cpu_busy_time(t1), psutil._cpu_busy_time(t2) + difference = t2 - t1 + if difference >= 0.05: + return + + @unittest.skipIf(CI_TESTING and OPENBSD, "unreliable on OPENBSD + CI") + def test_cpu_times_comparison(self): + # Make sure the sum of all per cpu times is almost equal to + # base "one cpu" times. On OpenBSD the sum of per-CPUs is + # higher for some reason. + base = psutil.cpu_times() + per_cpu = psutil.cpu_times(percpu=True) + summed_values = base._make([sum(num) for num in zip(*per_cpu)]) + for field in base._fields: + with self.subTest(field=field, base=base, per_cpu=per_cpu): + self.assertAlmostEqual( + getattr(base, field), + getattr(summed_values, field), + delta=1, + ) + + def _test_cpu_percent(self, percent, last_ret, new_ret): + try: + self.assertIsInstance(percent, float) + self.assertGreaterEqual(percent, 0.0) + self.assertIsNot(percent, -0.0) + self.assertLessEqual(percent, 100.0 * psutil.cpu_count()) + except AssertionError as err: + raise AssertionError( + "\n%s\nlast=%s\nnew=%s" + % (err, pprint.pformat(last_ret), pprint.pformat(new_ret)) + ) + + def test_cpu_percent(self): + last = psutil.cpu_percent(interval=0.001) + for _ in range(100): + new = psutil.cpu_percent(interval=None) + self._test_cpu_percent(new, last, new) + last = new + with self.assertRaises(ValueError): + psutil.cpu_percent(interval=-1) + + def test_per_cpu_percent(self): + last = psutil.cpu_percent(interval=0.001, percpu=True) + self.assertEqual(len(last), psutil.cpu_count()) + for _ in range(100): + new = psutil.cpu_percent(interval=None, percpu=True) + for percent in new: + self._test_cpu_percent(percent, last, new) + last = new + with self.assertRaises(ValueError): + psutil.cpu_percent(interval=-1, percpu=True) + + def test_cpu_times_percent(self): + last = psutil.cpu_times_percent(interval=0.001) + for _ in range(100): + new = psutil.cpu_times_percent(interval=None) + for percent in new: + self._test_cpu_percent(percent, last, new) + self._test_cpu_percent(sum(new), last, new) + last = new + with self.assertRaises(ValueError): + psutil.cpu_times_percent(interval=-1) + + def test_per_cpu_times_percent(self): + last = psutil.cpu_times_percent(interval=0.001, percpu=True) + self.assertEqual(len(last), psutil.cpu_count()) + for _ in range(100): + new = psutil.cpu_times_percent(interval=None, percpu=True) + for cpu in new: + for percent in cpu: + self._test_cpu_percent(percent, last, new) + self._test_cpu_percent(sum(cpu), last, new) + last = new + + def test_per_cpu_times_percent_negative(self): + # see: https://github.com/giampaolo/psutil/issues/645 + psutil.cpu_times_percent(percpu=True) + zero_times = [ + x._make([0 for x in range(len(x._fields))]) + for x in psutil.cpu_times(percpu=True) + ] + with mock.patch('psutil.cpu_times', return_value=zero_times): + for cpu in psutil.cpu_times_percent(percpu=True): + for percent in cpu: + self._test_cpu_percent(percent, None, None) + + def test_cpu_stats(self): + # Tested more extensively in per-platform test modules. + infos = psutil.cpu_stats() + self.assertEqual( + infos._fields, + ('ctx_switches', 'interrupts', 'soft_interrupts', 'syscalls'), + ) + for name in infos._fields: + value = getattr(infos, name) + self.assertGreaterEqual(value, 0) + # on AIX, ctx_switches is always 0 + if not AIX and name in ('ctx_switches', 'interrupts'): + self.assertGreater(value, 0) + + # TODO: remove this once 1892 is fixed + @unittest.skipIf( + MACOS and platform.machine() == 'arm64', "skipped due to #1892" + ) + @unittest.skipIf(not HAS_CPU_FREQ, "not supported") + def test_cpu_freq(self): + def check_ls(ls): + for nt in ls: + self.assertEqual(nt._fields, ('current', 'min', 'max')) + if nt.max != 0.0: + self.assertLessEqual(nt.current, nt.max) + for name in nt._fields: + value = getattr(nt, name) + self.assertIsInstance(value, (int, long, float)) + self.assertGreaterEqual(value, 0) + + ls = psutil.cpu_freq(percpu=True) + if FREEBSD and not ls: + raise self.skipTest("returns empty list on FreeBSD") + + assert ls, ls + check_ls([psutil.cpu_freq(percpu=False)]) + + if LINUX: + self.assertEqual(len(ls), psutil.cpu_count()) + + @unittest.skipIf(not HAS_GETLOADAVG, "not supported") + def test_getloadavg(self): + loadavg = psutil.getloadavg() + self.assertEqual(len(loadavg), 3) + for load in loadavg: + self.assertIsInstance(load, float) + self.assertGreaterEqual(load, 0.0) + + +class TestDiskAPIs(PsutilTestCase): + @unittest.skipIf(PYPY and not IS_64BIT, "unreliable on PYPY32 + 32BIT") + def test_disk_usage(self): + usage = psutil.disk_usage(os.getcwd()) + self.assertEqual(usage._fields, ('total', 'used', 'free', 'percent')) + + assert usage.total > 0, usage + assert usage.used > 0, usage + assert usage.free > 0, usage + assert usage.total > usage.used, usage + assert usage.total > usage.free, usage + assert 0 <= usage.percent <= 100, usage.percent + if hasattr(shutil, 'disk_usage'): + # py >= 3.3, see: http://bugs.python.org/issue12442 + shutil_usage = shutil.disk_usage(os.getcwd()) + tolerance = 5 * 1024 * 1024 # 5MB + self.assertEqual(usage.total, shutil_usage.total) + self.assertAlmostEqual( + usage.free, shutil_usage.free, delta=tolerance + ) + if not MACOS_12PLUS: + # see https://github.com/giampaolo/psutil/issues/2147 + self.assertAlmostEqual( + usage.used, shutil_usage.used, delta=tolerance + ) + + # if path does not exist OSError ENOENT is expected across + # all platforms + fname = self.get_testfn() + with self.assertRaises(FileNotFoundError): + psutil.disk_usage(fname) + + @unittest.skipIf(not ASCII_FS, "not an ASCII fs") + def test_disk_usage_unicode(self): + # See: https://github.com/giampaolo/psutil/issues/416 + with self.assertRaises(UnicodeEncodeError): + psutil.disk_usage(UNICODE_SUFFIX) + + def test_disk_usage_bytes(self): + psutil.disk_usage(b'.') + + def test_disk_partitions(self): + def check_ntuple(nt): + self.assertIsInstance(nt.device, str) + self.assertIsInstance(nt.mountpoint, str) + self.assertIsInstance(nt.fstype, str) + self.assertIsInstance(nt.opts, str) + self.assertIsInstance(nt.maxfile, (int, type(None))) + self.assertIsInstance(nt.maxpath, (int, type(None))) + if nt.maxfile is not None and not GITHUB_ACTIONS: + self.assertGreater(nt.maxfile, 0) + if nt.maxpath is not None: + self.assertGreater(nt.maxpath, 0) + + # all = False + ls = psutil.disk_partitions(all=False) + self.assertTrue(ls, msg=ls) + for disk in ls: + check_ntuple(disk) + if WINDOWS and 'cdrom' in disk.opts: + continue + if not POSIX: + assert os.path.exists(disk.device), disk + else: + # we cannot make any assumption about this, see: + # http://goo.gl/p9c43 + disk.device # noqa + # on modern systems mount points can also be files + assert os.path.exists(disk.mountpoint), disk + assert disk.fstype, disk + + # all = True + ls = psutil.disk_partitions(all=True) + self.assertTrue(ls, msg=ls) + for disk in psutil.disk_partitions(all=True): + check_ntuple(disk) + if not WINDOWS and disk.mountpoint: + try: + os.stat(disk.mountpoint) + except OSError as err: + if GITHUB_ACTIONS and MACOS and err.errno == errno.EIO: + continue + # http://mail.python.org/pipermail/python-dev/ + # 2012-June/120787.html + if err.errno not in (errno.EPERM, errno.EACCES): + raise + else: + assert os.path.exists(disk.mountpoint), disk + + # --- + + def find_mount_point(path): + path = os.path.abspath(path) + while not os.path.ismount(path): + path = os.path.dirname(path) + return path.lower() + + mount = find_mount_point(__file__) + mounts = [ + x.mountpoint.lower() + for x in psutil.disk_partitions(all=True) + if x.mountpoint + ] + self.assertIn(mount, mounts) + + @unittest.skipIf( + LINUX and not os.path.exists('/proc/diskstats'), + '/proc/diskstats not available on this linux version', + ) + @unittest.skipIf( + CI_TESTING and not psutil.disk_io_counters(), "unreliable on CI" + ) # no visible disks + def test_disk_io_counters(self): + def check_ntuple(nt): + self.assertEqual(nt[0], nt.read_count) + self.assertEqual(nt[1], nt.write_count) + self.assertEqual(nt[2], nt.read_bytes) + self.assertEqual(nt[3], nt.write_bytes) + if not (OPENBSD or NETBSD): + self.assertEqual(nt[4], nt.read_time) + self.assertEqual(nt[5], nt.write_time) + if LINUX: + self.assertEqual(nt[6], nt.read_merged_count) + self.assertEqual(nt[7], nt.write_merged_count) + self.assertEqual(nt[8], nt.busy_time) + elif FREEBSD: + self.assertEqual(nt[6], nt.busy_time) + for name in nt._fields: + assert getattr(nt, name) >= 0, nt + + ret = psutil.disk_io_counters(perdisk=False) + assert ret is not None, "no disks on this system?" + check_ntuple(ret) + ret = psutil.disk_io_counters(perdisk=True) + # make sure there are no duplicates + self.assertEqual(len(ret), len(set(ret))) + for key in ret: + assert key, key + check_ntuple(ret[key]) + + def test_disk_io_counters_no_disks(self): + # Emulate a case where no disks are installed, see: + # https://github.com/giampaolo/psutil/issues/1062 + with mock.patch( + 'psutil._psplatform.disk_io_counters', return_value={} + ) as m: + self.assertIsNone(psutil.disk_io_counters(perdisk=False)) + self.assertEqual(psutil.disk_io_counters(perdisk=True), {}) + assert m.called + + +class TestNetAPIs(PsutilTestCase): + @unittest.skipIf(not HAS_NET_IO_COUNTERS, 'not supported') + def test_net_io_counters(self): + def check_ntuple(nt): + self.assertEqual(nt[0], nt.bytes_sent) + self.assertEqual(nt[1], nt.bytes_recv) + self.assertEqual(nt[2], nt.packets_sent) + self.assertEqual(nt[3], nt.packets_recv) + self.assertEqual(nt[4], nt.errin) + self.assertEqual(nt[5], nt.errout) + self.assertEqual(nt[6], nt.dropin) + self.assertEqual(nt[7], nt.dropout) + assert nt.bytes_sent >= 0, nt + assert nt.bytes_recv >= 0, nt + assert nt.packets_sent >= 0, nt + assert nt.packets_recv >= 0, nt + assert nt.errin >= 0, nt + assert nt.errout >= 0, nt + assert nt.dropin >= 0, nt + assert nt.dropout >= 0, nt + + ret = psutil.net_io_counters(pernic=False) + check_ntuple(ret) + ret = psutil.net_io_counters(pernic=True) + self.assertNotEqual(ret, []) + for key in ret: + self.assertTrue(key) + self.assertIsInstance(key, str) + check_ntuple(ret[key]) + + @unittest.skipIf(not HAS_NET_IO_COUNTERS, 'not supported') + def test_net_io_counters_no_nics(self): + # Emulate a case where no NICs are installed, see: + # https://github.com/giampaolo/psutil/issues/1062 + with mock.patch( + 'psutil._psplatform.net_io_counters', return_value={} + ) as m: + self.assertIsNone(psutil.net_io_counters(pernic=False)) + self.assertEqual(psutil.net_io_counters(pernic=True), {}) + assert m.called + + def test_net_if_addrs(self): + nics = psutil.net_if_addrs() + assert nics, nics + + nic_stats = psutil.net_if_stats() + + # Not reliable on all platforms (net_if_addrs() reports more + # interfaces). + # self.assertEqual(sorted(nics.keys()), + # sorted(psutil.net_io_counters(pernic=True).keys())) + + families = set([socket.AF_INET, socket.AF_INET6, psutil.AF_LINK]) + for nic, addrs in nics.items(): + self.assertIsInstance(nic, str) + self.assertEqual(len(set(addrs)), len(addrs)) + for addr in addrs: + self.assertIsInstance(addr.family, int) + self.assertIsInstance(addr.address, str) + self.assertIsInstance(addr.netmask, (str, type(None))) + self.assertIsInstance(addr.broadcast, (str, type(None))) + self.assertIn(addr.family, families) + if PY3 and not PYPY: + self.assertIsInstance(addr.family, enum.IntEnum) + if nic_stats[nic].isup: + # Do not test binding to addresses of interfaces + # that are down + if addr.family == socket.AF_INET: + s = socket.socket(addr.family) + with contextlib.closing(s): + s.bind((addr.address, 0)) + elif addr.family == socket.AF_INET6: + info = socket.getaddrinfo( + addr.address, + 0, + socket.AF_INET6, + socket.SOCK_STREAM, + 0, + socket.AI_PASSIVE, + )[0] + af, socktype, proto, canonname, sa = info + s = socket.socket(af, socktype, proto) + with contextlib.closing(s): + s.bind(sa) + for ip in ( + addr.address, + addr.netmask, + addr.broadcast, + addr.ptp, + ): + if ip is not None: + # TODO: skip AF_INET6 for now because I get: + # AddressValueError: Only hex digits permitted in + # u'c6f3%lxcbr0' in u'fe80::c8e0:fff:fe54:c6f3%lxcbr0' + if addr.family != socket.AF_INET6: + check_net_address(ip, addr.family) + # broadcast and ptp addresses are mutually exclusive + if addr.broadcast: + self.assertIsNone(addr.ptp) + elif addr.ptp: + self.assertIsNone(addr.broadcast) + + if BSD or MACOS or SUNOS: + if hasattr(socket, "AF_LINK"): + self.assertEqual(psutil.AF_LINK, socket.AF_LINK) + elif LINUX: + self.assertEqual(psutil.AF_LINK, socket.AF_PACKET) + elif WINDOWS: + self.assertEqual(psutil.AF_LINK, -1) + + def test_net_if_addrs_mac_null_bytes(self): + # Simulate that the underlying C function returns an incomplete + # MAC address. psutil is supposed to fill it with null bytes. + # https://github.com/giampaolo/psutil/issues/786 + if POSIX: + ret = [('em1', psutil.AF_LINK, '06:3d:29', None, None, None)] + else: + ret = [('em1', -1, '06-3d-29', None, None, None)] + with mock.patch( + 'psutil._psplatform.net_if_addrs', return_value=ret + ) as m: + addr = psutil.net_if_addrs()['em1'][0] + assert m.called + if POSIX: + self.assertEqual(addr.address, '06:3d:29:00:00:00') + else: + self.assertEqual(addr.address, '06-3d-29-00-00-00') + + def test_net_if_stats(self): + nics = psutil.net_if_stats() + assert nics, nics + all_duplexes = ( + psutil.NIC_DUPLEX_FULL, + psutil.NIC_DUPLEX_HALF, + psutil.NIC_DUPLEX_UNKNOWN, + ) + for name, stats in nics.items(): + self.assertIsInstance(name, str) + isup, duplex, speed, mtu, flags = stats + self.assertIsInstance(isup, bool) + self.assertIn(duplex, all_duplexes) + self.assertIn(duplex, all_duplexes) + self.assertGreaterEqual(speed, 0) + self.assertGreaterEqual(mtu, 0) + self.assertIsInstance(flags, str) + + @unittest.skipIf( + not (LINUX or BSD or MACOS), "LINUX or BSD or MACOS specific" + ) + def test_net_if_stats_enodev(self): + # See: https://github.com/giampaolo/psutil/issues/1279 + with mock.patch( + 'psutil._psutil_posix.net_if_mtu', + side_effect=OSError(errno.ENODEV, ""), + ) as m: + ret = psutil.net_if_stats() + self.assertEqual(ret, {}) + assert m.called + + +class TestSensorsAPIs(PsutilTestCase): + @unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported") + def test_sensors_temperatures(self): + temps = psutil.sensors_temperatures() + for name, entries in temps.items(): + self.assertIsInstance(name, str) + for entry in entries: + self.assertIsInstance(entry.label, str) + if entry.current is not None: + self.assertGreaterEqual(entry.current, 0) + if entry.high is not None: + self.assertGreaterEqual(entry.high, 0) + if entry.critical is not None: + self.assertGreaterEqual(entry.critical, 0) + + @unittest.skipIf(not HAS_SENSORS_TEMPERATURES, "not supported") + def test_sensors_temperatures_fahreneit(self): + d = {'coretemp': [('label', 50.0, 60.0, 70.0)]} + with mock.patch( + "psutil._psplatform.sensors_temperatures", return_value=d + ) as m: + temps = psutil.sensors_temperatures(fahrenheit=True)['coretemp'][0] + assert m.called + self.assertEqual(temps.current, 122.0) + self.assertEqual(temps.high, 140.0) + self.assertEqual(temps.critical, 158.0) + + @unittest.skipIf(not HAS_SENSORS_BATTERY, "not supported") + @unittest.skipIf(not HAS_BATTERY, "no battery") + def test_sensors_battery(self): + ret = psutil.sensors_battery() + self.assertGreaterEqual(ret.percent, 0) + self.assertLessEqual(ret.percent, 100) + if ret.secsleft not in ( + psutil.POWER_TIME_UNKNOWN, + psutil.POWER_TIME_UNLIMITED, + ): + self.assertGreaterEqual(ret.secsleft, 0) + else: + if ret.secsleft == psutil.POWER_TIME_UNLIMITED: + self.assertTrue(ret.power_plugged) + self.assertIsInstance(ret.power_plugged, bool) + + @unittest.skipIf(not HAS_SENSORS_FANS, "not supported") + def test_sensors_fans(self): + fans = psutil.sensors_fans() + for name, entries in fans.items(): + self.assertIsInstance(name, str) + for entry in entries: + self.assertIsInstance(entry.label, str) + self.assertIsInstance(entry.current, (int, long)) + self.assertGreaterEqual(entry.current, 0) + + +if __name__ == '__main__': + from psutil.tests.runner import run_from_name + + run_from_name(__file__) diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_system.pyc b/addon/globalPlugins/soundmanager/psutil/tests/test_system.pyc deleted file mode 100644 index b95d6e9..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/tests/test_system.pyc and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_testutils.py b/addon/globalPlugins/soundmanager/psutil/tests/test_testutils.py new file mode 100644 index 0000000..147972c --- /dev/null +++ b/addon/globalPlugins/soundmanager/psutil/tests/test_testutils.py @@ -0,0 +1,453 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Tests for testing utils (psutil.tests namespace).""" + +import collections +import contextlib +import errno +import os +import socket +import stat +import subprocess +import unittest + +import psutil +import psutil.tests +from psutil import FREEBSD +from psutil import NETBSD +from psutil import POSIX +from psutil._common import open_binary +from psutil._common import open_text +from psutil._common import supports_ipv6 +from psutil.tests import CI_TESTING +from psutil.tests import COVERAGE +from psutil.tests import HAS_CONNECTIONS_UNIX +from psutil.tests import PYTHON_EXE +from psutil.tests import PYTHON_EXE_ENV +from psutil.tests import PsutilTestCase +from psutil.tests import TestMemoryLeak +from psutil.tests import bind_socket +from psutil.tests import bind_unix_socket +from psutil.tests import call_until +from psutil.tests import chdir +from psutil.tests import create_sockets +from psutil.tests import filter_proc_connections +from psutil.tests import get_free_port +from psutil.tests import is_namedtuple +from psutil.tests import mock +from psutil.tests import process_namespace +from psutil.tests import reap_children +from psutil.tests import retry +from psutil.tests import retry_on_failure +from psutil.tests import safe_mkdir +from psutil.tests import safe_rmpath +from psutil.tests import serialrun +from psutil.tests import system_namespace +from psutil.tests import tcp_socketpair +from psutil.tests import terminate +from psutil.tests import unix_socketpair +from psutil.tests import wait_for_file +from psutil.tests import wait_for_pid + + +# =================================================================== +# --- Unit tests for test utilities. +# =================================================================== + + +class TestRetryDecorator(PsutilTestCase): + @mock.patch('time.sleep') + def test_retry_success(self, sleep): + # Fail 3 times out of 5; make sure the decorated fun returns. + + @retry(retries=5, interval=1, logfun=None) + def foo(): + while queue: + queue.pop() + 1 / 0 # noqa + return 1 + + queue = list(range(3)) + self.assertEqual(foo(), 1) + self.assertEqual(sleep.call_count, 3) + + @mock.patch('time.sleep') + def test_retry_failure(self, sleep): + # Fail 6 times out of 5; th function is supposed to raise exc. + @retry(retries=5, interval=1, logfun=None) + def foo(): + while queue: + queue.pop() + 1 / 0 # noqa + return 1 + + queue = list(range(6)) + self.assertRaises(ZeroDivisionError, foo) + self.assertEqual(sleep.call_count, 5) + + @mock.patch('time.sleep') + def test_exception_arg(self, sleep): + @retry(exception=ValueError, interval=1) + def foo(): + raise TypeError + + self.assertRaises(TypeError, foo) + self.assertEqual(sleep.call_count, 0) + + @mock.patch('time.sleep') + def test_no_interval_arg(self, sleep): + # if interval is not specified sleep is not supposed to be called + + @retry(retries=5, interval=None, logfun=None) + def foo(): + 1 / 0 # noqa + + self.assertRaises(ZeroDivisionError, foo) + self.assertEqual(sleep.call_count, 0) + + @mock.patch('time.sleep') + def test_retries_arg(self, sleep): + @retry(retries=5, interval=1, logfun=None) + def foo(): + 1 / 0 # noqa + + self.assertRaises(ZeroDivisionError, foo) + self.assertEqual(sleep.call_count, 5) + + @mock.patch('time.sleep') + def test_retries_and_timeout_args(self, sleep): + self.assertRaises(ValueError, retry, retries=5, timeout=1) + + +class TestSyncTestUtils(PsutilTestCase): + def test_wait_for_pid(self): + wait_for_pid(os.getpid()) + nopid = max(psutil.pids()) + 99999 + with mock.patch('psutil.tests.retry.__iter__', return_value=iter([0])): + self.assertRaises(psutil.NoSuchProcess, wait_for_pid, nopid) + + def test_wait_for_file(self): + testfn = self.get_testfn() + with open(testfn, 'w') as f: + f.write('foo') + wait_for_file(testfn) + assert not os.path.exists(testfn) + + def test_wait_for_file_empty(self): + testfn = self.get_testfn() + with open(testfn, 'w'): + pass + wait_for_file(testfn, empty=True) + assert not os.path.exists(testfn) + + def test_wait_for_file_no_file(self): + testfn = self.get_testfn() + with mock.patch('psutil.tests.retry.__iter__', return_value=iter([0])): + self.assertRaises(IOError, wait_for_file, testfn) + + def test_wait_for_file_no_delete(self): + testfn = self.get_testfn() + with open(testfn, 'w') as f: + f.write('foo') + wait_for_file(testfn, delete=False) + assert os.path.exists(testfn) + + def test_call_until(self): + ret = call_until(lambda: 1, "ret == 1") + self.assertEqual(ret, 1) + + +class TestFSTestUtils(PsutilTestCase): + def test_open_text(self): + with open_text(__file__) as f: + self.assertEqual(f.mode, 'r') + + def test_open_binary(self): + with open_binary(__file__) as f: + self.assertEqual(f.mode, 'rb') + + def test_safe_mkdir(self): + testfn = self.get_testfn() + safe_mkdir(testfn) + assert os.path.isdir(testfn) + safe_mkdir(testfn) + assert os.path.isdir(testfn) + + def test_safe_rmpath(self): + # test file is removed + testfn = self.get_testfn() + open(testfn, 'w').close() + safe_rmpath(testfn) + assert not os.path.exists(testfn) + # test no exception if path does not exist + safe_rmpath(testfn) + # test dir is removed + os.mkdir(testfn) + safe_rmpath(testfn) + assert not os.path.exists(testfn) + # test other exceptions are raised + with mock.patch( + 'psutil.tests.os.stat', side_effect=OSError(errno.EINVAL, "") + ) as m: + with self.assertRaises(OSError): + safe_rmpath(testfn) + assert m.called + + def test_chdir(self): + testfn = self.get_testfn() + base = os.getcwd() + os.mkdir(testfn) + with chdir(testfn): + self.assertEqual(os.getcwd(), os.path.join(base, testfn)) + self.assertEqual(os.getcwd(), base) + + +class TestProcessUtils(PsutilTestCase): + def test_reap_children(self): + subp = self.spawn_testproc() + p = psutil.Process(subp.pid) + assert p.is_running() + reap_children() + assert not p.is_running() + assert not psutil.tests._pids_started + assert not psutil.tests._subprocesses_started + + def test_spawn_children_pair(self): + child, grandchild = self.spawn_children_pair() + self.assertNotEqual(child.pid, grandchild.pid) + assert child.is_running() + assert grandchild.is_running() + children = psutil.Process().children() + self.assertEqual(children, [child]) + children = psutil.Process().children(recursive=True) + self.assertEqual(len(children), 2) + self.assertIn(child, children) + self.assertIn(grandchild, children) + self.assertEqual(child.ppid(), os.getpid()) + self.assertEqual(grandchild.ppid(), child.pid) + + terminate(child) + assert not child.is_running() + assert grandchild.is_running() + + terminate(grandchild) + assert not grandchild.is_running() + + @unittest.skipIf(not POSIX, "POSIX only") + def test_spawn_zombie(self): + parent, zombie = self.spawn_zombie() + self.assertEqual(zombie.status(), psutil.STATUS_ZOMBIE) + + def test_terminate(self): + # by subprocess.Popen + p = self.spawn_testproc() + terminate(p) + self.assertPidGone(p.pid) + terminate(p) + # by psutil.Process + p = psutil.Process(self.spawn_testproc().pid) + terminate(p) + self.assertPidGone(p.pid) + terminate(p) + # by psutil.Popen + cmd = [PYTHON_EXE, "-c", "import time; time.sleep(60);"] + p = psutil.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=PYTHON_EXE_ENV, + ) + terminate(p) + self.assertPidGone(p.pid) + terminate(p) + # by PID + pid = self.spawn_testproc().pid + terminate(pid) + self.assertPidGone(p.pid) + terminate(pid) + # zombie + if POSIX: + parent, zombie = self.spawn_zombie() + terminate(parent) + terminate(zombie) + self.assertPidGone(parent.pid) + self.assertPidGone(zombie.pid) + + +class TestNetUtils(PsutilTestCase): + def bind_socket(self): + port = get_free_port() + with contextlib.closing(bind_socket(addr=('', port))) as s: + self.assertEqual(s.getsockname()[1], port) + + @unittest.skipIf(not POSIX, "POSIX only") + def test_bind_unix_socket(self): + name = self.get_testfn() + sock = bind_unix_socket(name) + with contextlib.closing(sock): + self.assertEqual(sock.family, socket.AF_UNIX) + self.assertEqual(sock.type, socket.SOCK_STREAM) + self.assertEqual(sock.getsockname(), name) + assert os.path.exists(name) + assert stat.S_ISSOCK(os.stat(name).st_mode) + # UDP + name = self.get_testfn() + sock = bind_unix_socket(name, type=socket.SOCK_DGRAM) + with contextlib.closing(sock): + self.assertEqual(sock.type, socket.SOCK_DGRAM) + + def tcp_tcp_socketpair(self): + addr = ("127.0.0.1", get_free_port()) + server, client = tcp_socketpair(socket.AF_INET, addr=addr) + with contextlib.closing(server): + with contextlib.closing(client): + # Ensure they are connected and the positions are + # correct. + self.assertEqual(server.getsockname(), addr) + self.assertEqual(client.getpeername(), addr) + self.assertNotEqual(client.getsockname(), addr) + + @unittest.skipIf(not POSIX, "POSIX only") + @unittest.skipIf( + NETBSD or FREEBSD, "/var/run/log UNIX socket opened by default" + ) + def test_unix_socketpair(self): + p = psutil.Process() + num_fds = p.num_fds() + self.assertEqual( + filter_proc_connections(p.connections(kind='unix')), [] + ) + name = self.get_testfn() + server, client = unix_socketpair(name) + try: + assert os.path.exists(name) + assert stat.S_ISSOCK(os.stat(name).st_mode) + self.assertEqual(p.num_fds() - num_fds, 2) + self.assertEqual( + len(filter_proc_connections(p.connections(kind='unix'))), 2 + ) + self.assertEqual(server.getsockname(), name) + self.assertEqual(client.getpeername(), name) + finally: + client.close() + server.close() + + def test_create_sockets(self): + with create_sockets() as socks: + fams = collections.defaultdict(int) + types = collections.defaultdict(int) + for s in socks: + fams[s.family] += 1 + # work around http://bugs.python.org/issue30204 + types[s.getsockopt(socket.SOL_SOCKET, socket.SO_TYPE)] += 1 + self.assertGreaterEqual(fams[socket.AF_INET], 2) + if supports_ipv6(): + self.assertGreaterEqual(fams[socket.AF_INET6], 2) + if POSIX and HAS_CONNECTIONS_UNIX: + self.assertGreaterEqual(fams[socket.AF_UNIX], 2) + self.assertGreaterEqual(types[socket.SOCK_STREAM], 2) + self.assertGreaterEqual(types[socket.SOCK_DGRAM], 2) + + +@serialrun +class TestMemLeakClass(TestMemoryLeak): + @retry_on_failure() + def test_times(self): + def fun(): + cnt['cnt'] += 1 + + cnt = {'cnt': 0} + self.execute(fun, times=10, warmup_times=15) + self.assertEqual(cnt['cnt'], 26) + + def test_param_err(self): + self.assertRaises(ValueError, self.execute, lambda: 0, times=0) + self.assertRaises(ValueError, self.execute, lambda: 0, times=-1) + self.assertRaises(ValueError, self.execute, lambda: 0, warmup_times=-1) + self.assertRaises(ValueError, self.execute, lambda: 0, tolerance=-1) + self.assertRaises(ValueError, self.execute, lambda: 0, retries=-1) + + @retry_on_failure() + @unittest.skipIf(CI_TESTING, "skipped on CI") + @unittest.skipIf(COVERAGE, "skipped during test coverage") + def test_leak_mem(self): + ls = [] + + def fun(ls=ls): + ls.append("x" * 124 * 1024) + + try: + # will consume around 30M in total + self.assertRaisesRegex( + AssertionError, "extra-mem", self.execute, fun, times=50 + ) + finally: + del ls + + def test_unclosed_files(self): + def fun(): + f = open(__file__) + self.addCleanup(f.close) + box.append(f) + + box = [] + kind = "fd" if POSIX else "handle" + self.assertRaisesRegex( + AssertionError, "unclosed " + kind, self.execute, fun + ) + + def test_tolerance(self): + def fun(): + ls.append("x" * 24 * 1024) + + ls = [] + times = 100 + self.execute( + fun, times=times, warmup_times=0, tolerance=200 * 1024 * 1024 + ) + self.assertEqual(len(ls), times + 1) + + def test_execute_w_exc(self): + def fun_1(): + 1 / 0 # noqa + + self.execute_w_exc(ZeroDivisionError, fun_1) + with self.assertRaises(ZeroDivisionError): + self.execute_w_exc(OSError, fun_1) + + def fun_2(): + pass + + with self.assertRaises(AssertionError): + self.execute_w_exc(ZeroDivisionError, fun_2) + + +class TestTestingUtils(PsutilTestCase): + def test_process_namespace(self): + p = psutil.Process() + ns = process_namespace(p) + ns.test() + fun = [x for x in ns.iter(ns.getters) if x[1] == 'ppid'][0][0] + self.assertEqual(fun(), p.ppid()) + + def test_system_namespace(self): + ns = system_namespace() + fun = [x for x in ns.iter(ns.getters) if x[1] == 'net_if_addrs'][0][0] + self.assertEqual(fun(), psutil.net_if_addrs()) + + +class TestOtherUtils(PsutilTestCase): + def test_is_namedtuple(self): + assert is_namedtuple(collections.namedtuple('foo', 'a b c')(1, 2, 3)) + assert not is_namedtuple(tuple()) + + +if __name__ == '__main__': + from psutil.tests.runner import run_from_name + + run_from_name(__file__) diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_unicode.py b/addon/globalPlugins/soundmanager/psutil/tests/test_unicode.py index f7115e3..9d3706f 100644 --- a/addon/globalPlugins/soundmanager/psutil/tests/test_unicode.py +++ b/addon/globalPlugins/soundmanager/psutil/tests/test_unicode.py @@ -1,370 +1,365 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -""" -Notes about unicode handling in psutil -====================================== - -In psutil these are the APIs returning or dealing with a string -('not tested' means they are not tested to deal with non-ASCII strings): - -* Process.cmdline() -* Process.connections('unix') -* Process.cwd() -* Process.environ() -* Process.exe() -* Process.memory_maps() -* Process.name() -* Process.open_files() -* Process.username() (not tested) - -* disk_io_counters() (not tested) -* disk_partitions() (not tested) -* disk_usage(str) -* net_connections('unix') -* net_if_addrs() (not tested) -* net_if_stats() (not tested) -* net_io_counters() (not tested) -* sensors_fans() (not tested) -* sensors_temperatures() (not tested) -* users() (not tested) - -* WindowsService.binpath() (not tested) -* WindowsService.description() (not tested) -* WindowsService.display_name() (not tested) -* WindowsService.name() (not tested) -* WindowsService.status() (not tested) -* WindowsService.username() (not tested) - -In here we create a unicode path with a funky non-ASCII name and (where -possible) make psutil return it back (e.g. on name(), exe(), open_files(), -etc.) and make sure that: - -* psutil never crashes with UnicodeDecodeError -* the returned path matches - -For a detailed explanation of how psutil handles unicode see: -- https://github.com/giampaolo/psutil/issues/1040 -- http://psutil.readthedocs.io/#unicode -""" - -import os -import traceback -import warnings -from contextlib import closing - -from psutil import BSD -from psutil import MACOS -from psutil import OPENBSD -from psutil import POSIX -from psutil import WINDOWS -from psutil._compat import PY3 -from psutil._compat import u -from psutil.tests import APPVEYOR -from psutil.tests import ASCII_FS -from psutil.tests import bind_unix_socket -from psutil.tests import chdir -from psutil.tests import copyload_shared_lib -from psutil.tests import create_exe -from psutil.tests import get_test_subprocess -from psutil.tests import HAS_CONNECTIONS_UNIX -from psutil.tests import HAS_ENVIRON -from psutil.tests import HAS_MEMORY_MAPS -from psutil.tests import mock -from psutil.tests import PYPY -from psutil.tests import reap_children -from psutil.tests import safe_mkdir -from psutil.tests import safe_rmpath as _safe_rmpath -from psutil.tests import skip_on_access_denied -from psutil.tests import TESTFILE_PREFIX -from psutil.tests import TESTFN -from psutil.tests import TESTFN_UNICODE -from psutil.tests import TRAVIS -from psutil.tests import unittest -from psutil.tests import unix_socket_path -import psutil - - -def safe_rmpath(path): - if APPVEYOR: - # TODO - this is quite random and I'm not sure why it happens, - # nor I can reproduce it locally: - # https://ci.appveyor.com/project/giampaolo/psutil/build/job/ - # jiq2cgd6stsbtn60 - # safe_rmpath() happens after reap_children() so this is weird - # Perhaps wait_procs() on Windows is broken? Maybe because - # of STILL_ACTIVE? - # https://github.com/giampaolo/psutil/blob/ - # 68c7a70728a31d8b8b58f4be6c4c0baa2f449eda/psutil/arch/ - # windows/process_info.c#L146 - try: - return _safe_rmpath(path) - except WindowsError: - traceback.print_exc() - else: - return _safe_rmpath(path) - - -def subprocess_supports_unicode(name): - """Return True if both the fs and the subprocess module can - deal with a unicode file name. - """ - if PY3: - return True - try: - safe_rmpath(name) - create_exe(name) - get_test_subprocess(cmd=[name]) - except UnicodeEncodeError: - return False - else: - return True - finally: - reap_children() - - -# An invalid unicode string. -if PY3: - INVALID_NAME = (TESTFN.encode('utf8') + b"f\xc0\x80").decode( - 'utf8', 'surrogateescape') -else: - INVALID_NAME = TESTFN + "f\xc0\x80" - - -# =================================================================== -# FS APIs -# =================================================================== - - -class _BaseFSAPIsTests(object): - funky_name = None - - @classmethod - def setUpClass(cls): - safe_rmpath(cls.funky_name) - create_exe(cls.funky_name) - - @classmethod - def tearDownClass(cls): - reap_children() - safe_rmpath(cls.funky_name) - - def tearDown(self): - reap_children() - - def expect_exact_path_match(self): - raise NotImplementedError("must be implemented in subclass") - - def test_proc_exe(self): - subp = get_test_subprocess(cmd=[self.funky_name]) - p = psutil.Process(subp.pid) - exe = p.exe() - self.assertIsInstance(exe, str) - if self.expect_exact_path_match(): - self.assertEqual(exe, self.funky_name) - - def test_proc_name(self): - subp = get_test_subprocess(cmd=[self.funky_name]) - if WINDOWS: - # On Windows name() is determined from exe() first, because - # it's faster; we want to overcome the internal optimization - # and test name() instead of exe(). - with mock.patch("psutil._psplatform.cext.proc_exe", - side_effect=psutil.AccessDenied(os.getpid())) as m: - name = psutil.Process(subp.pid).name() - assert m.called - else: - name = psutil.Process(subp.pid).name() - self.assertIsInstance(name, str) - if self.expect_exact_path_match(): - self.assertEqual(name, os.path.basename(self.funky_name)) - - def test_proc_cmdline(self): - subp = get_test_subprocess(cmd=[self.funky_name]) - p = psutil.Process(subp.pid) - cmdline = p.cmdline() - for part in cmdline: - self.assertIsInstance(part, str) - if self.expect_exact_path_match(): - self.assertEqual(cmdline, [self.funky_name]) - - def test_proc_cwd(self): - dname = self.funky_name + "2" - self.addCleanup(safe_rmpath, dname) - safe_mkdir(dname) - with chdir(dname): - p = psutil.Process() - cwd = p.cwd() - self.assertIsInstance(p.cwd(), str) - if self.expect_exact_path_match(): - self.assertEqual(cwd, dname) - - def test_proc_open_files(self): - p = psutil.Process() - start = set(p.open_files()) - with open(self.funky_name, 'rb'): - new = set(p.open_files()) - path = (new - start).pop().path - self.assertIsInstance(path, str) - if BSD and not path: - # XXX - see https://github.com/giampaolo/psutil/issues/595 - return self.skipTest("open_files on BSD is broken") - if self.expect_exact_path_match(): - self.assertEqual(os.path.normcase(path), - os.path.normcase(self.funky_name)) - - @unittest.skipIf(not POSIX, "POSIX only") - def test_proc_connections(self): - suffix = os.path.basename(self.funky_name) - with unix_socket_path(suffix=suffix) as name: - try: - sock = bind_unix_socket(name) - except UnicodeEncodeError: - if PY3: - raise - else: - raise unittest.SkipTest("not supported") - with closing(sock): - conn = psutil.Process().connections('unix')[0] - self.assertIsInstance(conn.laddr, str) - # AF_UNIX addr not set on OpenBSD - if not OPENBSD: - self.assertEqual(conn.laddr, name) - - @unittest.skipIf(not POSIX, "POSIX only") - @unittest.skipIf(not HAS_CONNECTIONS_UNIX, "can't list UNIX sockets") - @skip_on_access_denied() - def test_net_connections(self): - def find_sock(cons): - for conn in cons: - if os.path.basename(conn.laddr).startswith(TESTFILE_PREFIX): - return conn - raise ValueError("connection not found") - - suffix = os.path.basename(self.funky_name) - with unix_socket_path(suffix=suffix) as name: - try: - sock = bind_unix_socket(name) - except UnicodeEncodeError: - if PY3: - raise - else: - raise unittest.SkipTest("not supported") - with closing(sock): - cons = psutil.net_connections(kind='unix') - # AF_UNIX addr not set on OpenBSD - if not OPENBSD: - conn = find_sock(cons) - self.assertIsInstance(conn.laddr, str) - self.assertEqual(conn.laddr, name) - - def test_disk_usage(self): - dname = self.funky_name + "2" - self.addCleanup(safe_rmpath, dname) - safe_mkdir(dname) - psutil.disk_usage(dname) - - @unittest.skipIf(not HAS_MEMORY_MAPS, "not supported") - @unittest.skipIf(not PY3, "ctypes does not support unicode on PY2") - def test_memory_maps(self): - # XXX: on Python 2, using ctypes.CDLL with a unicode path - # opens a message box which blocks the test run. - with copyload_shared_lib(dst_prefix=self.funky_name) as funky_path: - def normpath(p): - return os.path.realpath(os.path.normcase(p)) - libpaths = [normpath(x.path) - for x in psutil.Process().memory_maps()] - # ...just to have a clearer msg in case of failure - libpaths = [x for x in libpaths if TESTFILE_PREFIX in x] - self.assertIn(normpath(funky_path), libpaths) - for path in libpaths: - self.assertIsInstance(path, str) - - -# https://travis-ci.org/giampaolo/psutil/jobs/440073249 -@unittest.skipIf(PYPY and TRAVIS, "unreliable on PYPY + TRAVIS") -@unittest.skipIf(MACOS and TRAVIS, "unreliable on TRAVIS") # TODO -@unittest.skipIf(ASCII_FS, "ASCII fs") -@unittest.skipIf(not subprocess_supports_unicode(TESTFN_UNICODE), - "subprocess can't deal with unicode") -class TestFSAPIs(_BaseFSAPIsTests, unittest.TestCase): - """Test FS APIs with a funky, valid, UTF8 path name.""" - funky_name = TESTFN_UNICODE - - @classmethod - def expect_exact_path_match(cls): - # Do not expect psutil to correctly handle unicode paths on - # Python 2 if os.listdir() is not able either. - if PY3: - return True - else: - here = '.' if isinstance(cls.funky_name, str) else u('.') - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - return cls.funky_name in os.listdir(here) - - -@unittest.skipIf(PYPY and TRAVIS, "unreliable on PYPY + TRAVIS") -@unittest.skipIf(MACOS and TRAVIS, "unreliable on TRAVIS") # TODO -@unittest.skipIf(not subprocess_supports_unicode(INVALID_NAME), - "subprocess can't deal with invalid unicode") -class TestFSAPIsWithInvalidPath(_BaseFSAPIsTests, unittest.TestCase): - """Test FS APIs with a funky, invalid path name.""" - funky_name = INVALID_NAME - - @classmethod - def expect_exact_path_match(cls): - # Invalid unicode names are supposed to work on Python 2. - return True - - -@unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestWinProcessName(unittest.TestCase): - - def test_name_type(self): - # On Windows name() is determined from exe() first, because - # it's faster; we want to overcome the internal optimization - # and test name() instead of exe(). - with mock.patch("psutil._psplatform.cext.proc_exe", - side_effect=psutil.AccessDenied(os.getpid())) as m: - self.assertIsInstance(psutil.Process().name(), str) - assert m.called - - -# =================================================================== -# Non fs APIs -# =================================================================== - - -class TestNonFSAPIS(unittest.TestCase): - """Unicode tests for non fs-related APIs.""" - - def tearDown(self): - reap_children() - - @unittest.skipIf(not HAS_ENVIRON, "not supported") - def test_proc_environ(self): - # Note: differently from others, this test does not deal - # with fs paths. On Python 2 subprocess module is broken as - # it's not able to handle with non-ASCII env vars, so - # we use "è", which is part of the extended ASCII table - # (unicode point <= 255). - env = os.environ.copy() - funky_str = TESTFN_UNICODE if PY3 else 'è' - env['FUNNY_ARG'] = funky_str - sproc = get_test_subprocess(env=env) - p = psutil.Process(sproc.pid) - env = p.environ() - for k, v in env.items(): - self.assertIsInstance(k, str) - self.assertIsInstance(v, str) - self.assertEqual(env['FUNNY_ARG'], funky_str) - - -if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Notes about unicode handling in psutil +======================================. + +Starting from version 5.3.0 psutil adds unicode support, see: +https://github.com/giampaolo/psutil/issues/1040 +The notes below apply to *any* API returning a string such as +process exe(), cwd() or username(): + +* all strings are encoded by using the OS filesystem encoding + (sys.getfilesystemencoding()) which varies depending on the platform + (e.g. "UTF-8" on macOS, "mbcs" on Win) +* no API call is supposed to crash with UnicodeDecodeError +* instead, in case of badly encoded data returned by the OS, the + following error handlers are used to replace the corrupted characters in + the string: + * Python 3: sys.getfilesystemencodeerrors() (PY 3.6+) or + "surrogatescape" on POSIX and "replace" on Windows + * Python 2: "replace" +* on Python 2 all APIs return bytes (str type), never unicode +* on Python 2, you can go back to unicode by doing: + + >>> unicode(p.exe(), sys.getdefaultencoding(), errors="replace") + +For a detailed explanation of how psutil handles unicode see #1040. + +Tests +===== + +List of APIs returning or dealing with a string: +('not tested' means they are not tested to deal with non-ASCII strings): + +* Process.cmdline() +* Process.connections('unix') +* Process.cwd() +* Process.environ() +* Process.exe() +* Process.memory_maps() +* Process.name() +* Process.open_files() +* Process.username() (not tested) + +* disk_io_counters() (not tested) +* disk_partitions() (not tested) +* disk_usage(str) +* net_connections('unix') +* net_if_addrs() (not tested) +* net_if_stats() (not tested) +* net_io_counters() (not tested) +* sensors_fans() (not tested) +* sensors_temperatures() (not tested) +* users() (not tested) + +* WindowsService.binpath() (not tested) +* WindowsService.description() (not tested) +* WindowsService.display_name() (not tested) +* WindowsService.name() (not tested) +* WindowsService.status() (not tested) +* WindowsService.username() (not tested) + +In here we create a unicode path with a funky non-ASCII name and (where +possible) make psutil return it back (e.g. on name(), exe(), open_files(), +etc.) and make sure that: + +* psutil never crashes with UnicodeDecodeError +* the returned path matches +""" + +import os +import shutil +import traceback +import unittest +import warnings +from contextlib import closing + +import psutil +from psutil import BSD +from psutil import POSIX +from psutil import WINDOWS +from psutil._compat import PY3 +from psutil._compat import super +from psutil._compat import u +from psutil.tests import APPVEYOR +from psutil.tests import ASCII_FS +from psutil.tests import CI_TESTING +from psutil.tests import HAS_CONNECTIONS_UNIX +from psutil.tests import HAS_ENVIRON +from psutil.tests import HAS_MEMORY_MAPS +from psutil.tests import INVALID_UNICODE_SUFFIX +from psutil.tests import PYPY +from psutil.tests import TESTFN_PREFIX +from psutil.tests import UNICODE_SUFFIX +from psutil.tests import PsutilTestCase +from psutil.tests import bind_unix_socket +from psutil.tests import chdir +from psutil.tests import copyload_shared_lib +from psutil.tests import create_py_exe +from psutil.tests import get_testfn +from psutil.tests import safe_mkdir +from psutil.tests import safe_rmpath +from psutil.tests import serialrun +from psutil.tests import skip_on_access_denied +from psutil.tests import spawn_testproc +from psutil.tests import terminate + + +if APPVEYOR: + + def safe_rmpath(path): # NOQA + # TODO - this is quite random and I'm not sure why it happens, + # nor I can reproduce it locally: + # https://ci.appveyor.com/project/giampaolo/psutil/build/job/ + # jiq2cgd6stsbtn60 + # safe_rmpath() happens after reap_children() so this is weird + # Perhaps wait_procs() on Windows is broken? Maybe because + # of STILL_ACTIVE? + # https://github.com/giampaolo/psutil/blob/ + # 68c7a70728a31d8b8b58f4be6c4c0baa2f449eda/psutil/arch/ + # windows/process_info.c#L146 + from psutil.tests import safe_rmpath as rm + + try: + return rm(path) + except WindowsError: + traceback.print_exc() + + +def try_unicode(suffix): + """Return True if both the fs and the subprocess module can + deal with a unicode file name. + """ + sproc = None + testfn = get_testfn(suffix=suffix) + try: + safe_rmpath(testfn) + create_py_exe(testfn) + sproc = spawn_testproc(cmd=[testfn]) + shutil.copyfile(testfn, testfn + '-2') + safe_rmpath(testfn + '-2') + except (UnicodeEncodeError, IOError): + return False + else: + return True + finally: + if sproc is not None: + terminate(sproc) + safe_rmpath(testfn) + + +# =================================================================== +# FS APIs +# =================================================================== + + +class BaseUnicodeTest(PsutilTestCase): + funky_suffix = None + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.skip_tests = False + cls.funky_name = None + if cls.funky_suffix is not None: + if not try_unicode(cls.funky_suffix): + cls.skip_tests = True + else: + cls.funky_name = get_testfn(suffix=cls.funky_suffix) + create_py_exe(cls.funky_name) + + def setUp(self): + super().setUp() + if self.skip_tests: + raise self.skipTest("can't handle unicode str") + + +@serialrun +@unittest.skipIf(ASCII_FS, "ASCII fs") +@unittest.skipIf(PYPY and not PY3, "too much trouble on PYPY2") +class TestFSAPIs(BaseUnicodeTest): + """Test FS APIs with a funky, valid, UTF8 path name.""" + + funky_suffix = UNICODE_SUFFIX + + def expect_exact_path_match(self): + # Do not expect psutil to correctly handle unicode paths on + # Python 2 if os.listdir() is not able either. + here = '.' if isinstance(self.funky_name, str) else u('.') + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + return self.funky_name in os.listdir(here) + + # --- + + def test_proc_exe(self): + cmd = [self.funky_name, "-c", "import time; time.sleep(10)"] + subp = self.spawn_testproc(cmd) + p = psutil.Process(subp.pid) + exe = p.exe() + self.assertIsInstance(exe, str) + if self.expect_exact_path_match(): + self.assertEqual( + os.path.normcase(exe), os.path.normcase(self.funky_name) + ) + + def test_proc_name(self): + cmd = [self.funky_name, "-c", "import time; time.sleep(10)"] + subp = self.spawn_testproc(cmd) + name = psutil.Process(subp.pid).name() + self.assertIsInstance(name, str) + if self.expect_exact_path_match(): + self.assertEqual(name, os.path.basename(self.funky_name)) + + def test_proc_cmdline(self): + cmd = [self.funky_name, "-c", "import time; time.sleep(10)"] + subp = self.spawn_testproc(cmd) + p = psutil.Process(subp.pid) + cmdline = p.cmdline() + for part in cmdline: + self.assertIsInstance(part, str) + if self.expect_exact_path_match(): + self.assertEqual(cmdline, cmd) + + def test_proc_cwd(self): + dname = self.funky_name + "2" + self.addCleanup(safe_rmpath, dname) + safe_mkdir(dname) + with chdir(dname): + p = psutil.Process() + cwd = p.cwd() + self.assertIsInstance(p.cwd(), str) + if self.expect_exact_path_match(): + self.assertEqual(cwd, dname) + + @unittest.skipIf(PYPY and WINDOWS, "fails on PYPY + WINDOWS") + def test_proc_open_files(self): + p = psutil.Process() + start = set(p.open_files()) + with open(self.funky_name, 'rb'): + new = set(p.open_files()) + path = (new - start).pop().path + self.assertIsInstance(path, str) + if BSD and not path: + # XXX - see https://github.com/giampaolo/psutil/issues/595 + return self.skipTest("open_files on BSD is broken") + if self.expect_exact_path_match(): + self.assertEqual( + os.path.normcase(path), os.path.normcase(self.funky_name) + ) + + @unittest.skipIf(not POSIX, "POSIX only") + def test_proc_connections(self): + name = self.get_testfn(suffix=self.funky_suffix) + try: + sock = bind_unix_socket(name) + except UnicodeEncodeError: + if PY3: + raise + else: + raise unittest.SkipTest("not supported") + with closing(sock): + conn = psutil.Process().connections('unix')[0] + self.assertIsInstance(conn.laddr, str) + self.assertEqual(conn.laddr, name) + + @unittest.skipIf(not POSIX, "POSIX only") + @unittest.skipIf(not HAS_CONNECTIONS_UNIX, "can't list UNIX sockets") + @skip_on_access_denied() + def test_net_connections(self): + def find_sock(cons): + for conn in cons: + if os.path.basename(conn.laddr).startswith(TESTFN_PREFIX): + return conn + raise ValueError("connection not found") + + name = self.get_testfn(suffix=self.funky_suffix) + try: + sock = bind_unix_socket(name) + except UnicodeEncodeError: + if PY3: + raise + else: + raise unittest.SkipTest("not supported") + with closing(sock): + cons = psutil.net_connections(kind='unix') + conn = find_sock(cons) + self.assertIsInstance(conn.laddr, str) + self.assertEqual(conn.laddr, name) + + def test_disk_usage(self): + dname = self.funky_name + "2" + self.addCleanup(safe_rmpath, dname) + safe_mkdir(dname) + psutil.disk_usage(dname) + + @unittest.skipIf(not HAS_MEMORY_MAPS, "not supported") + @unittest.skipIf(not PY3, "ctypes does not support unicode on PY2") + @unittest.skipIf(PYPY, "unstable on PYPY") + def test_memory_maps(self): + # XXX: on Python 2, using ctypes.CDLL with a unicode path + # opens a message box which blocks the test run. + with copyload_shared_lib(suffix=self.funky_suffix) as funky_path: + + def normpath(p): + return os.path.realpath(os.path.normcase(p)) + + libpaths = [ + normpath(x.path) for x in psutil.Process().memory_maps() + ] + # ...just to have a clearer msg in case of failure + libpaths = [x for x in libpaths if TESTFN_PREFIX in x] + self.assertIn(normpath(funky_path), libpaths) + for path in libpaths: + self.assertIsInstance(path, str) + + +@unittest.skipIf(CI_TESTING, "unreliable on CI") +class TestFSAPIsWithInvalidPath(TestFSAPIs): + """Test FS APIs with a funky, invalid path name.""" + + funky_suffix = INVALID_UNICODE_SUFFIX + + def expect_exact_path_match(self): + # Invalid unicode names are supposed to work on Python 2. + return True + + +# =================================================================== +# Non fs APIs +# =================================================================== + + +class TestNonFSAPIS(BaseUnicodeTest): + """Unicode tests for non fs-related APIs.""" + + funky_suffix = UNICODE_SUFFIX if PY3 else 'è' + + @unittest.skipIf(not HAS_ENVIRON, "not supported") + @unittest.skipIf(PYPY and WINDOWS, "segfaults on PYPY + WINDOWS") + def test_proc_environ(self): + # Note: differently from others, this test does not deal + # with fs paths. On Python 2 subprocess module is broken as + # it's not able to handle with non-ASCII env vars, so + # we use "è", which is part of the extended ASCII table + # (unicode point <= 255). + env = os.environ.copy() + env['FUNNY_ARG'] = self.funky_suffix + sproc = self.spawn_testproc(env=env) + p = psutil.Process(sproc.pid) + env = p.environ() + for k, v in env.items(): + self.assertIsInstance(k, str) + self.assertIsInstance(v, str) + self.assertEqual(env['FUNNY_ARG'], self.funky_suffix) + + +if __name__ == '__main__': + from psutil.tests.runner import run_from_name + + run_from_name(__file__) diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_unicode.pyc b/addon/globalPlugins/soundmanager/psutil/tests/test_unicode.pyc deleted file mode 100644 index 9f80df9..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/tests/test_unicode.pyc and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_windows.py b/addon/globalPlugins/soundmanager/psutil/tests/test_windows.py index 5a998dd..007579f 100644 --- a/addon/globalPlugins/soundmanager/psutil/tests/test_windows.py +++ b/addon/globalPlugins/soundmanager/psutil/tests/test_windows.py @@ -1,875 +1,967 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -* - -# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -"""Windows specific tests.""" - -import datetime -import errno -import glob -import os -import platform -import re -import signal -import subprocess -import sys -import time -import warnings - -import psutil -from psutil import WINDOWS -from psutil.tests import APPVEYOR -from psutil.tests import get_test_subprocess -from psutil.tests import HAS_BATTERY -from psutil.tests import mock -from psutil.tests import PY3 -from psutil.tests import reap_children -from psutil.tests import retry_on_failure -from psutil.tests import sh -from psutil.tests import unittest - -with warnings.catch_warnings(): - warnings.simplefilter("ignore") - try: - import win32api # requires "pip install pypiwin32" - import win32con - import win32process - import wmi # requires "pip install wmi" / "make setup-dev-env" - except ImportError: - if os.name == 'nt': - raise - - -cext = psutil._psplatform.cext - -# are we a 64 bit process -IS_64_BIT = sys.maxsize > 2**32 - - -def wrap_exceptions(fun): - def wrapper(self, *args, **kwargs): - try: - return fun(self, *args, **kwargs) - except OSError as err: - from psutil._pswindows import ACCESS_DENIED_SET - if err.errno in ACCESS_DENIED_SET: - raise psutil.AccessDenied(None, None) - if err.errno == errno.ESRCH: - raise psutil.NoSuchProcess(None, None) - raise - return wrapper - - -# =================================================================== -# System APIs -# =================================================================== - - -@unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestCpuAPIs(unittest.TestCase): - - @unittest.skipIf('NUMBER_OF_PROCESSORS' not in os.environ, - 'NUMBER_OF_PROCESSORS env var is not available') - def test_cpu_count_vs_NUMBER_OF_PROCESSORS(self): - # Will likely fail on many-cores systems: - # https://stackoverflow.com/questions/31209256 - num_cpus = int(os.environ['NUMBER_OF_PROCESSORS']) - self.assertEqual(num_cpus, psutil.cpu_count()) - - def test_cpu_count_vs_GetSystemInfo(self): - # Will likely fail on many-cores systems: - # https://stackoverflow.com/questions/31209256 - sys_value = win32api.GetSystemInfo()[5] - psutil_value = psutil.cpu_count() - self.assertEqual(sys_value, psutil_value) - - def test_cpu_count_logical_vs_wmi(self): - w = wmi.WMI() - proc = w.Win32_Processor()[0] - self.assertEqual(psutil.cpu_count(), proc.NumberOfLogicalProcessors) - - def test_cpu_count_phys_vs_wmi(self): - w = wmi.WMI() - proc = w.Win32_Processor()[0] - self.assertEqual(psutil.cpu_count(logical=False), proc.NumberOfCores) - - def test_cpu_count_vs_cpu_times(self): - self.assertEqual(psutil.cpu_count(), - len(psutil.cpu_times(percpu=True))) - - def test_cpu_freq(self): - w = wmi.WMI() - proc = w.Win32_Processor()[0] - self.assertEqual(proc.CurrentClockSpeed, psutil.cpu_freq().current) - self.assertEqual(proc.MaxClockSpeed, psutil.cpu_freq().max) - - -@unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestSystemAPIs(unittest.TestCase): - - def test_nic_names(self): - out = sh('ipconfig /all') - nics = psutil.net_io_counters(pernic=True).keys() - for nic in nics: - if "pseudo-interface" in nic.replace(' ', '-').lower(): - continue - if nic not in out: - self.fail( - "%r nic wasn't found in 'ipconfig /all' output" % nic) - - def test_total_phymem(self): - w = wmi.WMI().Win32_ComputerSystem()[0] - self.assertEqual(int(w.TotalPhysicalMemory), - psutil.virtual_memory().total) - - # @unittest.skipIf(wmi is None, "wmi module is not installed") - # def test__UPTIME(self): - # # _UPTIME constant is not public but it is used internally - # # as value to return for pid 0 creation time. - # # WMI behaves the same. - # w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] - # p = psutil.Process(0) - # wmic_create = str(w.CreationDate.split('.')[0]) - # psutil_create = time.strftime("%Y%m%d%H%M%S", - # time.localtime(p.create_time())) - - # Note: this test is not very reliable - @unittest.skipIf(APPVEYOR, "test not relieable on appveyor") - @retry_on_failure() - def test_pids(self): - # Note: this test might fail if the OS is starting/killing - # other processes in the meantime - w = wmi.WMI().Win32_Process() - wmi_pids = set([x.ProcessId for x in w]) - psutil_pids = set(psutil.pids()) - self.assertEqual(wmi_pids, psutil_pids) - - @retry_on_failure() - def test_disks(self): - ps_parts = psutil.disk_partitions(all=True) - wmi_parts = wmi.WMI().Win32_LogicalDisk() - for ps_part in ps_parts: - for wmi_part in wmi_parts: - if ps_part.device.replace('\\', '') == wmi_part.DeviceID: - if not ps_part.mountpoint: - # this is usually a CD-ROM with no disk inserted - break - if 'cdrom' in ps_part.opts: - break - try: - usage = psutil.disk_usage(ps_part.mountpoint) - except OSError as err: - if err.errno == errno.ENOENT: - # usually this is the floppy - break - else: - raise - self.assertEqual(usage.total, int(wmi_part.Size)) - wmi_free = int(wmi_part.FreeSpace) - self.assertEqual(usage.free, wmi_free) - # 10 MB tollerance - if abs(usage.free - wmi_free) > 10 * 1024 * 1024: - self.fail("psutil=%s, wmi=%s" % ( - usage.free, wmi_free)) - break - else: - self.fail("can't find partition %s" % repr(ps_part)) - - def test_disk_usage(self): - for disk in psutil.disk_partitions(): - if 'cdrom' in disk.opts: - continue - sys_value = win32api.GetDiskFreeSpaceEx(disk.mountpoint) - psutil_value = psutil.disk_usage(disk.mountpoint) - self.assertAlmostEqual(sys_value[0], psutil_value.free, - delta=1024 * 1024) - self.assertAlmostEqual(sys_value[1], psutil_value.total, - delta=1024 * 1024) - self.assertEqual(psutil_value.used, - psutil_value.total - psutil_value.free) - - def test_disk_partitions(self): - sys_value = [ - x + '\\' for x in win32api.GetLogicalDriveStrings().split("\\\x00") - if x and not x.startswith('A:')] - psutil_value = [x.mountpoint for x in psutil.disk_partitions(all=True)] - self.assertEqual(sys_value, psutil_value) - - def test_net_if_stats(self): - ps_names = set(cext.net_if_stats()) - wmi_adapters = wmi.WMI().Win32_NetworkAdapter() - wmi_names = set() - for wmi_adapter in wmi_adapters: - wmi_names.add(wmi_adapter.Name) - wmi_names.add(wmi_adapter.NetConnectionID) - self.assertTrue(ps_names & wmi_names, - "no common entries in %s, %s" % (ps_names, wmi_names)) - - def test_boot_time(self): - wmi_os = wmi.WMI().Win32_OperatingSystem() - wmi_btime_str = wmi_os[0].LastBootUpTime.split('.')[0] - wmi_btime_dt = datetime.datetime.strptime( - wmi_btime_str, "%Y%m%d%H%M%S") - psutil_dt = datetime.datetime.fromtimestamp(psutil.boot_time()) - diff = abs((wmi_btime_dt - psutil_dt).total_seconds()) - self.assertLessEqual(diff, 1) - - def test_boot_time_fluctuation(self): - # https://github.com/giampaolo/psutil/issues/1007 - with mock.patch('psutil._pswindows.cext.boot_time', return_value=5): - self.assertEqual(psutil.boot_time(), 5) - with mock.patch('psutil._pswindows.cext.boot_time', return_value=4): - self.assertEqual(psutil.boot_time(), 5) - with mock.patch('psutil._pswindows.cext.boot_time', return_value=6): - self.assertEqual(psutil.boot_time(), 5) - with mock.patch('psutil._pswindows.cext.boot_time', return_value=333): - self.assertEqual(psutil.boot_time(), 333) - - -# =================================================================== -# sensors_battery() -# =================================================================== - - -@unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestSensorsBattery(unittest.TestCase): - - def test_has_battery(self): - if win32api.GetPwrCapabilities()['SystemBatteriesPresent']: - self.assertIsNotNone(psutil.sensors_battery()) - else: - self.assertIsNone(psutil.sensors_battery()) - - @unittest.skipIf(not HAS_BATTERY, "no battery") - def test_percent(self): - w = wmi.WMI() - battery_wmi = w.query('select * from Win32_Battery')[0] - battery_psutil = psutil.sensors_battery() - self.assertAlmostEqual( - battery_psutil.percent, battery_wmi.EstimatedChargeRemaining, - delta=1) - - @unittest.skipIf(not HAS_BATTERY, "no battery") - def test_power_plugged(self): - w = wmi.WMI() - battery_wmi = w.query('select * from Win32_Battery')[0] - battery_psutil = psutil.sensors_battery() - # Status codes: - # https://msdn.microsoft.com/en-us/library/aa394074(v=vs.85).aspx - self.assertEqual(battery_psutil.power_plugged, - battery_wmi.BatteryStatus == 2) - - def test_emulate_no_battery(self): - with mock.patch("psutil._pswindows.cext.sensors_battery", - return_value=(0, 128, 0, 0)) as m: - self.assertIsNone(psutil.sensors_battery()) - assert m.called - - def test_emulate_power_connected(self): - with mock.patch("psutil._pswindows.cext.sensors_battery", - return_value=(1, 0, 0, 0)) as m: - self.assertEqual(psutil.sensors_battery().secsleft, - psutil.POWER_TIME_UNLIMITED) - assert m.called - - def test_emulate_power_charging(self): - with mock.patch("psutil._pswindows.cext.sensors_battery", - return_value=(0, 8, 0, 0)) as m: - self.assertEqual(psutil.sensors_battery().secsleft, - psutil.POWER_TIME_UNLIMITED) - assert m.called - - def test_emulate_secs_left_unknown(self): - with mock.patch("psutil._pswindows.cext.sensors_battery", - return_value=(0, 0, 0, -1)) as m: - self.assertEqual(psutil.sensors_battery().secsleft, - psutil.POWER_TIME_UNKNOWN) - assert m.called - - -# =================================================================== -# Process APIs -# =================================================================== - - -@unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestProcess(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.pid = get_test_subprocess().pid - - @classmethod - def tearDownClass(cls): - reap_children() - - def test_issue_24(self): - p = psutil.Process(0) - self.assertRaises(psutil.AccessDenied, p.kill) - - def test_special_pid(self): - p = psutil.Process(4) - self.assertEqual(p.name(), 'System') - # use __str__ to access all common Process properties to check - # that nothing strange happens - str(p) - p.username() - self.assertTrue(p.create_time() >= 0.0) - try: - rss, vms = p.memory_info()[:2] - except psutil.AccessDenied: - # expected on Windows Vista and Windows 7 - if not platform.uname()[1] in ('vista', 'win-7', 'win7'): - raise - else: - self.assertTrue(rss > 0) - - def test_send_signal(self): - p = psutil.Process(self.pid) - self.assertRaises(ValueError, p.send_signal, signal.SIGINT) - - def test_exe_and_name(self): - for p in psutil.process_iter(): - # On Windows name() is never supposed to raise AccessDenied, - # see https://github.com/giampaolo/psutil/issues/627 - try: - name = p.name() - except psutil.NoSuchProcess: - pass - else: - try: - self.assertEqual(os.path.basename(p.exe()), name) - except psutil.Error: - continue - - def test_num_handles_increment(self): - p = psutil.Process(os.getpid()) - before = p.num_handles() - handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, - win32con.FALSE, os.getpid()) - after = p.num_handles() - self.assertEqual(after, before + 1) - win32api.CloseHandle(handle) - self.assertEqual(p.num_handles(), before) - - def test_handles_leak(self): - # Call all Process methods and make sure no handles are left - # open. This is here mainly to make sure functions using - # OpenProcess() always call CloseHandle(). - def call(p, attr): - attr = getattr(p, name, None) - if attr is not None and callable(attr): - attr() - else: - attr - - p = psutil.Process(self.pid) - failures = [] - for name in dir(psutil.Process): - if name.startswith('_') \ - or name in ('terminate', 'kill', 'suspend', 'resume', - 'nice', 'send_signal', 'wait', 'children', - 'as_dict', 'memory_info_ex'): - continue - else: - try: - call(p, name) - num1 = p.num_handles() - call(p, name) - num2 = p.num_handles() - except (psutil.NoSuchProcess, psutil.AccessDenied): - pass - else: - if num2 > num1: - fail = \ - "failure while processing Process.%s method " \ - "(before=%s, after=%s)" % (name, num1, num2) - failures.append(fail) - if failures: - self.fail('\n' + '\n'.join(failures)) - - @unittest.skipIf(not sys.version_info >= (2, 7), - "CTRL_* signals not supported") - def test_ctrl_signals(self): - p = psutil.Process(get_test_subprocess().pid) - p.send_signal(signal.CTRL_C_EVENT) - p.send_signal(signal.CTRL_BREAK_EVENT) - p.kill() - p.wait() - self.assertRaises(psutil.NoSuchProcess, - p.send_signal, signal.CTRL_C_EVENT) - self.assertRaises(psutil.NoSuchProcess, - p.send_signal, signal.CTRL_BREAK_EVENT) - - def test_username(self): - self.assertEqual(psutil.Process().username(), - win32api.GetUserNameEx(win32con.NameSamCompatible)) - - def test_cmdline(self): - sys_value = re.sub(' +', ' ', win32api.GetCommandLine()).strip() - psutil_value = ' '.join(psutil.Process().cmdline()) - self.assertEqual(sys_value, psutil_value) - - # XXX - occasional failures - - # def test_cpu_times(self): - # handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, - # win32con.FALSE, os.getpid()) - # self.addCleanup(win32api.CloseHandle, handle) - # sys_value = win32process.GetProcessTimes(handle) - # psutil_value = psutil.Process().cpu_times() - # self.assertAlmostEqual( - # psutil_value.user, sys_value['UserTime'] / 10000000.0, - # delta=0.2) - # self.assertAlmostEqual( - # psutil_value.user, sys_value['KernelTime'] / 10000000.0, - # delta=0.2) - - def test_nice(self): - handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, - win32con.FALSE, os.getpid()) - self.addCleanup(win32api.CloseHandle, handle) - sys_value = win32process.GetPriorityClass(handle) - psutil_value = psutil.Process().nice() - self.assertEqual(psutil_value, sys_value) - - def test_memory_info(self): - handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, - win32con.FALSE, self.pid) - self.addCleanup(win32api.CloseHandle, handle) - sys_value = win32process.GetProcessMemoryInfo(handle) - psutil_value = psutil.Process(self.pid).memory_info() - self.assertEqual( - sys_value['PeakWorkingSetSize'], psutil_value.peak_wset) - self.assertEqual( - sys_value['WorkingSetSize'], psutil_value.wset) - self.assertEqual( - sys_value['QuotaPeakPagedPoolUsage'], psutil_value.peak_paged_pool) - self.assertEqual( - sys_value['QuotaPagedPoolUsage'], psutil_value.paged_pool) - self.assertEqual( - sys_value['QuotaPeakNonPagedPoolUsage'], - psutil_value.peak_nonpaged_pool) - self.assertEqual( - sys_value['QuotaNonPagedPoolUsage'], psutil_value.nonpaged_pool) - self.assertEqual( - sys_value['PagefileUsage'], psutil_value.pagefile) - self.assertEqual( - sys_value['PeakPagefileUsage'], psutil_value.peak_pagefile) - - self.assertEqual(psutil_value.rss, psutil_value.wset) - self.assertEqual(psutil_value.vms, psutil_value.pagefile) - - def test_wait(self): - handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, - win32con.FALSE, self.pid) - self.addCleanup(win32api.CloseHandle, handle) - p = psutil.Process(self.pid) - p.terminate() - psutil_value = p.wait() - sys_value = win32process.GetExitCodeProcess(handle) - self.assertEqual(psutil_value, sys_value) - - def test_cpu_affinity(self): - def from_bitmask(x): - return [i for i in range(64) if (1 << i) & x] - - handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, - win32con.FALSE, self.pid) - self.addCleanup(win32api.CloseHandle, handle) - sys_value = from_bitmask( - win32process.GetProcessAffinityMask(handle)[0]) - psutil_value = psutil.Process(self.pid).cpu_affinity() - self.assertEqual(psutil_value, sys_value) - - def test_io_counters(self): - handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, - win32con.FALSE, os.getpid()) - self.addCleanup(win32api.CloseHandle, handle) - sys_value = win32process.GetProcessIoCounters(handle) - psutil_value = psutil.Process().io_counters() - self.assertEqual( - psutil_value.read_count, sys_value['ReadOperationCount']) - self.assertEqual( - psutil_value.write_count, sys_value['WriteOperationCount']) - self.assertEqual( - psutil_value.read_bytes, sys_value['ReadTransferCount']) - self.assertEqual( - psutil_value.write_bytes, sys_value['WriteTransferCount']) - self.assertEqual( - psutil_value.other_count, sys_value['OtherOperationCount']) - self.assertEqual( - psutil_value.other_bytes, sys_value['OtherTransferCount']) - - def test_num_handles(self): - import ctypes - import ctypes.wintypes - PROCESS_QUERY_INFORMATION = 0x400 - handle = ctypes.windll.kernel32.OpenProcess( - PROCESS_QUERY_INFORMATION, 0, self.pid) - self.addCleanup(ctypes.windll.kernel32.CloseHandle, handle) - - hndcnt = ctypes.wintypes.DWORD() - ctypes.windll.kernel32.GetProcessHandleCount( - handle, ctypes.byref(hndcnt)) - sys_value = hndcnt.value - psutil_value = psutil.Process(self.pid).num_handles() - self.assertEqual(psutil_value, sys_value) - - -@unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestProcessWMI(unittest.TestCase): - """Compare Process API results with WMI.""" - - @classmethod - def setUpClass(cls): - cls.pid = get_test_subprocess().pid - - @classmethod - def tearDownClass(cls): - reap_children() - - def test_name(self): - w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] - p = psutil.Process(self.pid) - self.assertEqual(p.name(), w.Caption) - - def test_exe(self): - w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] - p = psutil.Process(self.pid) - # Note: wmi reports the exe as a lower case string. - # Being Windows paths case-insensitive we ignore that. - self.assertEqual(p.exe().lower(), w.ExecutablePath.lower()) - - def test_cmdline(self): - w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] - p = psutil.Process(self.pid) - self.assertEqual(' '.join(p.cmdline()), - w.CommandLine.replace('"', '')) - - def test_username(self): - w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] - p = psutil.Process(self.pid) - domain, _, username = w.GetOwner() - username = "%s\\%s" % (domain, username) - self.assertEqual(p.username(), username) - - def test_memory_rss(self): - time.sleep(0.1) - w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] - p = psutil.Process(self.pid) - rss = p.memory_info().rss - self.assertEqual(rss, int(w.WorkingSetSize)) - - def test_memory_vms(self): - time.sleep(0.1) - w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] - p = psutil.Process(self.pid) - vms = p.memory_info().vms - # http://msdn.microsoft.com/en-us/library/aa394372(VS.85).aspx - # ...claims that PageFileUsage is represented in Kilo - # bytes but funnily enough on certain platforms bytes are - # returned instead. - wmi_usage = int(w.PageFileUsage) - if (vms != wmi_usage) and (vms != wmi_usage * 1024): - self.fail("wmi=%s, psutil=%s" % (wmi_usage, vms)) - - def test_create_time(self): - w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] - p = psutil.Process(self.pid) - wmic_create = str(w.CreationDate.split('.')[0]) - psutil_create = time.strftime("%Y%m%d%H%M%S", - time.localtime(p.create_time())) - self.assertEqual(wmic_create, psutil_create) - - -@unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestDualProcessImplementation(unittest.TestCase): - """ - Certain APIs on Windows have 2 internal implementations, one - based on documented Windows APIs, another one based - NtQuerySystemInformation() which gets called as fallback in - case the first fails because of limited permission error. - Here we test that the two methods return the exact same value, - see: - https://github.com/giampaolo/psutil/issues/304 - """ - - @classmethod - def setUpClass(cls): - cls.pid = get_test_subprocess().pid - - @classmethod - def tearDownClass(cls): - reap_children() - # --- - # same tests as above but mimicks the AccessDenied failure of - # the first (fast) method failing with AD. - - def test_name(self): - name = psutil.Process(self.pid).name() - with mock.patch("psutil._psplatform.cext.proc_exe", - side_effect=psutil.AccessDenied(os.getpid())) as fun: - self.assertEqual(psutil.Process(self.pid).name(), name) - assert fun.called - - def test_memory_info(self): - mem_1 = psutil.Process(self.pid).memory_info() - with mock.patch("psutil._psplatform.cext.proc_memory_info", - side_effect=OSError(errno.EPERM, "msg")) as fun: - mem_2 = psutil.Process(self.pid).memory_info() - self.assertEqual(len(mem_1), len(mem_2)) - for i in range(len(mem_1)): - self.assertGreaterEqual(mem_1[i], 0) - self.assertGreaterEqual(mem_2[i], 0) - self.assertAlmostEqual(mem_1[i], mem_2[i], delta=512) - assert fun.called - - def test_create_time(self): - ctime = psutil.Process(self.pid).create_time() - with mock.patch("psutil._psplatform.cext.proc_create_time", - side_effect=OSError(errno.EPERM, "msg")) as fun: - self.assertEqual(psutil.Process(self.pid).create_time(), ctime) - assert fun.called - - def test_cpu_times(self): - cpu_times_1 = psutil.Process(self.pid).cpu_times() - with mock.patch("psutil._psplatform.cext.proc_cpu_times", - side_effect=OSError(errno.EPERM, "msg")) as fun: - cpu_times_2 = psutil.Process(self.pid).cpu_times() - assert fun.called - self.assertAlmostEqual( - cpu_times_1.user, cpu_times_2.user, delta=0.01) - self.assertAlmostEqual( - cpu_times_1.system, cpu_times_2.system, delta=0.01) - - def test_io_counters(self): - io_counters_1 = psutil.Process(self.pid).io_counters() - with mock.patch("psutil._psplatform.cext.proc_io_counters", - side_effect=OSError(errno.EPERM, "msg")) as fun: - io_counters_2 = psutil.Process(self.pid).io_counters() - for i in range(len(io_counters_1)): - self.assertAlmostEqual( - io_counters_1[i], io_counters_2[i], delta=5) - assert fun.called - - def test_num_handles(self): - num_handles = psutil.Process(self.pid).num_handles() - with mock.patch("psutil._psplatform.cext.proc_num_handles", - side_effect=OSError(errno.EPERM, "msg")) as fun: - self.assertEqual(psutil.Process(self.pid).num_handles(), - num_handles) - assert fun.called - - def test_cmdline(self): - from psutil._pswindows import convert_oserror - for pid in psutil.pids(): - try: - a = cext.proc_cmdline(pid, use_peb=True) - b = cext.proc_cmdline(pid, use_peb=False) - except OSError as err: - err = convert_oserror(err) - if not isinstance(err, (psutil.AccessDenied, - psutil.NoSuchProcess)): - raise - else: - self.assertEqual(a, b) - - -@unittest.skipIf(not WINDOWS, "WINDOWS only") -class RemoteProcessTestCase(unittest.TestCase): - """Certain functions require calling ReadProcessMemory. - This trivially works when called on the current process. - Check that this works on other processes, especially when they - have a different bitness. - """ - - @staticmethod - def find_other_interpreter(): - # find a python interpreter that is of the opposite bitness from us - code = "import sys; sys.stdout.write(str(sys.maxsize > 2**32))" - - # XXX: a different and probably more stable approach might be to access - # the registry but accessing 64 bit paths from a 32 bit process - for filename in glob.glob(r"C:\Python*\python.exe"): - proc = subprocess.Popen(args=[filename, "-c", code], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - output, _ = proc.communicate() - if output == str(not IS_64_BIT): - return filename - - @classmethod - def setUpClass(cls): - other_python = cls.find_other_interpreter() - - if other_python is None: - raise unittest.SkipTest( - "could not find interpreter with opposite bitness") - - if IS_64_BIT: - cls.python64 = sys.executable - cls.python32 = other_python - else: - cls.python64 = other_python - cls.python32 = sys.executable - - test_args = ["-c", "import sys; sys.stdin.read()"] - - def setUp(self): - env = os.environ.copy() - env["THINK_OF_A_NUMBER"] = str(os.getpid()) - self.proc32 = get_test_subprocess([self.python32] + self.test_args, - env=env, - stdin=subprocess.PIPE) - self.proc64 = get_test_subprocess([self.python64] + self.test_args, - env=env, - stdin=subprocess.PIPE) - - def tearDown(self): - self.proc32.communicate() - self.proc64.communicate() - reap_children() - - @classmethod - def tearDownClass(cls): - reap_children() - - def test_cmdline_32(self): - p = psutil.Process(self.proc32.pid) - self.assertEqual(len(p.cmdline()), 3) - self.assertEqual(p.cmdline()[1:], self.test_args) - - def test_cmdline_64(self): - p = psutil.Process(self.proc64.pid) - self.assertEqual(len(p.cmdline()), 3) - self.assertEqual(p.cmdline()[1:], self.test_args) - - def test_cwd_32(self): - p = psutil.Process(self.proc32.pid) - self.assertEqual(p.cwd(), os.getcwd()) - - def test_cwd_64(self): - p = psutil.Process(self.proc64.pid) - self.assertEqual(p.cwd(), os.getcwd()) - - def test_environ_32(self): - p = psutil.Process(self.proc32.pid) - e = p.environ() - self.assertIn("THINK_OF_A_NUMBER", e) - self.assertEquals(e["THINK_OF_A_NUMBER"], str(os.getpid())) - - def test_environ_64(self): - p = psutil.Process(self.proc64.pid) - try: - p.environ() - except psutil.AccessDenied: - pass - - -# =================================================================== -# Windows services -# =================================================================== - - -@unittest.skipIf(not WINDOWS, "WINDOWS only") -class TestServices(unittest.TestCase): - - def test_win_service_iter(self): - valid_statuses = set([ - "running", - "paused", - "start", - "pause", - "continue", - "stop", - "stopped", - ]) - valid_start_types = set([ - "automatic", - "manual", - "disabled", - ]) - valid_statuses = set([ - "running", - "paused", - "start_pending", - "pause_pending", - "continue_pending", - "stop_pending", - "stopped" - ]) - for serv in psutil.win_service_iter(): - data = serv.as_dict() - self.assertIsInstance(data['name'], str) - self.assertNotEqual(data['name'].strip(), "") - self.assertIsInstance(data['display_name'], str) - self.assertIsInstance(data['username'], str) - self.assertIn(data['status'], valid_statuses) - if data['pid'] is not None: - psutil.Process(data['pid']) - self.assertIsInstance(data['binpath'], str) - self.assertIsInstance(data['username'], str) - self.assertIsInstance(data['start_type'], str) - self.assertIn(data['start_type'], valid_start_types) - self.assertIn(data['status'], valid_statuses) - self.assertIsInstance(data['description'], str) - pid = serv.pid() - if pid is not None: - p = psutil.Process(pid) - self.assertTrue(p.is_running()) - # win_service_get - s = psutil.win_service_get(serv.name()) - # test __eq__ - self.assertEqual(serv, s) - - def test_win_service_get(self): - ERROR_SERVICE_DOES_NOT_EXIST = \ - psutil._psplatform.cext.ERROR_SERVICE_DOES_NOT_EXIST - ERROR_ACCESS_DENIED = psutil._psplatform.cext.ERROR_ACCESS_DENIED - - name = next(psutil.win_service_iter()).name() - with self.assertRaises(psutil.NoSuchProcess) as cm: - psutil.win_service_get(name + '???') - self.assertEqual(cm.exception.name, name + '???') - - # test NoSuchProcess - service = psutil.win_service_get(name) - if PY3: - args = (0, "msg", 0, ERROR_SERVICE_DOES_NOT_EXIST) - else: - args = (ERROR_SERVICE_DOES_NOT_EXIST, "msg") - exc = WindowsError(*args) - with mock.patch("psutil._psplatform.cext.winservice_query_status", - side_effect=exc): - self.assertRaises(psutil.NoSuchProcess, service.status) - with mock.patch("psutil._psplatform.cext.winservice_query_config", - side_effect=exc): - self.assertRaises(psutil.NoSuchProcess, service.username) - - # test AccessDenied - if PY3: - args = (0, "msg", 0, ERROR_ACCESS_DENIED) - else: - args = (ERROR_ACCESS_DENIED, "msg") - exc = WindowsError(*args) - with mock.patch("psutil._psplatform.cext.winservice_query_status", - side_effect=exc): - self.assertRaises(psutil.AccessDenied, service.status) - with mock.patch("psutil._psplatform.cext.winservice_query_config", - side_effect=exc): - self.assertRaises(psutil.AccessDenied, service.username) - - # test __str__ and __repr__ - self.assertIn(service.name(), str(service)) - self.assertIn(service.display_name(), str(service)) - self.assertIn(service.name(), repr(service)) - self.assertIn(service.display_name(), repr(service)) - - -if __name__ == '__main__': - from psutil.tests.runner import run - run(__file__) +#!/usr/bin/env python3 +# -*- coding: UTF-8 -* + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Windows specific tests.""" + +import datetime +import errno +import glob +import os +import platform +import re +import signal +import subprocess +import sys +import time +import unittest +import warnings + +import psutil +from psutil import WINDOWS +from psutil._compat import FileNotFoundError +from psutil._compat import super +from psutil._compat import which +from psutil.tests import APPVEYOR +from psutil.tests import GITHUB_ACTIONS +from psutil.tests import HAS_BATTERY +from psutil.tests import IS_64BIT +from psutil.tests import PY3 +from psutil.tests import PYPY +from psutil.tests import TOLERANCE_DISK_USAGE +from psutil.tests import TOLERANCE_SYS_MEM +from psutil.tests import PsutilTestCase +from psutil.tests import mock +from psutil.tests import retry_on_failure +from psutil.tests import sh +from psutil.tests import spawn_testproc +from psutil.tests import terminate + + +if WINDOWS and not PYPY: + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + import win32api # requires "pip install pywin32" + import win32con + import win32process + import wmi # requires "pip install wmi" / "make setup-dev-env" + +if WINDOWS: + from psutil._pswindows import convert_oserror + + +cext = psutil._psplatform.cext + + +@unittest.skipIf(not WINDOWS, "WINDOWS only") +@unittest.skipIf(PYPY, "pywin32 not available on PYPY") +# https://github.com/giampaolo/psutil/pull/1762#issuecomment-632892692 +@unittest.skipIf(GITHUB_ACTIONS and not PY3, "pywin32 broken on GITHUB + PY2") +class WindowsTestCase(PsutilTestCase): + pass + + +def powershell(cmd): + """Currently not used, but available just in case. Usage: + + >>> powershell( + "Get-CIMInstance Win32_PageFileUsage | Select AllocatedBaseSize") + """ + if not which("powershell.exe"): + raise unittest.SkipTest("powershell.exe not available") + cmdline = ( + 'powershell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive ' + + '-NoProfile -WindowStyle Hidden -Command "%s"' % cmd + ) + return sh(cmdline) + + +def wmic(path, what, converter=int): + """Currently not used, but available just in case. Usage: + + >>> wmic("Win32_OperatingSystem", "FreePhysicalMemory") + 2134124534 + """ + out = sh("wmic path %s get %s" % (path, what)).strip() + data = "".join(out.splitlines()[1:]).strip() # get rid of the header + if converter is not None: + if "," in what: + return tuple([converter(x) for x in data.split()]) + else: + return converter(data) + else: + return data + + +# =================================================================== +# System APIs +# =================================================================== + + +class TestCpuAPIs(WindowsTestCase): + @unittest.skipIf( + 'NUMBER_OF_PROCESSORS' not in os.environ, + 'NUMBER_OF_PROCESSORS env var is not available', + ) + def test_cpu_count_vs_NUMBER_OF_PROCESSORS(self): + # Will likely fail on many-cores systems: + # https://stackoverflow.com/questions/31209256 + num_cpus = int(os.environ['NUMBER_OF_PROCESSORS']) + self.assertEqual(num_cpus, psutil.cpu_count()) + + def test_cpu_count_vs_GetSystemInfo(self): + # Will likely fail on many-cores systems: + # https://stackoverflow.com/questions/31209256 + sys_value = win32api.GetSystemInfo()[5] + psutil_value = psutil.cpu_count() + self.assertEqual(sys_value, psutil_value) + + def test_cpu_count_logical_vs_wmi(self): + w = wmi.WMI() + procs = sum( + proc.NumberOfLogicalProcessors for proc in w.Win32_Processor() + ) + self.assertEqual(psutil.cpu_count(), procs) + + def test_cpu_count_cores_vs_wmi(self): + w = wmi.WMI() + cores = sum(proc.NumberOfCores for proc in w.Win32_Processor()) + self.assertEqual(psutil.cpu_count(logical=False), cores) + + def test_cpu_count_vs_cpu_times(self): + self.assertEqual( + psutil.cpu_count(), len(psutil.cpu_times(percpu=True)) + ) + + def test_cpu_freq(self): + w = wmi.WMI() + proc = w.Win32_Processor()[0] + self.assertEqual(proc.CurrentClockSpeed, psutil.cpu_freq().current) + self.assertEqual(proc.MaxClockSpeed, psutil.cpu_freq().max) + + +class TestSystemAPIs(WindowsTestCase): + def test_nic_names(self): + out = sh('ipconfig /all') + nics = psutil.net_io_counters(pernic=True).keys() + for nic in nics: + if "pseudo-interface" in nic.replace(' ', '-').lower(): + continue + if nic not in out: + raise self.fail( + "%r nic wasn't found in 'ipconfig /all' output" % nic + ) + + def test_total_phymem(self): + w = wmi.WMI().Win32_ComputerSystem()[0] + self.assertEqual( + int(w.TotalPhysicalMemory), psutil.virtual_memory().total + ) + + def test_free_phymem(self): + w = wmi.WMI().Win32_PerfRawData_PerfOS_Memory()[0] + self.assertAlmostEqual( + int(w.AvailableBytes), + psutil.virtual_memory().free, + delta=TOLERANCE_SYS_MEM, + ) + + def test_total_swapmem(self): + w = wmi.WMI().Win32_PerfRawData_PerfOS_Memory()[0] + self.assertEqual( + int(w.CommitLimit) - psutil.virtual_memory().total, + psutil.swap_memory().total, + ) + if psutil.swap_memory().total == 0: + self.assertEqual(0, psutil.swap_memory().free) + self.assertEqual(0, psutil.swap_memory().used) + + def test_percent_swapmem(self): + if psutil.swap_memory().total > 0: + w = wmi.WMI().Win32_PerfRawData_PerfOS_PagingFile(Name="_Total")[0] + # calculate swap usage to percent + percentSwap = int(w.PercentUsage) * 100 / int(w.PercentUsage_Base) + # exact percent may change but should be reasonable + # assert within +/- 5% and between 0 and 100% + self.assertGreaterEqual(psutil.swap_memory().percent, 0) + self.assertAlmostEqual( + psutil.swap_memory().percent, percentSwap, delta=5 + ) + self.assertLessEqual(psutil.swap_memory().percent, 100) + + # @unittest.skipIf(wmi is None, "wmi module is not installed") + # def test__UPTIME(self): + # # _UPTIME constant is not public but it is used internally + # # as value to return for pid 0 creation time. + # # WMI behaves the same. + # w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] + # p = psutil.Process(0) + # wmic_create = str(w.CreationDate.split('.')[0]) + # psutil_create = time.strftime("%Y%m%d%H%M%S", + # time.localtime(p.create_time())) + + # Note: this test is not very reliable + @unittest.skipIf(APPVEYOR, "test not relieable on appveyor") + @retry_on_failure() + def test_pids(self): + # Note: this test might fail if the OS is starting/killing + # other processes in the meantime + w = wmi.WMI().Win32_Process() + wmi_pids = set([x.ProcessId for x in w]) + psutil_pids = set(psutil.pids()) + self.assertEqual(wmi_pids, psutil_pids) + + @retry_on_failure() + def test_disks(self): + ps_parts = psutil.disk_partitions(all=True) + wmi_parts = wmi.WMI().Win32_LogicalDisk() + for ps_part in ps_parts: + for wmi_part in wmi_parts: + if ps_part.device.replace('\\', '') == wmi_part.DeviceID: + if not ps_part.mountpoint: + # this is usually a CD-ROM with no disk inserted + break + if 'cdrom' in ps_part.opts: + break + if ps_part.mountpoint.startswith('A:'): + break # floppy + try: + usage = psutil.disk_usage(ps_part.mountpoint) + except FileNotFoundError: + # usually this is the floppy + break + self.assertEqual(usage.total, int(wmi_part.Size)) + wmi_free = int(wmi_part.FreeSpace) + self.assertEqual(usage.free, wmi_free) + # 10 MB tolerance + if abs(usage.free - wmi_free) > 10 * 1024 * 1024: + raise self.fail( + "psutil=%s, wmi=%s" % (usage.free, wmi_free) + ) + break + else: + raise self.fail("can't find partition %s" % repr(ps_part)) + + @retry_on_failure() + def test_disk_usage(self): + for disk in psutil.disk_partitions(): + if 'cdrom' in disk.opts: + continue + sys_value = win32api.GetDiskFreeSpaceEx(disk.mountpoint) + psutil_value = psutil.disk_usage(disk.mountpoint) + self.assertAlmostEqual( + sys_value[0], psutil_value.free, delta=TOLERANCE_DISK_USAGE + ) + self.assertAlmostEqual( + sys_value[1], psutil_value.total, delta=TOLERANCE_DISK_USAGE + ) + self.assertEqual( + psutil_value.used, psutil_value.total - psutil_value.free + ) + + def test_disk_partitions(self): + sys_value = [ + x + '\\' + for x in win32api.GetLogicalDriveStrings().split("\\\x00") + if x and not x.startswith('A:') + ] + psutil_value = [ + x.mountpoint + for x in psutil.disk_partitions(all=True) + if not x.mountpoint.startswith('A:') + ] + self.assertEqual(sys_value, psutil_value) + + def test_net_if_stats(self): + ps_names = set(cext.net_if_stats()) + wmi_adapters = wmi.WMI().Win32_NetworkAdapter() + wmi_names = set() + for wmi_adapter in wmi_adapters: + wmi_names.add(wmi_adapter.Name) + wmi_names.add(wmi_adapter.NetConnectionID) + self.assertTrue( + ps_names & wmi_names, + "no common entries in %s, %s" % (ps_names, wmi_names), + ) + + def test_boot_time(self): + wmi_os = wmi.WMI().Win32_OperatingSystem() + wmi_btime_str = wmi_os[0].LastBootUpTime.split('.')[0] + wmi_btime_dt = datetime.datetime.strptime( + wmi_btime_str, "%Y%m%d%H%M%S" + ) + psutil_dt = datetime.datetime.fromtimestamp(psutil.boot_time()) + diff = abs((wmi_btime_dt - psutil_dt).total_seconds()) + self.assertLessEqual(diff, 5) + + def test_boot_time_fluctuation(self): + # https://github.com/giampaolo/psutil/issues/1007 + with mock.patch('psutil._pswindows.cext.boot_time', return_value=5): + self.assertEqual(psutil.boot_time(), 5) + with mock.patch('psutil._pswindows.cext.boot_time', return_value=4): + self.assertEqual(psutil.boot_time(), 5) + with mock.patch('psutil._pswindows.cext.boot_time', return_value=6): + self.assertEqual(psutil.boot_time(), 5) + with mock.patch('psutil._pswindows.cext.boot_time', return_value=333): + self.assertEqual(psutil.boot_time(), 333) + + +# =================================================================== +# sensors_battery() +# =================================================================== + + +class TestSensorsBattery(WindowsTestCase): + def test_has_battery(self): + if win32api.GetPwrCapabilities()['SystemBatteriesPresent']: + self.assertIsNotNone(psutil.sensors_battery()) + else: + self.assertIsNone(psutil.sensors_battery()) + + @unittest.skipIf(not HAS_BATTERY, "no battery") + def test_percent(self): + w = wmi.WMI() + battery_wmi = w.query('select * from Win32_Battery')[0] + battery_psutil = psutil.sensors_battery() + self.assertAlmostEqual( + battery_psutil.percent, + battery_wmi.EstimatedChargeRemaining, + delta=1, + ) + + @unittest.skipIf(not HAS_BATTERY, "no battery") + def test_power_plugged(self): + w = wmi.WMI() + battery_wmi = w.query('select * from Win32_Battery')[0] + battery_psutil = psutil.sensors_battery() + # Status codes: + # https://msdn.microsoft.com/en-us/library/aa394074(v=vs.85).aspx + self.assertEqual( + battery_psutil.power_plugged, battery_wmi.BatteryStatus == 2 + ) + + def test_emulate_no_battery(self): + with mock.patch( + "psutil._pswindows.cext.sensors_battery", + return_value=(0, 128, 0, 0), + ) as m: + self.assertIsNone(psutil.sensors_battery()) + assert m.called + + def test_emulate_power_connected(self): + with mock.patch( + "psutil._pswindows.cext.sensors_battery", return_value=(1, 0, 0, 0) + ) as m: + self.assertEqual( + psutil.sensors_battery().secsleft, psutil.POWER_TIME_UNLIMITED + ) + assert m.called + + def test_emulate_power_charging(self): + with mock.patch( + "psutil._pswindows.cext.sensors_battery", return_value=(0, 8, 0, 0) + ) as m: + self.assertEqual( + psutil.sensors_battery().secsleft, psutil.POWER_TIME_UNLIMITED + ) + assert m.called + + def test_emulate_secs_left_unknown(self): + with mock.patch( + "psutil._pswindows.cext.sensors_battery", + return_value=(0, 0, 0, -1), + ) as m: + self.assertEqual( + psutil.sensors_battery().secsleft, psutil.POWER_TIME_UNKNOWN + ) + assert m.called + + +# =================================================================== +# Process APIs +# =================================================================== + + +class TestProcess(WindowsTestCase): + @classmethod + def setUpClass(cls): + cls.pid = spawn_testproc().pid + + @classmethod + def tearDownClass(cls): + terminate(cls.pid) + + def test_issue_24(self): + p = psutil.Process(0) + self.assertRaises(psutil.AccessDenied, p.kill) + + def test_special_pid(self): + p = psutil.Process(4) + self.assertEqual(p.name(), 'System') + # use __str__ to access all common Process properties to check + # that nothing strange happens + str(p) + p.username() + self.assertGreaterEqual(p.create_time(), 0.0) + try: + rss, vms = p.memory_info()[:2] + except psutil.AccessDenied: + # expected on Windows Vista and Windows 7 + if platform.uname()[1] not in ('vista', 'win-7', 'win7'): + raise + else: + self.assertGreater(rss, 0) + + def test_send_signal(self): + p = psutil.Process(self.pid) + self.assertRaises(ValueError, p.send_signal, signal.SIGINT) + + def test_num_handles_increment(self): + p = psutil.Process(os.getpid()) + before = p.num_handles() + handle = win32api.OpenProcess( + win32con.PROCESS_QUERY_INFORMATION, win32con.FALSE, os.getpid() + ) + after = p.num_handles() + self.assertEqual(after, before + 1) + win32api.CloseHandle(handle) + self.assertEqual(p.num_handles(), before) + + def test_ctrl_signals(self): + p = psutil.Process(self.spawn_testproc().pid) + p.send_signal(signal.CTRL_C_EVENT) + p.send_signal(signal.CTRL_BREAK_EVENT) + p.kill() + p.wait() + self.assertRaises( + psutil.NoSuchProcess, p.send_signal, signal.CTRL_C_EVENT + ) + self.assertRaises( + psutil.NoSuchProcess, p.send_signal, signal.CTRL_BREAK_EVENT + ) + + def test_username(self): + name = win32api.GetUserNameEx(win32con.NameSamCompatible) + if name.endswith('$'): + # When running as a service account (most likely to be + # NetworkService), these user name calculations don't produce the + # same result, causing the test to fail. + raise unittest.SkipTest('running as service account') + self.assertEqual(psutil.Process().username(), name) + + def test_cmdline(self): + sys_value = re.sub('[ ]+', ' ', win32api.GetCommandLine()).strip() + psutil_value = ' '.join(psutil.Process().cmdline()) + if sys_value[0] == '"' != psutil_value[0]: + # The PyWin32 command line may retain quotes around argv[0] if they + # were used unnecessarily, while psutil will omit them. So remove + # the first 2 quotes from sys_value if not in psutil_value. + # A path to an executable will not contain quotes, so this is safe. + sys_value = sys_value.replace('"', '', 2) + self.assertEqual(sys_value, psutil_value) + + # XXX - occasional failures + + # def test_cpu_times(self): + # handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, + # win32con.FALSE, os.getpid()) + # self.addCleanup(win32api.CloseHandle, handle) + # sys_value = win32process.GetProcessTimes(handle) + # psutil_value = psutil.Process().cpu_times() + # self.assertAlmostEqual( + # psutil_value.user, sys_value['UserTime'] / 10000000.0, + # delta=0.2) + # self.assertAlmostEqual( + # psutil_value.user, sys_value['KernelTime'] / 10000000.0, + # delta=0.2) + + def test_nice(self): + handle = win32api.OpenProcess( + win32con.PROCESS_QUERY_INFORMATION, win32con.FALSE, os.getpid() + ) + self.addCleanup(win32api.CloseHandle, handle) + sys_value = win32process.GetPriorityClass(handle) + psutil_value = psutil.Process().nice() + self.assertEqual(psutil_value, sys_value) + + def test_memory_info(self): + handle = win32api.OpenProcess( + win32con.PROCESS_QUERY_INFORMATION, win32con.FALSE, self.pid + ) + self.addCleanup(win32api.CloseHandle, handle) + sys_value = win32process.GetProcessMemoryInfo(handle) + psutil_value = psutil.Process(self.pid).memory_info() + self.assertEqual( + sys_value['PeakWorkingSetSize'], psutil_value.peak_wset + ) + self.assertEqual(sys_value['WorkingSetSize'], psutil_value.wset) + self.assertEqual( + sys_value['QuotaPeakPagedPoolUsage'], psutil_value.peak_paged_pool + ) + self.assertEqual( + sys_value['QuotaPagedPoolUsage'], psutil_value.paged_pool + ) + self.assertEqual( + sys_value['QuotaPeakNonPagedPoolUsage'], + psutil_value.peak_nonpaged_pool, + ) + self.assertEqual( + sys_value['QuotaNonPagedPoolUsage'], psutil_value.nonpaged_pool + ) + self.assertEqual(sys_value['PagefileUsage'], psutil_value.pagefile) + self.assertEqual( + sys_value['PeakPagefileUsage'], psutil_value.peak_pagefile + ) + + self.assertEqual(psutil_value.rss, psutil_value.wset) + self.assertEqual(psutil_value.vms, psutil_value.pagefile) + + def test_wait(self): + handle = win32api.OpenProcess( + win32con.PROCESS_QUERY_INFORMATION, win32con.FALSE, self.pid + ) + self.addCleanup(win32api.CloseHandle, handle) + p = psutil.Process(self.pid) + p.terminate() + psutil_value = p.wait() + sys_value = win32process.GetExitCodeProcess(handle) + self.assertEqual(psutil_value, sys_value) + + def test_cpu_affinity(self): + def from_bitmask(x): + return [i for i in range(64) if (1 << i) & x] + + handle = win32api.OpenProcess( + win32con.PROCESS_QUERY_INFORMATION, win32con.FALSE, self.pid + ) + self.addCleanup(win32api.CloseHandle, handle) + sys_value = from_bitmask( + win32process.GetProcessAffinityMask(handle)[0] + ) + psutil_value = psutil.Process(self.pid).cpu_affinity() + self.assertEqual(psutil_value, sys_value) + + def test_io_counters(self): + handle = win32api.OpenProcess( + win32con.PROCESS_QUERY_INFORMATION, win32con.FALSE, os.getpid() + ) + self.addCleanup(win32api.CloseHandle, handle) + sys_value = win32process.GetProcessIoCounters(handle) + psutil_value = psutil.Process().io_counters() + self.assertEqual( + psutil_value.read_count, sys_value['ReadOperationCount'] + ) + self.assertEqual( + psutil_value.write_count, sys_value['WriteOperationCount'] + ) + self.assertEqual( + psutil_value.read_bytes, sys_value['ReadTransferCount'] + ) + self.assertEqual( + psutil_value.write_bytes, sys_value['WriteTransferCount'] + ) + self.assertEqual( + psutil_value.other_count, sys_value['OtherOperationCount'] + ) + self.assertEqual( + psutil_value.other_bytes, sys_value['OtherTransferCount'] + ) + + def test_num_handles(self): + import ctypes + import ctypes.wintypes + + PROCESS_QUERY_INFORMATION = 0x400 + handle = ctypes.windll.kernel32.OpenProcess( + PROCESS_QUERY_INFORMATION, 0, self.pid + ) + self.addCleanup(ctypes.windll.kernel32.CloseHandle, handle) + + hndcnt = ctypes.wintypes.DWORD() + ctypes.windll.kernel32.GetProcessHandleCount( + handle, ctypes.byref(hndcnt) + ) + sys_value = hndcnt.value + psutil_value = psutil.Process(self.pid).num_handles() + self.assertEqual(psutil_value, sys_value) + + def test_error_partial_copy(self): + # https://github.com/giampaolo/psutil/issues/875 + exc = WindowsError() + exc.winerror = 299 + with mock.patch("psutil._psplatform.cext.proc_cwd", side_effect=exc): + with mock.patch("time.sleep") as m: + p = psutil.Process() + self.assertRaises(psutil.AccessDenied, p.cwd) + self.assertGreaterEqual(m.call_count, 5) + + def test_exe(self): + # NtQuerySystemInformation succeeds if process is gone. Make sure + # it raises NSP for a non existent pid. + pid = psutil.pids()[-1] + 99999 + proc = psutil._psplatform.Process(pid) + self.assertRaises(psutil.NoSuchProcess, proc.exe) + + +class TestProcessWMI(WindowsTestCase): + """Compare Process API results with WMI.""" + + @classmethod + def setUpClass(cls): + cls.pid = spawn_testproc().pid + + @classmethod + def tearDownClass(cls): + terminate(cls.pid) + + def test_name(self): + w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] + p = psutil.Process(self.pid) + self.assertEqual(p.name(), w.Caption) + + # This fail on github because using virtualenv for test environment + @unittest.skipIf(GITHUB_ACTIONS, "unreliable path on GITHUB_ACTIONS") + def test_exe(self): + w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] + p = psutil.Process(self.pid) + # Note: wmi reports the exe as a lower case string. + # Being Windows paths case-insensitive we ignore that. + self.assertEqual(p.exe().lower(), w.ExecutablePath.lower()) + + def test_cmdline(self): + w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] + p = psutil.Process(self.pid) + self.assertEqual(' '.join(p.cmdline()), w.CommandLine.replace('"', '')) + + def test_username(self): + w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] + p = psutil.Process(self.pid) + domain, _, username = w.GetOwner() + username = "%s\\%s" % (domain, username) + self.assertEqual(p.username(), username) + + @retry_on_failure() + def test_memory_rss(self): + w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] + p = psutil.Process(self.pid) + rss = p.memory_info().rss + self.assertEqual(rss, int(w.WorkingSetSize)) + + @retry_on_failure() + def test_memory_vms(self): + w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] + p = psutil.Process(self.pid) + vms = p.memory_info().vms + # http://msdn.microsoft.com/en-us/library/aa394372(VS.85).aspx + # ...claims that PageFileUsage is represented in Kilo + # bytes but funnily enough on certain platforms bytes are + # returned instead. + wmi_usage = int(w.PageFileUsage) + if vms not in (wmi_usage, wmi_usage * 1024): + raise self.fail("wmi=%s, psutil=%s" % (wmi_usage, vms)) + + def test_create_time(self): + w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] + p = psutil.Process(self.pid) + wmic_create = str(w.CreationDate.split('.')[0]) + psutil_create = time.strftime( + "%Y%m%d%H%M%S", time.localtime(p.create_time()) + ) + self.assertEqual(wmic_create, psutil_create) + + +# --- + + +@unittest.skipIf(not WINDOWS, "WINDOWS only") +class TestDualProcessImplementation(PsutilTestCase): + """Certain APIs on Windows have 2 internal implementations, one + based on documented Windows APIs, another one based + NtQuerySystemInformation() which gets called as fallback in + case the first fails because of limited permission error. + Here we test that the two methods return the exact same value, + see: + https://github.com/giampaolo/psutil/issues/304. + """ + + @classmethod + def setUpClass(cls): + cls.pid = spawn_testproc().pid + + @classmethod + def tearDownClass(cls): + terminate(cls.pid) + + def test_memory_info(self): + mem_1 = psutil.Process(self.pid).memory_info() + with mock.patch( + "psutil._psplatform.cext.proc_memory_info", + side_effect=OSError(errno.EPERM, "msg"), + ) as fun: + mem_2 = psutil.Process(self.pid).memory_info() + self.assertEqual(len(mem_1), len(mem_2)) + for i in range(len(mem_1)): + self.assertGreaterEqual(mem_1[i], 0) + self.assertGreaterEqual(mem_2[i], 0) + self.assertAlmostEqual(mem_1[i], mem_2[i], delta=512) + assert fun.called + + def test_create_time(self): + ctime = psutil.Process(self.pid).create_time() + with mock.patch( + "psutil._psplatform.cext.proc_times", + side_effect=OSError(errno.EPERM, "msg"), + ) as fun: + self.assertEqual(psutil.Process(self.pid).create_time(), ctime) + assert fun.called + + def test_cpu_times(self): + cpu_times_1 = psutil.Process(self.pid).cpu_times() + with mock.patch( + "psutil._psplatform.cext.proc_times", + side_effect=OSError(errno.EPERM, "msg"), + ) as fun: + cpu_times_2 = psutil.Process(self.pid).cpu_times() + assert fun.called + self.assertAlmostEqual( + cpu_times_1.user, cpu_times_2.user, delta=0.01 + ) + self.assertAlmostEqual( + cpu_times_1.system, cpu_times_2.system, delta=0.01 + ) + + def test_io_counters(self): + io_counters_1 = psutil.Process(self.pid).io_counters() + with mock.patch( + "psutil._psplatform.cext.proc_io_counters", + side_effect=OSError(errno.EPERM, "msg"), + ) as fun: + io_counters_2 = psutil.Process(self.pid).io_counters() + for i in range(len(io_counters_1)): + self.assertAlmostEqual( + io_counters_1[i], io_counters_2[i], delta=5 + ) + assert fun.called + + def test_num_handles(self): + num_handles = psutil.Process(self.pid).num_handles() + with mock.patch( + "psutil._psplatform.cext.proc_num_handles", + side_effect=OSError(errno.EPERM, "msg"), + ) as fun: + self.assertEqual( + psutil.Process(self.pid).num_handles(), num_handles + ) + assert fun.called + + def test_cmdline(self): + for pid in psutil.pids(): + try: + a = cext.proc_cmdline(pid, use_peb=True) + b = cext.proc_cmdline(pid, use_peb=False) + except OSError as err: + err = convert_oserror(err) + if not isinstance( + err, (psutil.AccessDenied, psutil.NoSuchProcess) + ): + raise + else: + self.assertEqual(a, b) + + +@unittest.skipIf(not WINDOWS, "WINDOWS only") +class RemoteProcessTestCase(PsutilTestCase): + """Certain functions require calling ReadProcessMemory. + This trivially works when called on the current process. + Check that this works on other processes, especially when they + have a different bitness. + """ + + @staticmethod + def find_other_interpreter(): + # find a python interpreter that is of the opposite bitness from us + code = "import sys; sys.stdout.write(str(sys.maxsize > 2**32))" + + # XXX: a different and probably more stable approach might be to access + # the registry but accessing 64 bit paths from a 32 bit process + for filename in glob.glob(r"C:\Python*\python.exe"): + proc = subprocess.Popen( + args=[filename, "-c", code], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + output, _ = proc.communicate() + proc.wait() + if output == str(not IS_64BIT): + return filename + + test_args = ["-c", "import sys; sys.stdin.read()"] + + def setUp(self): + super().setUp() + + other_python = self.find_other_interpreter() + if other_python is None: + raise unittest.SkipTest( + "could not find interpreter with opposite bitness" + ) + if IS_64BIT: + self.python64 = sys.executable + self.python32 = other_python + else: + self.python64 = other_python + self.python32 = sys.executable + + env = os.environ.copy() + env["THINK_OF_A_NUMBER"] = str(os.getpid()) + self.proc32 = self.spawn_testproc( + [self.python32] + self.test_args, env=env, stdin=subprocess.PIPE + ) + self.proc64 = self.spawn_testproc( + [self.python64] + self.test_args, env=env, stdin=subprocess.PIPE + ) + + def tearDown(self): + super().tearDown() + self.proc32.communicate() + self.proc64.communicate() + + def test_cmdline_32(self): + p = psutil.Process(self.proc32.pid) + self.assertEqual(len(p.cmdline()), 3) + self.assertEqual(p.cmdline()[1:], self.test_args) + + def test_cmdline_64(self): + p = psutil.Process(self.proc64.pid) + self.assertEqual(len(p.cmdline()), 3) + self.assertEqual(p.cmdline()[1:], self.test_args) + + def test_cwd_32(self): + p = psutil.Process(self.proc32.pid) + self.assertEqual(p.cwd(), os.getcwd()) + + def test_cwd_64(self): + p = psutil.Process(self.proc64.pid) + self.assertEqual(p.cwd(), os.getcwd()) + + def test_environ_32(self): + p = psutil.Process(self.proc32.pid) + e = p.environ() + self.assertIn("THINK_OF_A_NUMBER", e) + self.assertEqual(e["THINK_OF_A_NUMBER"], str(os.getpid())) + + def test_environ_64(self): + p = psutil.Process(self.proc64.pid) + try: + p.environ() + except psutil.AccessDenied: + pass + + +# =================================================================== +# Windows services +# =================================================================== + + +@unittest.skipIf(not WINDOWS, "WINDOWS only") +class TestServices(PsutilTestCase): + def test_win_service_iter(self): + valid_statuses = set([ + "running", + "paused", + "start", + "pause", + "continue", + "stop", + "stopped", + ]) + valid_start_types = set(["automatic", "manual", "disabled"]) + valid_statuses = set([ + "running", + "paused", + "start_pending", + "pause_pending", + "continue_pending", + "stop_pending", + "stopped", + ]) + for serv in psutil.win_service_iter(): + data = serv.as_dict() + self.assertIsInstance(data['name'], str) + self.assertNotEqual(data['name'].strip(), "") + self.assertIsInstance(data['display_name'], str) + self.assertIsInstance(data['username'], str) + self.assertIn(data['status'], valid_statuses) + if data['pid'] is not None: + psutil.Process(data['pid']) + self.assertIsInstance(data['binpath'], str) + self.assertIsInstance(data['username'], str) + self.assertIsInstance(data['start_type'], str) + self.assertIn(data['start_type'], valid_start_types) + self.assertIn(data['status'], valid_statuses) + self.assertIsInstance(data['description'], str) + pid = serv.pid() + if pid is not None: + p = psutil.Process(pid) + self.assertTrue(p.is_running()) + # win_service_get + s = psutil.win_service_get(serv.name()) + # test __eq__ + self.assertEqual(serv, s) + + def test_win_service_get(self): + ERROR_SERVICE_DOES_NOT_EXIST = ( + psutil._psplatform.cext.ERROR_SERVICE_DOES_NOT_EXIST + ) + ERROR_ACCESS_DENIED = psutil._psplatform.cext.ERROR_ACCESS_DENIED + + name = next(psutil.win_service_iter()).name() + with self.assertRaises(psutil.NoSuchProcess) as cm: + psutil.win_service_get(name + '???') + self.assertEqual(cm.exception.name, name + '???') + + # test NoSuchProcess + service = psutil.win_service_get(name) + if PY3: + args = (0, "msg", 0, ERROR_SERVICE_DOES_NOT_EXIST) + else: + args = (ERROR_SERVICE_DOES_NOT_EXIST, "msg") + exc = WindowsError(*args) + with mock.patch( + "psutil._psplatform.cext.winservice_query_status", side_effect=exc + ): + self.assertRaises(psutil.NoSuchProcess, service.status) + with mock.patch( + "psutil._psplatform.cext.winservice_query_config", side_effect=exc + ): + self.assertRaises(psutil.NoSuchProcess, service.username) + + # test AccessDenied + if PY3: + args = (0, "msg", 0, ERROR_ACCESS_DENIED) + else: + args = (ERROR_ACCESS_DENIED, "msg") + exc = WindowsError(*args) + with mock.patch( + "psutil._psplatform.cext.winservice_query_status", side_effect=exc + ): + self.assertRaises(psutil.AccessDenied, service.status) + with mock.patch( + "psutil._psplatform.cext.winservice_query_config", side_effect=exc + ): + self.assertRaises(psutil.AccessDenied, service.username) + + # test __str__ and __repr__ + self.assertIn(service.name(), str(service)) + self.assertIn(service.display_name(), str(service)) + self.assertIn(service.name(), repr(service)) + self.assertIn(service.display_name(), repr(service)) + + +if __name__ == '__main__': + from psutil.tests.runner import run_from_name + + run_from_name(__file__) diff --git a/addon/globalPlugins/soundmanager/psutil/tests/test_windows.pyc b/addon/globalPlugins/soundmanager/psutil/tests/test_windows.pyc deleted file mode 100644 index 4c83308..0000000 Binary files a/addon/globalPlugins/soundmanager/psutil/tests/test_windows.pyc and /dev/null differ diff --git a/addon/globalPlugins/soundmanager/pycaw/__pycache__/__init__.cpython-311.pyc b/addon/globalPlugins/soundmanager/pycaw/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..f6e8a64 Binary files /dev/null and b/addon/globalPlugins/soundmanager/pycaw/__pycache__/__init__.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/pycaw/__pycache__/callbacks.cpython-311.pyc b/addon/globalPlugins/soundmanager/pycaw/__pycache__/callbacks.cpython-311.pyc new file mode 100644 index 0000000..d011d0a Binary files /dev/null and b/addon/globalPlugins/soundmanager/pycaw/__pycache__/callbacks.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/pycaw/__pycache__/constants.cpython-311.pyc b/addon/globalPlugins/soundmanager/pycaw/__pycache__/constants.cpython-311.pyc new file mode 100644 index 0000000..11f9c16 Binary files /dev/null and b/addon/globalPlugins/soundmanager/pycaw/__pycache__/constants.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/pycaw/__pycache__/magic.cpython-311.pyc b/addon/globalPlugins/soundmanager/pycaw/__pycache__/magic.cpython-311.pyc new file mode 100644 index 0000000..b5f37ba Binary files /dev/null and b/addon/globalPlugins/soundmanager/pycaw/__pycache__/magic.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/pycaw/__pycache__/pycaw.cpython-311.pyc b/addon/globalPlugins/soundmanager/pycaw/__pycache__/pycaw.cpython-311.pyc new file mode 100644 index 0000000..406cb61 Binary files /dev/null and b/addon/globalPlugins/soundmanager/pycaw/__pycache__/pycaw.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/pycaw/__pycache__/utils.cpython-311.pyc b/addon/globalPlugins/soundmanager/pycaw/__pycache__/utils.cpython-311.pyc new file mode 100644 index 0000000..c0e9e2c Binary files /dev/null and b/addon/globalPlugins/soundmanager/pycaw/__pycache__/utils.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/pycaw/api/__init__.py b/addon/globalPlugins/soundmanager/pycaw/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/addon/globalPlugins/soundmanager/pycaw/api/__pycache__/__init__.cpython-311.pyc b/addon/globalPlugins/soundmanager/pycaw/api/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..8b1fc8c Binary files /dev/null and b/addon/globalPlugins/soundmanager/pycaw/api/__pycache__/__init__.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/pycaw/api/audioclient/__init__.py b/addon/globalPlugins/soundmanager/pycaw/api/audioclient/__init__.py new file mode 100644 index 0000000..0a1005d --- /dev/null +++ b/addon/globalPlugins/soundmanager/pycaw/api/audioclient/__init__.py @@ -0,0 +1,163 @@ +from ctypes import HRESULT, POINTER, c_float +from ctypes import c_longlong as REFERENCE_TIME +from ctypes import c_uint32 as UINT32 +from ctypes.wintypes import BOOL, DWORD, HANDLE + +from comtypes import COMMETHOD, GUID, IUnknown + +from .depend import WAVEFORMATEX + + +class ISimpleAudioVolume(IUnknown): + _iid_ = GUID("{87CE5498-68D6-44E5-9215-6DA47EF883D8}") + _methods_ = ( + # HRESULT SetMasterVolume( + # [in] float fLevel, + # [in] LPCGUID EventContext); + COMMETHOD( + [], + HRESULT, + "SetMasterVolume", + (["in"], c_float, "fLevel"), + (["in"], POINTER(GUID), "EventContext"), + ), + # HRESULT GetMasterVolume([out] float *pfLevel); + COMMETHOD( + [], HRESULT, "GetMasterVolume", (["out"], POINTER(c_float), "pfLevel") + ), + # HRESULT SetMute( + # [in] BOOL bMute, + # [in] LPCGUID EventContext); + COMMETHOD( + [], + HRESULT, + "SetMute", + (["in"], BOOL, "bMute"), + (["in"], POINTER(GUID), "EventContext"), + ), + # HRESULT GetMute([out] BOOL *pbMute); + COMMETHOD([], HRESULT, "GetMute", (["out"], POINTER(BOOL), "pbMute")), + ) + + +class IAudioClient(IUnknown): + _iid_ = GUID("{1cb9ad4c-dbfa-4c32-b178-c2f568a703b2}") + _methods_ = ( + # HRESULT Initialize( + # [in] AUDCLNT_SHAREMODE ShareMode, + # [in] DWORD StreamFlags, + # [in] REFERENCE_TIME hnsBufferDuration, + # [in] REFERENCE_TIME hnsPeriodicity, + # [in] const WAVEFORMATEX *pFormat, + # [in] LPCGUID AudioSessionGuid); + COMMETHOD( + [], + HRESULT, + "Initialize", + (["in"], DWORD, "ShareMode"), + (["in"], DWORD, "StreamFlags"), + (["in"], REFERENCE_TIME, "hnsBufferDuration"), + (["in"], REFERENCE_TIME, "hnsPeriodicity"), + (["in"], POINTER(WAVEFORMATEX), "pFormat"), + (["in"], POINTER(GUID), "AudioSessionGuid"), + ), + # HRESULT GetBufferSize( + # [out] UINT32 *pNumBufferFrames); + COMMETHOD( + [], HRESULT, "GetBufferSize", (["out"], POINTER(UINT32), "pNumBufferFrames") + ), + # HRESULT GetStreamLatency( + # [out] REFERENCE_TIME *phnsLatency); + COMMETHOD( + [], + HRESULT, + "GetStreamLatency", + (["out"], POINTER(REFERENCE_TIME), "phnsLatency"), + ), + # HRESULT GetCurrentPadding( + # [out] UINT32 *pNumPaddingFrames); + COMMETHOD( + [], + HRESULT, + "GetCurrentPadding", + (["out"], POINTER(UINT32), "pNumPaddingFrames"), + ), + # HRESULT IsFormatSupported( + # [in] AUDCLNT_SHAREMODE ShareMode, + # [in] const WAVEFORMATEX *pFormat, + # [out,unique] WAVEFORMATEX **ppClosestMatch); + COMMETHOD( + [], + HRESULT, + "IsFormatSupported", + (["in"], DWORD, "ShareMode"), + (["in"], POINTER(WAVEFORMATEX), "pFormat"), + (["out"], POINTER(POINTER(WAVEFORMATEX)), "ppClosestMatch"), + ), + # HRESULT GetMixFormat( + # [out] WAVEFORMATEX **ppDeviceFormat + # ); + COMMETHOD( + [], + HRESULT, + "GetMixFormat", + (["out"], POINTER(POINTER(WAVEFORMATEX)), "ppDeviceFormat"), + ), + # HRESULT GetDevicePeriod( + # [out] REFERENCE_TIME *phnsDefaultDevicePeriod, + # [out] REFERENCE_TIME *phnsMinimumDevicePeriod); + COMMETHOD( + [], + HRESULT, + "GetDevicePeriod", + (["out"], POINTER(REFERENCE_TIME), "phnsDefaultDevicePeriod"), + (["out"], POINTER(REFERENCE_TIME), "phnsMinimumDevicePeriod"), + ), + # HRESULT Start(void); + COMMETHOD([], HRESULT, "Start"), + # HRESULT Stop(void); + COMMETHOD([], HRESULT, "Stop"), + # HRESULT Reset(void); + COMMETHOD([], HRESULT, "Reset"), + # HRESULT SetEventHandle([in] HANDLE eventHandle); + COMMETHOD( + [], + HRESULT, + "SetEventHandle", + (["in"], HANDLE, "eventHandle"), + ), + # HRESULT GetService( + # [in] REFIID riid, + # [out] void **ppv); + COMMETHOD( + [], + HRESULT, + "GetService", + (["in"], POINTER(GUID), "iid"), + (["out"], POINTER(POINTER(IUnknown)), "ppv"), + ), + ) + + +class IChannelAudioVolume(IUnknown): + _iid_ = GUID("{1c158861-b533-4b30-b1cf-e853e51c59b8}") + _methods_ = ( + COMMETHOD( + [], HRESULT, "GetChannelCount", (["out"], POINTER(UINT32), "pnChannelCount") + ), + COMMETHOD( + [], + HRESULT, + "SetChannelVolume", + (["in"], UINT32, "dwIndex"), + (["in"], c_float, "fLevel"), + (["in"], POINTER(GUID), "EventContext"), + ), + COMMETHOD( + [], + HRESULT, + "GetChannelVolume", + (["in"], UINT32, "dwIndex"), + (["out"], POINTER(c_float), "pfLevel"), + ), + ) diff --git a/addon/globalPlugins/soundmanager/pycaw/api/audioclient/__pycache__/__init__.cpython-311.pyc b/addon/globalPlugins/soundmanager/pycaw/api/audioclient/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..ab6ba20 Binary files /dev/null and b/addon/globalPlugins/soundmanager/pycaw/api/audioclient/__pycache__/__init__.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/pycaw/api/audioclient/__pycache__/depend.cpython-311.pyc b/addon/globalPlugins/soundmanager/pycaw/api/audioclient/__pycache__/depend.cpython-311.pyc new file mode 100644 index 0000000..bd93ed3 Binary files /dev/null and b/addon/globalPlugins/soundmanager/pycaw/api/audioclient/__pycache__/depend.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/pycaw/api/audioclient/depend.py b/addon/globalPlugins/soundmanager/pycaw/api/audioclient/depend.py new file mode 100644 index 0000000..155df6b --- /dev/null +++ b/addon/globalPlugins/soundmanager/pycaw/api/audioclient/depend.py @@ -0,0 +1,14 @@ +from ctypes import Structure +from ctypes.wintypes import WORD + + +class WAVEFORMATEX(Structure): + _fields_ = [ + ("wFormatTag", WORD), + ("nChannels", WORD), + ("nSamplesPerSec", WORD), + ("nAvgBytesPerSec", WORD), + ("nBlockAlign", WORD), + ("wBitsPerSample", WORD), + ("cbSize", WORD), + ] diff --git a/addon/globalPlugins/soundmanager/pycaw/api/audiopolicy/__init__.py b/addon/globalPlugins/soundmanager/pycaw/api/audiopolicy/__init__.py new file mode 100644 index 0000000..1ac7a01 --- /dev/null +++ b/addon/globalPlugins/soundmanager/pycaw/api/audiopolicy/__init__.py @@ -0,0 +1,294 @@ +from ctypes import HRESULT, POINTER, c_float, c_uint32 +from ctypes.wintypes import BOOL, DWORD, INT, LPCWSTR, LPWSTR + +from comtypes import COMMETHOD, GUID, IUnknown + +from ..audioclient import ISimpleAudioVolume + + +class IAudioSessionEvents(IUnknown): + _iid_ = GUID("{073d618c-490a-4f9f-9d18-7bec6fc21121}") + _methods_ = ( + # HRESULT OnDisplayNameChanged( + # [in] LPCWSTR NewDisplayName, + # [in] LPCGUID EventContext); + COMMETHOD( + [], + HRESULT, + "OnDisplayNameChanged", + (["in"], LPCWSTR, "NewDisplayName"), + (["in"], POINTER(GUID), "EventContext"), + ), + # HRESULT OnIconPathChanged( + # [in] LPCWSTR NewIconPath, + # [in] LPCGUID EventContext); + COMMETHOD( + [], + HRESULT, + "OnIconPathChanged", + (["in"], LPCWSTR, "NewIconPath"), + (["in"], POINTER(GUID), "EventContext"), + ), + # HRESULT OnSimpleVolumeChanged( + # [in] float NewVolume, + # [in] BOOL NewMute, + # [in] LPCGUID EventContext); + COMMETHOD( + [], + HRESULT, + "OnSimpleVolumeChanged", + (["in"], c_float, "NewVolume"), + (["in"], BOOL, "NewMute"), + (["in"], POINTER(GUID), "EventContext"), + ), + # HRESULT OnChannelVolumeChanged( + # [in] DWORD ChannelCount, + # [in] float [] NewChannelVolumeArray, + # [in] DWORD ChangedChannel, + # [in] LPCGUID EventContext); + COMMETHOD( + [], + HRESULT, + "OnChannelVolumeChanged", + (["in"], DWORD, "ChannelCount"), + (["in"], (c_float * 8), "NewChannelVolumeArray"), + (["in"], DWORD, "ChangedChannel"), + (["in"], POINTER(GUID), "EventContext"), + ), + # HRESULT OnGroupingParamChanged( + # [in] LPCGUID NewGroupingParam, + # [in] LPCGUID EventContext); + COMMETHOD( + [], + HRESULT, + "OnGroupingParamChanged", + (["in"], POINTER(GUID), "NewGroupingParam"), + (["in"], POINTER(GUID), "EventContext"), + ), + # HRESULT OnStateChanged( + # AudioSessionState NewState); + COMMETHOD([], HRESULT, "OnStateChanged", (["in"], DWORD, "NewState")), + # HRESULT OnSessionDisconnected( + # [in] AudioSessionDisconnectReason DisconnectReason); + COMMETHOD( + [], HRESULT, "OnSessionDisconnected", (["in"], DWORD, "DisconnectReason") + ), + ) + + +class IAudioSessionControl(IUnknown): + _iid_ = GUID("{F4B1A599-7266-4319-A8CA-E70ACB11E8CD}") + _methods_ = ( + # HRESULT GetState ([out] AudioSessionState *pRetVal); + COMMETHOD([], HRESULT, "GetState", (["out"], POINTER(DWORD), "pRetVal")), + # HRESULT GetDisplayName([out] LPWSTR *pRetVal); + COMMETHOD([], HRESULT, "GetDisplayName", (["out"], POINTER(LPWSTR), "pRetVal")), + # HRESULT SetDisplayName( + # [in] LPCWSTR Value, + # [in] LPCGUID EventContext); + COMMETHOD( + [], + HRESULT, + "SetDisplayName", + (["in"], LPCWSTR, "Value"), + (["in"], POINTER(GUID), "EventContext"), + ), + # HRESULT GetIconPath([out] LPWSTR *pRetVal); + COMMETHOD([], HRESULT, "GetIconPath", (["out"], POINTER(LPWSTR), "pRetVal")), + # HRESULT SetIconPath( + # [in] LPCWSTR Value, + # [in] LPCGUID EventContext); + COMMETHOD( + [], + HRESULT, + "SetIconPath", + (["in"], LPCWSTR, "Value"), + (["in"], POINTER(GUID), "EventContext"), + ), + # HRESULT GetGroupingParam([out] GUID *pRetVal); + COMMETHOD([], HRESULT, "GetGroupingParam", (["out"], POINTER(GUID), "pRetVal")), + # HRESULT SetGroupingParam( + # [in] LPCGUID Grouping, + # [in] LPCGUID EventContext); + COMMETHOD( + [], + HRESULT, + "SetGroupingParam", + (["in"], POINTER(GUID), "Grouping"), + (["in"], POINTER(GUID), "EventContext"), + ), + # HRESULT RegisterAudioSessionNotification( + # [in] IAudioSessionEvents *NewNotifications); + COMMETHOD( + [], + HRESULT, + "RegisterAudioSessionNotification", + (["in"], POINTER(IAudioSessionEvents), "NewNotifications"), + ), + # HRESULT UnregisterAudioSessionNotification( + # [in] IAudioSessionEvents *NewNotifications); + COMMETHOD( + [], + HRESULT, + "UnregisterAudioSessionNotification", + (["in"], POINTER(IAudioSessionEvents), "NewNotifications"), + ), + ) + + +class IAudioSessionControl2(IAudioSessionControl): + _iid_ = GUID("{BFB7FF88-7239-4FC9-8FA2-07C950BE9C6D}") + _methods_ = ( + # HRESULT GetSessionIdentifier([out] LPWSTR *pRetVal); + COMMETHOD( + [], HRESULT, "GetSessionIdentifier", (["out"], POINTER(LPWSTR), "pRetVal") + ), + # HRESULT GetSessionInstanceIdentifier([out] LPWSTR *pRetVal); + COMMETHOD( + [], + HRESULT, + "GetSessionInstanceIdentifier", + (["out"], POINTER(LPWSTR), "pRetVal"), + ), + # HRESULT GetProcessId([out] DWORD *pRetVal); + COMMETHOD([], HRESULT, "GetProcessId", (["out"], POINTER(DWORD), "pRetVal")), + # HRESULT IsSystemSoundsSession(); + COMMETHOD([], HRESULT, "IsSystemSoundsSession"), + # HRESULT SetDuckingPreference([in] BOOL optOut); + COMMETHOD([], HRESULT, "SetDuckingPreferences", (["in"], BOOL, "optOut")), + ) + + +class IAudioSessionEnumerator(IUnknown): + _iid_ = GUID("{E2F5BB11-0570-40CA-ACDD-3AA01277DEE8}") + _methods_ = ( + # HRESULT GetCount([out] int *SessionCount); + COMMETHOD([], HRESULT, "GetCount", (["out"], POINTER(INT), "SessionCount")), + # HRESULT GetSession( + # [in] int SessionCount, + # [out] IAudioSessionControl **Session); + COMMETHOD( + [], + HRESULT, + "GetSession", + (["in"], INT, "SessionCount"), + (["out"], POINTER(POINTER(IAudioSessionControl)), "Session"), + ), + ) + + +class IAudioSessionManager(IUnknown): + _iid_ = GUID("{BFA971F1-4d5e-40bb-935e-967039bfbee4}") + _methods_ = ( + # HRESULT GetAudioSessionControl( + # [in] LPCGUID AudioSessionGuid, + # [in] DWORD StreamFlags, + # [out] IAudioSessionControl **SessionControl); + COMMETHOD( + [], + HRESULT, + "GetAudioSessionControl", + (["in"], POINTER(GUID), "AudioSessionGuid"), + (["in"], DWORD, "StreamFlags"), + (["out"], POINTER(POINTER(IAudioSessionControl)), "SessionControl"), + ), + # HRESULT GetSimpleAudioVolume( + # [in] LPCGUID AudioSessionGuid, + # [in] DWORD CrossProcessSession, + # [out] ISimpleAudioVolume **AudioVolume); + COMMETHOD( + [], + HRESULT, + "GetSimpleAudioVolume", + (["in"], POINTER(GUID), "AudioSessionGuid"), + (["in"], DWORD, "CrossProcessSession"), + (["out"], POINTER(POINTER(ISimpleAudioVolume)), "AudioVolume"), + ), + ) + + +class IAudioSessionNotification(IUnknown): + _iid_ = GUID("{8aad9bb7-39e1-4c62-a3ab-ff6e76dcf9c8}") + _methods_ = ( + # HRESULT OnSessionCreated( + # ['in'] IAudioSessionControl *NewSession + # ); + COMMETHOD( + [], + HRESULT, + "OnSessionCreated", + (["in"], POINTER(IAudioSessionControl), "NewSession"), + ), + ) + + +class IAudioVolumeDuckNotification(IUnknown): + _iid_ = GUID("{C3B284D4-6D39-4359-B3CF-B56DDB3BB39C}") + _methods_ = ( + # HRESULT OnVolumeDuckNotification( + # [in] LPCWSTR sessionID, + # [in] UINT32 countCommunicationSessions); + COMMETHOD( + [], + HRESULT, + "OnVolumeDuckNotification", + (["in"], LPCWSTR, "sessionID"), + (["in"], c_uint32, "countCommunicationSessions"), + ), + # HRESULT OnVolumeUnduckNotification( + # [in] LPCWSTR sessionID); + COMMETHOD( + [], + HRESULT, + "OnVolumeUnduckNotification", + (["in"], LPCWSTR, "sessionID"), + ), + ) + + +class IAudioSessionManager2(IAudioSessionManager): + _iid_ = GUID("{77aa99a0-1bd6-484f-8bc7-2c654c9a9b6f}") + _methods_ = ( + # HRESULT GetSessionEnumerator( + # [out] IAudioSessionEnumerator **SessionList); + COMMETHOD( + [], + HRESULT, + "GetSessionEnumerator", + (["out"], POINTER(POINTER(IAudioSessionEnumerator)), "SessionList"), + ), + # HRESULT RegisterSessionNotification( + # IAudioSessionNotification *SessionNotification); + COMMETHOD( + [], + HRESULT, + "RegisterSessionNotification", + (["in"], POINTER(IAudioSessionNotification), "SessionNotification"), + ), + # HRESULT UnregisterSessionNotification( + # IAudioSessionNotification *SessionNotification); + COMMETHOD( + [], + HRESULT, + "UnregisterSessionNotification", + (["in"], POINTER(IAudioSessionNotification), "SessionNotification"), + ), + # HRESULT RegisterDuckNotification( + # LPCWSTR SessionID, + # IAudioVolumeDuckNotification *duckNotification); + COMMETHOD( + [], + HRESULT, + "RegisterDuckNotification", + (["in"], LPCWSTR, "SessionID"), + (["in"], POINTER(IAudioVolumeDuckNotification), "duckNotification"), + ), + # HRESULT UnregisterDuckNotification( + # IAudioVolumeDuckNotification *duckNotification); + COMMETHOD( + [], + HRESULT, + "UnregisterDuckNotification", + (["in"], POINTER(IAudioVolumeDuckNotification), "duckNotification"), + ), + ) diff --git a/addon/globalPlugins/soundmanager/pycaw/api/audiopolicy/__pycache__/__init__.cpython-311.pyc b/addon/globalPlugins/soundmanager/pycaw/api/audiopolicy/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..eaf6d85 Binary files /dev/null and b/addon/globalPlugins/soundmanager/pycaw/api/audiopolicy/__pycache__/__init__.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/pycaw/api/endpointvolume/__init__.py b/addon/globalPlugins/soundmanager/pycaw/api/endpointvolume/__init__.py new file mode 100644 index 0000000..7147cd5 --- /dev/null +++ b/addon/globalPlugins/soundmanager/pycaw/api/endpointvolume/__init__.py @@ -0,0 +1,177 @@ +from ctypes import HRESULT, POINTER, c_float +from ctypes.wintypes import BOOL, DWORD, UINT + +from comtypes import COMMETHOD, GUID, IUnknown + +from .depend import PAUDIO_VOLUME_NOTIFICATION_DATA + + +class IAudioEndpointVolumeCallback(IUnknown): + _iid_ = GUID("{b1136c83-b6b5-4add-98a5-a2df8eedf6fa}") + _methods_ = ( + # HRESULT OnNotify( + # [in] PAUDIO_VOLUME_NOTIFICATION_DATA pNotify); + COMMETHOD( + [], + HRESULT, + "OnNotify", + (["in"], PAUDIO_VOLUME_NOTIFICATION_DATA, "pNotify"), + ), + ) + + +class IAudioEndpointVolume(IUnknown): + _iid_ = GUID("{5CDF2C82-841E-4546-9722-0CF74078229A}") + _methods_ = ( + # HRESULT RegisterControlChangeNotify( + # [in] IAudioEndpointVolumeCallback *pNotify); + COMMETHOD( + [], + HRESULT, + "RegisterControlChangeNotify", + (["in"], POINTER(IAudioEndpointVolumeCallback), "pNotify"), + ), + # HRESULT UnregisterControlChangeNotify( + # [in] IAudioEndpointVolumeCallback *pNotify); + COMMETHOD( + [], + HRESULT, + "UnregisterControlChangeNotify", + (["in"], POINTER(IAudioEndpointVolumeCallback), "pNotify"), + ), + # HRESULT GetChannelCount([out] UINT *pnChannelCount); + COMMETHOD( + [], HRESULT, "GetChannelCount", (["out"], POINTER(UINT), "pnChannelCount") + ), + # HRESULT SetMasterVolumeLevel( + # [in] float fLevelDB, [in] LPCGUID pguidEventContext); + COMMETHOD( + [], + HRESULT, + "SetMasterVolumeLevel", + (["in"], c_float, "fLevelDB"), + (["in"], POINTER(GUID), "pguidEventContext"), + ), + # HRESULT SetMasterVolumeLevelScalar( + # [in] float fLevel, [in] LPCGUID pguidEventContext); + COMMETHOD( + [], + HRESULT, + "SetMasterVolumeLevelScalar", + (["in"], c_float, "fLevel"), + (["in"], POINTER(GUID), "pguidEventContext"), + ), + # HRESULT GetMasterVolumeLevel([out] float *pfLevelDB); + COMMETHOD( + [], + HRESULT, + "GetMasterVolumeLevel", + (["out"], POINTER(c_float), "pfLevelDB"), + ), + # HRESULT GetMasterVolumeLevelScalar([out] float *pfLevel); + COMMETHOD( + [], + HRESULT, + "GetMasterVolumeLevelScalar", + (["out"], POINTER(c_float), "pfLevelDB"), + ), + # HRESULT SetChannelVolumeLevel( + # [in] UINT nChannel, + # [in] float fLevelDB, + # [in] LPCGUID pguidEventContext); + COMMETHOD( + [], + HRESULT, + "SetChannelVolumeLevel", + (["in"], UINT, "nChannel"), + (["in"], c_float, "fLevelDB"), + (["in"], POINTER(GUID), "pguidEventContext"), + ), + # HRESULT SetChannelVolumeLevelScalar( + # [in] UINT nChannel, + # [in] float fLevel, + # [in] LPCGUID pguidEventContext); + COMMETHOD( + [], + HRESULT, + "SetChannelVolumeLevelScalar", + (["in"], DWORD, "nChannel"), + (["in"], c_float, "fLevelDB"), + (["in"], POINTER(GUID), "pguidEventContext"), + ), + # HRESULT GetChannelVolumeLevel( + # [in] UINT nChannel, + # [out] float *pfLevelDB); + COMMETHOD( + [], + HRESULT, + "GetChannelVolumeLevel", + (["in"], UINT, "nChannel"), + (["out"], POINTER(c_float), "pfLevelDB"), + ), + # HRESULT GetChannelVolumeLevelScalar( + # [in] UINT nChannel, + # [out] float *pfLevel); + COMMETHOD( + [], + HRESULT, + "GetChannelVolumeLevelScalar", + (["in"], DWORD, "nChannel"), + (["out"], POINTER(c_float), "pfLevelDB"), + ), + # HRESULT SetMute([in] BOOL bMute, [in] LPCGUID pguidEventContext); + COMMETHOD( + [], + HRESULT, + "SetMute", + (["in"], BOOL, "bMute"), + (["in"], POINTER(GUID), "pguidEventContext"), + ), + # HRESULT GetMute([out] BOOL *pbMute); + COMMETHOD([], HRESULT, "GetMute", (["out"], POINTER(BOOL), "pbMute")), + # HRESULT GetVolumeStepInfo( + # [out] UINT *pnStep, + # [out] UINT *pnStepCount); + COMMETHOD( + [], + HRESULT, + "GetVolumeStepInfo", + (["out"], POINTER(DWORD), "pnStep"), + (["out"], POINTER(DWORD), "pnStepCount"), + ), + # HRESULT VolumeStepUp([in] LPCGUID pguidEventContext); + COMMETHOD( + [], HRESULT, "VolumeStepUp", (["in"], POINTER(GUID), "pguidEventContext") + ), + # HRESULT VolumeStepDown([in] LPCGUID pguidEventContext); + COMMETHOD( + [], HRESULT, "VolumeStepDown", (["in"], POINTER(GUID), "pguidEventContext") + ), + # HRESULT QueryHardwareSupport([out] DWORD *pdwHardwareSupportMask); + COMMETHOD( + [], + HRESULT, + "QueryHardwareSupport", + (["out"], POINTER(DWORD), "pdwHardwareSupportMask"), + ), + # HRESULT GetVolumeRange( + # [out] float *pfLevelMinDB, + # [out] float *pfLevelMaxDB, + # [out] float *pfVolumeIncrementDB); + COMMETHOD( + [], + HRESULT, + "GetVolumeRange", + (["out"], POINTER(c_float), "pfMin"), + (["out"], POINTER(c_float), "pfMax"), + (["out"], POINTER(c_float), "pfIncr"), + ), + ) + + +class IAudioMeterInformation(IUnknown): + _iid_ = GUID("{C02216F6-8C67-4B5B-9D00-D008E73E0064}") + _methods_ = ( + # HRESULT GetPeakValue([out] c_float *pfPeak); + COMMETHOD([], HRESULT, "GetPeakValue", (["out"], POINTER(c_float), "pfPeak")), + ) diff --git a/addon/globalPlugins/soundmanager/pycaw/api/endpointvolume/__pycache__/__init__.cpython-311.pyc b/addon/globalPlugins/soundmanager/pycaw/api/endpointvolume/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..2e83a40 Binary files /dev/null and b/addon/globalPlugins/soundmanager/pycaw/api/endpointvolume/__pycache__/__init__.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/pycaw/api/endpointvolume/__pycache__/depend.cpython-311.pyc b/addon/globalPlugins/soundmanager/pycaw/api/endpointvolume/__pycache__/depend.cpython-311.pyc new file mode 100644 index 0000000..aa1aeaf Binary files /dev/null and b/addon/globalPlugins/soundmanager/pycaw/api/endpointvolume/__pycache__/depend.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/pycaw/api/endpointvolume/depend.py b/addon/globalPlugins/soundmanager/pycaw/api/endpointvolume/depend.py new file mode 100644 index 0000000..fbe484b --- /dev/null +++ b/addon/globalPlugins/soundmanager/pycaw/api/endpointvolume/depend.py @@ -0,0 +1,17 @@ +from ctypes import POINTER, Structure, c_float +from ctypes.wintypes import BOOL, UINT + +from comtypes import GUID + + +class AUDIO_VOLUME_NOTIFICATION_DATA(Structure): + _fields_ = [ + ("guidEventContext", GUID), + ("bMuted", BOOL), + ("fMasterVolume", c_float), + ("nChannels", UINT), + ("afChannelVolumes", c_float * 8), + ] + + +PAUDIO_VOLUME_NOTIFICATION_DATA = POINTER(AUDIO_VOLUME_NOTIFICATION_DATA) diff --git a/addon/globalPlugins/soundmanager/pycaw/api/mmdeviceapi/__init__.py b/addon/globalPlugins/soundmanager/pycaw/api/mmdeviceapi/__init__.py new file mode 100644 index 0000000..e126fe5 --- /dev/null +++ b/addon/globalPlugins/soundmanager/pycaw/api/mmdeviceapi/__init__.py @@ -0,0 +1,181 @@ +from ctypes import HRESULT, POINTER +from ctypes.wintypes import DWORD, LPCWSTR, LPWSTR, UINT + +from comtypes import COMMETHOD, GUID, IUnknown + +from .depend import PROPERTYKEY, IPropertyStore + + +class IMMDevice(IUnknown): + _iid_ = GUID("{D666063F-1587-4E43-81F1-B948E807363F}") + _methods_ = ( + # HRESULT Activate( + # [in] REFIID iid, + # [in] DWORD dwClsCtx, + # [in] PROPVARIANT *pActivationParams, + # [out] void **ppInterface); + COMMETHOD( + [], + HRESULT, + "Activate", + (["in"], POINTER(GUID), "iid"), + (["in"], DWORD, "dwClsCtx"), + (["in"], POINTER(DWORD), "pActivationParams"), + (["out"], POINTER(POINTER(IUnknown)), "ppInterface"), + ), + # HRESULT OpenPropertyStore( + # [in] DWORD stgmAccess, + # [out] IPropertyStore **ppProperties); + COMMETHOD( + [], + HRESULT, + "OpenPropertyStore", + (["in"], DWORD, "stgmAccess"), + (["out"], POINTER(POINTER(IPropertyStore)), "ppProperties"), + ), + # HRESULT GetId([out] LPWSTR *ppstrId); + COMMETHOD([], HRESULT, "GetId", (["out"], POINTER(LPWSTR), "ppstrId")), + # HRESULT GetState([out] DWORD *pdwState); + COMMETHOD([], HRESULT, "GetState", (["out"], POINTER(DWORD), "pdwState")), + ) + + +class IMMDeviceCollection(IUnknown): + _iid_ = GUID("{0BD7A1BE-7A1A-44DB-8397-CC5392387B5E}") + _methods_ = ( + # HRESULT GetCount([out] UINT *pcDevices); + COMMETHOD([], HRESULT, "GetCount", (["out"], POINTER(UINT), "pcDevices")), + # HRESULT Item([in] UINT nDevice, [out] IMMDevice **ppDevice); + COMMETHOD( + [], + HRESULT, + "Item", + (["in"], UINT, "nDevice"), + (["out"], POINTER(POINTER(IMMDevice)), "ppDevice"), + ), + ) + + +class IMMNotificationClient(IUnknown): + _case_insensitive_ = True + _iid_ = GUID("{7991EEC9-7E89-4D85-8390-6C703CEC60C0}") + _methods_ = ( + # HRESULT OnDeviceStateChanged( + # [in] LPCWSTR pwstrDeviceId, + # [in] DWORD dwNewState); + COMMETHOD( + [], + HRESULT, + "OnDeviceStateChanged", + (["in"], LPCWSTR, "pwstrDeviceId"), + (["in"], DWORD, "dwNewState"), + ), + # HRESULT OnDeviceAdded( + # [in] LPCWSTR pwstrDeviceId, + COMMETHOD( + [], + HRESULT, + "OnDeviceAdded", + (["in"], LPCWSTR, "pwstrDeviceId"), + ), + # HRESULT OnDeviceRemoved( + # [in] LPCWSTR pwstrDeviceId, + COMMETHOD( + [], + HRESULT, + "OnDeviceRemoved", + (["in"], LPCWSTR, "pwstrDeviceId"), + ), + # HRESULT OnDefaultDeviceChanged( + # [in] EDataFlow flow, + # [in] ERole role, + # [in] LPCWSTR pwstrDefaultDeviceId; + COMMETHOD( + [], + HRESULT, + "OnDefaultDeviceChanged", + (["in"], DWORD, "flow"), + (["in"], DWORD, "role"), + (["in"], LPCWSTR, "pwstrDefaultDeviceId"), + ), + # HRESULT OnPropertyValueChanged( + # [in] LPCWSTR pwstrDeviceId, + # [in] const PROPERTYKEY key); + COMMETHOD( + [], + HRESULT, + "OnPropertyValueChanged", + (["in"], LPCWSTR, "pwstrDeviceId"), + (["in"], PROPERTYKEY, "key"), + ), + ) + + +class IMMDeviceEnumerator(IUnknown): + _iid_ = GUID("{A95664D2-9614-4F35-A746-DE8DB63617E6}") + _methods_ = ( + # HRESULT EnumAudioEndpoints( + # [in] EDataFlow dataFlow, + # [in] DWORD dwStateMask, + # [out] IMMDeviceCollection **ppDevices); + COMMETHOD( + [], + HRESULT, + "EnumAudioEndpoints", + (["in"], DWORD, "dataFlow"), + (["in"], DWORD, "dwStateMask"), + (["out"], POINTER(POINTER(IMMDeviceCollection)), "ppDevices"), + ), + # HRESULT GetDefaultAudioEndpoint( + # [in] EDataFlow dataFlow, + # [in] ERole role, + # [out] IMMDevice **ppDevice); + COMMETHOD( + [], + HRESULT, + "GetDefaultAudioEndpoint", + (["in"], DWORD, "dataFlow"), + (["in"], DWORD, "role"), + (["out"], POINTER(POINTER(IMMDevice)), "ppDevices"), + ), + # HRESULT GetDevice( + # [in] LPCWSTR pwstrId, + # [out] IMMDevice **ppDevice); + COMMETHOD( + [], + HRESULT, + "GetDevice", + (["in"], LPCWSTR, "pwstrId"), + (["out"], POINTER(POINTER(IMMDevice)), "ppDevice"), + ), + # HRESULT RegisterEndpointNotificationCallback( + # [in] IMMNotificationClient *pClient); + COMMETHOD( + [], + HRESULT, + "RegisterEndpointNotificationCallback", + (["in"], POINTER(IMMNotificationClient), "pClient"), + ), + # HRESULT UnregisterEndpointNotificationCallback( + # [in] IMMNotificationClient *pClient); + COMMETHOD( + [], + HRESULT, + "UnregisterEndpointNotificationCallback", + (["in"], POINTER(IMMNotificationClient), "pClient"), + ), + ) + + +class IMMEndpoint(IUnknown): + _iid_ = GUID("{1BE09788-6894-4089-8586-9A2A6C265AC5}") + _methods_ = ( + # HRESULT GetDataFlow( + # [out] EDataFlow *pDataFlow); + COMMETHOD( + [], + HRESULT, + "GetDataFlow", + (["out"], POINTER(DWORD), "pDataFlow"), + ), + ) diff --git a/addon/globalPlugins/soundmanager/pycaw/api/mmdeviceapi/__pycache__/__init__.cpython-311.pyc b/addon/globalPlugins/soundmanager/pycaw/api/mmdeviceapi/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..b95bbf3 Binary files /dev/null and b/addon/globalPlugins/soundmanager/pycaw/api/mmdeviceapi/__pycache__/__init__.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/pycaw/api/mmdeviceapi/depend/__init__.py b/addon/globalPlugins/soundmanager/pycaw/api/mmdeviceapi/depend/__init__.py new file mode 100644 index 0000000..5aedf23 --- /dev/null +++ b/addon/globalPlugins/soundmanager/pycaw/api/mmdeviceapi/depend/__init__.py @@ -0,0 +1,47 @@ +from ctypes import HRESULT, POINTER +from ctypes.wintypes import DWORD + +from comtypes import COMMETHOD, GUID, IUnknown + +from .structures import PROPERTYKEY, PROPVARIANT + + +class IPropertyStore(IUnknown): + _iid_ = GUID("{886d8eeb-8cf2-4446-8d02-cdba1dbdcf99}") + _methods_ = ( + # HRESULT GetCount([out] DWORD *cProps); + COMMETHOD([], HRESULT, "GetCount", (["out"], POINTER(DWORD), "cProps")), + # HRESULT GetAt( + # [in] DWORD iProp, + # [out] PROPERTYKEY *pkey); + COMMETHOD( + [], + HRESULT, + "GetAt", + (["in"], DWORD, "iProp"), + (["out"], POINTER(PROPERTYKEY), "pkey"), + ), + # HRESULT GetValue( + # [in] REFPROPERTYKEY key, + # [out] PROPVARIANT *pv); + COMMETHOD( + [], + HRESULT, + "GetValue", + (["in"], POINTER(PROPERTYKEY), "key"), + (["out"], POINTER(PROPVARIANT), "pv"), + ), + # HRESULT SetValue( + # [in] REFPROPERTYKEY key, + # [in] REFPROPVARIANT propvar + # ); + COMMETHOD( + [], + HRESULT, + "SetValue", + (["in"], POINTER(PROPERTYKEY), "key"), + (["in"], POINTER(PROPVARIANT), "propvar"), + ), + # HRESULT Commit(); + COMMETHOD([], HRESULT, "Commit"), + ) diff --git a/addon/globalPlugins/soundmanager/pycaw/api/mmdeviceapi/depend/__pycache__/__init__.cpython-311.pyc b/addon/globalPlugins/soundmanager/pycaw/api/mmdeviceapi/depend/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..21ae10c Binary files /dev/null and b/addon/globalPlugins/soundmanager/pycaw/api/mmdeviceapi/depend/__pycache__/__init__.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/pycaw/api/mmdeviceapi/depend/__pycache__/structures.cpython-311.pyc b/addon/globalPlugins/soundmanager/pycaw/api/mmdeviceapi/depend/__pycache__/structures.cpython-311.pyc new file mode 100644 index 0000000..e519649 Binary files /dev/null and b/addon/globalPlugins/soundmanager/pycaw/api/mmdeviceapi/depend/__pycache__/structures.cpython-311.pyc differ diff --git a/addon/globalPlugins/soundmanager/pycaw/api/mmdeviceapi/depend/structures.py b/addon/globalPlugins/soundmanager/pycaw/api/mmdeviceapi/depend/structures.py new file mode 100644 index 0000000..a1166f7 --- /dev/null +++ b/addon/globalPlugins/soundmanager/pycaw/api/mmdeviceapi/depend/structures.py @@ -0,0 +1,54 @@ +from ctypes import Structure, Union, byref, windll +from ctypes.wintypes import DWORD, LONG, LPWSTR, ULARGE_INTEGER, VARIANT_BOOL, WORD + +from comtypes import GUID +from comtypes.automation import VARTYPE, VT_BOOL, VT_CLSID, VT_LPWSTR, VT_UI4 + + +class PROPVARIANT_UNION(Union): + _fields_ = [ + ("lVal", LONG), + ("uhVal", ULARGE_INTEGER), + ("boolVal", VARIANT_BOOL), + ("pwszVal", LPWSTR), + ("puuid", GUID), + ] + + +class PROPVARIANT(Structure): + _fields_ = [ + ("vt", VARTYPE), + ("reserved1", WORD), + ("reserved2", WORD), + ("reserved3", WORD), + ("union", PROPVARIANT_UNION), + ] + + def GetValue(self): + vt = self.vt + if vt == VT_BOOL: + return self.union.boolVal != 0 + elif vt == VT_LPWSTR: + # return Marshal.PtrToStringUni(union.pwszVal) + return self.union.pwszVal + elif vt == VT_UI4: + return self.union.lVal + elif vt == VT_CLSID: + # TODO + # return (Guid)Marshal.PtrToStructure(union.puuid, typeof(Guid)) + return + else: + return "%s:?" % (vt) + + def clear(self): + windll.ole32.PropVariantClear(byref(self)) + + +class PROPERTYKEY(Structure): + _fields_ = [ + ("fmtid", GUID), + ("pid", DWORD), + ] + + def __str__(self): + return "%s %s" % (self.fmtid, self.pid) diff --git a/addon/globalPlugins/soundmanager/pycaw/callbacks.py b/addon/globalPlugins/soundmanager/pycaw/callbacks.py new file mode 100644 index 0000000..11e8b11 --- /dev/null +++ b/addon/globalPlugins/soundmanager/pycaw/callbacks.py @@ -0,0 +1,359 @@ +from ctypes import pointer + +from comtypes import COMObject + +from pycaw.api.audiopolicy import ( + IAudioSessionControl2, + IAudioSessionEvents, + IAudioSessionNotification, +) +from pycaw.api.endpointvolume import IAudioEndpointVolumeCallback +from pycaw.api.mmdeviceapi import IMMNotificationClient +from pycaw.utils import AudioSession + + +class AudioSessionNotification(COMObject): + """ + Helper for audio session created callbacks. + + Note + ---- + In order for the AudioSessionNotification to work you need to play nicely + by following these Windows rules: + 1. Com needs to be in MTA. That is archived by defining + the following flag before pycaw or comtypes are imported: + sys.coinit_flags = 0 + 2. Get the AudioSessionManager: + mgr = AudioUtilities.GetAudioSessionManager() + 3. Create and register callback: + MyCustomCallback(AudioSessionNotification): + def on_session_created(self, new_session): + print("on_session_created") + callback = MyCustomCallback() + mgr.RegisterSessionNotification(callback) + 4. Call the session enumerator (otherwise on_session_created wont work) + mgr.GetSessionEnumerator() + 5. Unregister, when you are finished: + mgr.UnregisterSessionNotification(callback) + + Methods + ------- + Override the following method: + + def on_session_created(self, new_volume, new_mute, event_context): + Is fired, when a new audio session is created. + new_session : pycaw.utils.AudioSession + """ + + _com_interfaces_ = (IAudioSessionNotification,) + + def OnSessionCreated(self, new_session): + ctl2 = new_session.QueryInterface(IAudioSessionControl2) + new_session = AudioSession(ctl2) + self.on_session_created(new_session) + + def on_session_created(self, new_session): + """pycaw user interface""" + raise NotImplementedError + + +class AudioSessionEvents(COMObject): + """ + Helper for audio session callbacks. + + Methods + ------- + Override the following method(s): + + def on_display_name_changed(self, new_display_name, event_context): + Is fired, when the audio session name is changed. + new_display_name : str + The new name that is displayed. + event_context : comtypes.GUID + the guid "should" be unique to who made the changes. + access guid str with event_context.contents. + + def OnIconPathChanged(self, new_icon_path, event_context): + Is fired, when the audio session icon path is changed. + new_icon_path : str + The new path of the icon that is displayed. + event_context : comtypes.GUID + the guid "should" be unique to who made the changes. + access guid str with event_context.contents. + + def on_simple_volume_changed(self, new_volume, new_mute, event_context): + Is fired, when the audio session volume/mute changed. + new_volume : float + in range(0, 1) + new_mute : int + 0, 1 + event_context : comtypes.GUID + the guid "should" be unique to who made the changes. + access guid str with event_context.contents. + + def OnChannelVolumeChanged(self, channel_count, new_channel_volume_array, + changed_channel, event_context): + Is fired, when the audio session channels volume changed. + channel_count: int + This parameter specifies the number of audio channels in the session + submix. + new_channel_volume_array : float array + values in range(0, 1) + changed_channel : int + The number (x) of the channel whose volume level changed. + Use (x-1) as index of new_channel_volume_array + to get the new volume for the changed_channel (x) + event_context : comtypes.GUID + the guid "should" be unique to who made the changes. + access guid str with event_context.contents. + + def OnGroupingParamChanged(self, new_grouping_param, event_context): + Is fired, when the grouping parameter for the session has changed. + new_grouping_param : comtypes.GUID + points to a grouping-parameter GUID. + event_context : comtypes.GUID + the guid "should" be unique to who made the changes. + access guid str with event_context.contents. + + def on_state_changed(self, new_state, new_state_id): + Is fired, when the audio session state changed. + new_state : str + "Inactive", "Active", "Expired" + new_state_id : int + 0, 1, 2 + + def on_session_disconnected(self, disconnect_reason, disconnect_reason_id): + Is fired, when the audio session disconnected "hard". + Mostly on_state_changed == "Expired" is what you are looking for. + see self.AudioSessionDisconnectReason for disconnect_reason. + The use is similar to on_state_changed. + """ + + _com_interfaces_ = (IAudioSessionEvents,) + + # ======= DECODE RETURNED INT VALUE ======= + # see audiosessiontypes.h and audiopolicy.h + AudioSessionState = ("Inactive", "Active", "Expired") + + AudioSessionDisconnectReason = ( + "DeviceRemoval", + "ServerShutdown", + "FormatChanged", + "SessionLogoff", + "SessionDisconnected", + "ExclusiveModeOverride", + ) + + def OnDisplayNameChanged(self, new_display_name, event_context): + self.on_display_name_changed(new_display_name, event_context) + + def OnIconPathChanged(self, new_icon_path, event_context): + self.on_icon_path_changed(new_icon_path, event_context) + + def OnSimpleVolumeChanged(self, new_volume, new_mute, event_context): + self.on_simple_volume_changed(new_volume, new_mute, event_context) + + def OnChannelVolumeChanged( + self, channel_count, new_channel_volume_array, changed_channel, event_context + ): + self.on_channel_volume_changed( + channel_count, new_channel_volume_array, changed_channel, event_context + ) + + def OnGroupingParamChanged(self, new_grouping_param, event_context): + self.on_grouping_param_changed(new_grouping_param, event_context) + + def OnStateChanged(self, new_state_id): + new_state = self.AudioSessionState[new_state_id] + self.on_state_changed(new_state, new_state_id) + + def OnSessionDisconnected(self, disconnect_reason_id): + disconnect_reason = self.AudioSessionDisconnectReason[disconnect_reason_id] + self.on_session_disconnected(disconnect_reason, disconnect_reason_id) + + def on_display_name_changed(self, new_display_name, event_context): + """pycaw user interface""" + pass + + def on_icon_path_changed(self, new_icon_path, event_context): + """pycaw user interface""" + pass + + def on_simple_volume_changed(self, new_volume, new_mute, event_context): + """pycaw user interface""" + pass + + def on_channel_volume_changed( + self, channel_count, new_channel_volume_array, changed_channel, event_context + ): + """pycaw user interface""" + pass + + def on_grouping_param_changed(self, new_grouping_param, event_context): + """pycaw user interface""" + pass + + def on_state_changed(self, new_state, new_state_id): + """pycaw user interface""" + pass + + def on_session_disconnected(self, disconnect_reason, disconnect_reason_id): + """pycaw user interface""" + pass + + +class AudioEndpointVolumeCallback(COMObject): + """ + Helper for audio device volume callbacks. + + Methods + ------- + Override the following method: + + def on_notify(self, new_volume, new_mute, event_context, + channels, channel_volumes): + Is fired, when the audio device volume/mute changed. + new_volume : float + in range(0, 1) + new_mute : int + 0, 1 + event_context : comtypes.GUID + the guid "should" be unique to who made the changes. + access guid str with event_context.contents + channels : int + count of channels + channel_volumes : list : float + the channel volumes in range(0, 1) + len(channel_volumes) == channels + """ + + _com_interfaces_ = (IAudioEndpointVolumeCallback,) + + def OnNotify(self, pNotify): + """Fired by Windows, when the audio device volume/mute changed""" + + # get the data of the PAUDIO_VOLUME_NOTIFICATION_DATA Structure + notify_data = pNotify.contents + + channels = notify_data.nChannels + # _.afChannelVolumes is a c_float_Array_8 -> convert to list + channel_volumes = list(notify_data.afChannelVolumes) + # remove from 8 value list everything out of channel range + channel_volumes = channel_volumes[:channels] + + event_context = pointer(notify_data.guidEventContext) + + self.on_notify( + notify_data.fMasterVolume, + notify_data.bMuted, + event_context, + channels, + channel_volumes, + ) + + def on_notify(self, new_volume, new_mute, event_context, channels, channel_volumes): + """pycaw user interface""" + raise NotImplementedError + + +class MMNotificationClient(COMObject): + """ + Helper for audio endpoint device callbacks. + + Methods + ------- + Override the following method(s): + + def on_default_device_changed(flow, flow_id, role, role_id, default_device_id): + Is fired, when the default endpoint device for a role changed. + flow : str + String explaining the data-flow direction. + flow_id: int + Id of the data-flow direction. + role : str + String explaining the role of the device. + role_id: int + Id of the role. + default-device_id: str + String containing the default device id. + + def on_device_added(self, added_device_id): + Is fired when a new endpoint device is added. + added_device_id: str + String containing the added device id. + + def on_device_removed(self, added_device_id): + Is fired when a new endpoint device is removed. + removed_device_id: str + String containing the removed device id. + + def on_device_state_changed(self, device_id, new_state, new_state_id): + Is fired when the state of an endpoint device has changed. + device_id: str + String containing the id of the device that has changed state. + new_state: str + String containing the new state. + new_state_id: int + ID of the new state. + + def on_property_value_changed(self, device_id, property_struct, fmtid, pid): + Is fired when the value of a property belonging to an audio endpoint device + has changed. + device_id: str + String containing the id of the device for which a property is changed. + property_struct: pycaw.api.mmdeviceapi.depend.structures.PROPERTYKEY + A structure containing an unique GUID for the property and a PID + (property identifier). + fmtid: comtypes.GUID + GUID of the changed property. + pid: int + PID of the changed property. + """ + + _com_interfaces_ = (IMMNotificationClient,) + + DeviceStates = {1: "Active", 2: "Disabled", 4: "NotPresent", 8: "Unplugged"} + Roles = ["eConsole", "eMultimedia", "eCommunications", "ERole_enum_count"] + DataFlow = ["eRender", "eCapture", "eAll", "EDataFlow_enum_count"] + + def OnDefaultDeviceChanged(self, flow_id, role_id, default_device_id): + flow = self.DataFlow[flow_id] + role = self.Roles[role_id] + self.on_default_device_changed(flow, flow_id, role, role_id, default_device_id) + + def OnDeviceAdded(self, added_device_id): + self.on_device_added(added_device_id) + + def OnDeviceRemoved(self, removed_device_id): + self.on_device_removed(removed_device_id) + + def OnDeviceStateChanged(self, device_id, new_state_id): + new_state = self.DeviceStates[new_state_id] + self.on_device_state_changed(device_id, new_state, new_state_id) + + def OnPropertyValueChanged(self, device_id, property_struct): + fmtid = property_struct.fmtid + pid = property_struct.pid + self.on_property_value_changed(device_id, property_struct, fmtid, pid) + + def on_default_device_changed( + self, flow, flow_id, role, role_id, default_device_id + ): + """pycaw user interface""" + pass + + def on_device_added(self, added_device_id): + """pycaw user interface""" + pass + + def on_device_removed(self, removed_device_id): + """pycaw user interface""" + pass + + def on_device_state_changed(self, device_id, new_state, new_state_id): + """pycaw user interface""" + pass + + def on_property_value_changed(self, device_id, property_struct, fmtid, pid): + """pycaw user interface""" + pass diff --git a/addon/globalPlugins/soundmanager/pycaw/constants.py b/addon/globalPlugins/soundmanager/pycaw/constants.py new file mode 100644 index 0000000..ed5906c --- /dev/null +++ b/addon/globalPlugins/soundmanager/pycaw/constants.py @@ -0,0 +1,52 @@ +from enum import Enum, IntEnum + +from comtypes import GUID + +IID_Empty = GUID("{00000000-0000-0000-0000-000000000000}") + +CLSID_MMDeviceEnumerator = GUID("{BCDE0395-E52F-467C-8E3D-C4579291692E}") + + +class ERole(Enum): + eConsole = 0 + eMultimedia = 1 + eCommunications = 2 + ERole_enum_count = 3 + + +class EDataFlow(Enum): + eRender = 0 + eCapture = 1 + eAll = 2 + EDataFlow_enum_count = 3 + + +class DEVICE_STATE(Enum): + ACTIVE = 0x00000001 + DISABLED = 0x00000002 + NOTPRESENT = 0x00000004 + UNPLUGGED = 0x00000008 + MASK_ALL = 0x0000000F + + +class AudioDeviceState(Enum): + Active = 0x1 + Disabled = 0x2 + NotPresent = 0x4 + Unplugged = 0x8 + + +class STGM(Enum): + STGM_READ = 0x00000000 + + +class AUDCLNT_SHAREMODE(Enum): + AUDCLNT_SHAREMODE_SHARED = 0x00000001 + AUDCLNT_SHAREMODE_EXCLUSIVE = 0x00000002 + + +class AudioSessionState(IntEnum): + # IntEnum to make instances comparable. + Inactive = 0 + Active = 1 + Expired = 2 diff --git a/addon/globalPlugins/soundmanager/pycaw/magic.py b/addon/globalPlugins/soundmanager/pycaw/magic.py new file mode 100644 index 0000000..6e4b670 --- /dev/null +++ b/addon/globalPlugins/soundmanager/pycaw/magic.py @@ -0,0 +1,862 @@ +""" +Note +---- +'import pycaw.magic' must be generally at the topmost. +To be more specific: +It needs to be imported before any other pycaw or comtypes import. + + +Reserved Atrributes +------------------- +Note that certain methods and attributes are reserved for the magic module. + Please look into the source code for more information. +But to avoid conflicts now and in the future, i recommend using +a prefix for each of your custom methods and attributes. + + +COM Note for DEVS +----------------- +in order for AudioSessionNotification::OnSessionCreated to work +the entry point needs to CoInitialize +the main thread in Multi-threaded Apartment (MTA). + +All the other Notification will work in the "default" comtypes mode: + Single-threaded Apartment (STA) + +Its generally true that when you work with COM components, +that to need to match the apartment to the component. +But since we are only accessing COM components, +it works using a MTA client to access both STA and MTA COM objects. + +isort:skip_file +""" + +import atexit +import logging +import sys +import warnings + +# ____ COM WITH MULTITHREADED APARTMENT ____ +sys.coinit_flags = 0 # noqa: E402 + +# flake8: noqa: E402 +import psutil + +from ctypes import pointer +from _ctypes import COMError +from comtypes import GUID, COMObject + +from pycaw.api.audioclient import ISimpleAudioVolume +from pycaw.api.audiopolicy import ( + IAudioSessionControl2, + IAudioSessionEvents, + IAudioSessionNotification, +) +from pycaw.constants import AudioSessionState +from pycaw.utils import AudioUtilities + +log = logging.getLogger(__name__) +# use logging.INFO to skip the logs from the comtypes module + +__all__ = ("MagicManager", "MagicApp", "MagicSession") + + +class MagicManager(COMObject): + """ + The 'MagicManager' handles the magic. + + Features + -------- + - handles adding and removing sessions from a dictionary. + - the main dict 'magic_root_sessions' contains all active sessions. + + - If one or more MagicApps are hooked into the MagicManager, + (by creating a new instance of a MagicApp) + the MagicManager will hand over the requested sessions when available. + (Those sessions which match the MagicApp(app_exec) + like firefox.exe) + Multiple sessions can be controlled by one MagicApp by passing + multiple app_exec as a set to MagicApp: + MagicApp({"firefox.exe", "vlc.exe"}) + When an app name has multiple audio sessions, + they will be automatically 'merged' + + Note that when using MagicApp in multi session mode, that getting the + volume will result in getting the volume of the loudest session. + Mute and State will return the max(*states) state. + + - handles giving each Windows audio session an iid. (0, 1, 2 ...) + and manage it based on that. + + - unregister all (still active) sessions from callback at shutdown + + - OnSessionCreated is fired by Windows everytime a new session registers + """ + + _com_interfaces_ = (IAudioSessionNotification,) + magic_activated = False + + @classmethod + def str(cls): + """Get infos about the current state.""" + # __str__ wont work since MagicManager is a class + if not cls.magic_activated: + log.warning("Nothing to show. MagicManager needs to be activated") + return "unactive MagicManager" + + return ( + f"" + ) + + @classmethod + def activate_magic(cls): + """ + Gets only called once or never. Not on import. + Depending if MagicApp or MagicSession are used. + see: + self.add_magic_app() + self.magic_session() + """ + if cls.magic_activated: + warn = "cannot activate MagicManager. " "MagicManager is already active!" + log.warning(warn) + warnings.warn(warn) + return + + if cls.magic_activated is None: + warn = ( + " was already activated an closed. " + "Trying to activate again - untested" + ) + log.warning(warn) + warnings.warn(warn) + + cls.magic_activated = True + log.info(":: activate magic") + + # dict with idd -> magic_root_session -> _MagicRootSession + cls.magic_root_sessions = {} + cls.expired_magic_root_sessions = set() + # pycaw internal instance identifier + cls.iid_count = 0 + + # set of MagicApp instances + cls.magic_apps = set() + + # if registered via MagicManager.magic_session() + # will hold the MagicSession and additional args + kwargs + cls.MagicSessionConfigured = None + # dict like magic_root_sessions but for the + # magic_sessions wrappers + cls.magic_sessions = {} + + try: + cls._mgr = AudioUtilities.GetAudioSessionManager() + log.debug(" got manager") + except COMError: + cls.magic_activated = False + warn = " No speaker connected" + log.warning(warn) + raise ValueError(warn) + + # RegisterSessionNotification needs an instance not a class + cls._callback_magic_manager = cls() + cls._mgr.RegisterSessionNotification(cls._callback_magic_manager) + # has to get called - + # to make IAudioSessionNotification::OnSessionCreated working + sessionEnumerator = cls._mgr.GetSessionEnumerator() + + log.debug(" registered and activated session notification") + + # Scan for running session and add them to session_manager + + # get all active sessions + count = sessionEnumerator.GetCount() + log.info(f"{count} sessions already active") + + # add sessions to session_manager + log.debug("adding sessions manually") + + for i in range(count): + ctl = sessionEnumerator.GetSession(i) + cls.OnSessionCreated(ctl) + + # register clean up mechanism, when script is closed. + atexit.register(cls.clean_up) + + log.info(cls.str()) + + @classmethod + def OnSessionCreated(cls, ctl): + """Is fired, when a new audio session is created/found.""" + log.debug(":: new session") + + # create a pycaw internal instance identifier + iid = cls.iid_count + cls.iid_count += 1 + + # create a new magic_root_session + magic_root_session = _MagicRootSession(ctl, iid, cls) + + cls.magic_root_sessions[iid] = magic_root_session + + if cls.magic_apps: + # add exe to matching magic app + cls._match_sess_to_mapp(magic_root_session, iid) + + if cls.MagicSessionConfigured: + # if MagicSession is configured via cls.magic_session() + MagicSessionClass, args, kwargs = cls.MagicSessionConfigured + magic_session = MagicSessionClass.initialize( + magic_root_session, *args, **kwargs + ) + cls.magic_sessions[iid] = magic_session + + log.info(cls.str()) + + @classmethod + def magic_session(cls, MagicSessionClass, *args, **kwargs): + """ + gets called explicit by the user with a custom MagicSession. + this will tell the MagicManager to create for all current + and new sessions a custom MagicSession. + """ + if not cls.magic_activated: + cls.activate_magic() + + if cls.MagicSessionConfigured: + raise NotImplementedError("only one MagicSession wrapper is allowed") + + for iid, magic_root_session in cls.magic_root_sessions.items(): + magic_session = MagicSessionClass.initialize( + magic_root_session, *args, **kwargs + ) + cls.magic_sessions[iid] = magic_session + cls.MagicSessionConfigured = (MagicSessionClass, args, kwargs) + + @classmethod + def add_magic_app(cls, magic_app, app_execs): + """ + gets called when a new magic_app is created. + this will tell the MagicManager add all current and + new matching sessions by app_execs to the magic_app. + """ + if not cls.magic_activated: + cls.activate_magic() + log.info(f"searching matching active sessions for: {magic_app}") + for app_exec in app_execs: + for iid, magic_root_session in cls.magic_root_sessions.items(): + # and not magic_root_session.magic_app + # will prohibit multiple magic_apps to use the same + # magic_root_session + if ( + magic_root_session.app_exec == app_exec + and not magic_root_session.magic_app + ): + log.info(f"{magic_root_session} matched {magic_app}.") + magic_app.add_magic_root_session(iid, magic_root_session) + + # keep reference to magic_app to check later + # if new session should be added to this magic_app + cls.magic_apps.add(magic_app) + log.info(f"{magic_app} added to watchlist. {cls.str()}") + + @classmethod + def _match_sess_to_mapp(cls, magic_root_session, iid): + log.info(f"searching matching magic_app for: {magic_root_session}") + new_app_exec = magic_root_session.app_exec + for magic_app in cls.magic_apps: + for app_exec in magic_app.app_execs: + if app_exec == new_app_exec: + log.info(f"Match {magic_root_session} " f"{magic_app}") + magic_app.add_magic_root_session(iid, magic_root_session) + # return will prohibit multiple magic_apps + # to use the same magic_root_session + return + + @classmethod + def remove_session(cls, iid, magic_app=None): + """magic_root_session will get removed because it is expired""" + # pop(iid, None) must not be necessary + magic_root_session = cls.magic_root_sessions.pop(iid) + + log.info(f":: removed {magic_root_session}") + + # deactivate "trash" solution by commenting: + cls.expired_magic_root_sessions.add(magic_root_session) + + # unregister callback + # (magic_root_session -> _MagicRootSession -> IAudioSessionEvents) + magic_root_session.unregister_notification() + + # delete iid also from magic_app or magic_session + if cls.MagicSessionConfigured: + # pop session from magic sessions dict + del_magic_sessions = cls.magic_sessions.pop(iid) + + # remove circular references + magic_root_session.magic_session = None + del_magic_sessions.magic_root_session = None + + # try to remove session from the magic_app dict which is in possession + if magic_app: + # pop(iid, None) must not be necessary + magic_app.magic_root_sessions.pop(iid) + + # remove circular references + magic_root_session.magic_app = None + + log.info(f":: :: removed {magic_root_session} from {magic_app}") + + log.info(cls.str()) + + @classmethod + def empty_trash(cls): + while cls.expired_magic_root_sessions: + to_remove = cls.expired_magic_root_sessions.pop() + log.info( + ":: :: :: release " + f"from {to_remove}" + ) + + # at this point it is already unregistered ... + # see cls.remove_session() + # to_remove.unregister_notification() + + @classmethod + def clean_up(cls): + log.info(":: reverse spell") + cls._mgr.UnregisterSessionNotification(cls._callback_magic_manager) + log.info(f":: :: unregistered {cls.str()}") + cls.unregister_all() + + @classmethod + def unregister_all(cls): + # since cls.magic_root_sessions can always change (multithreading) + # instead of a for loop I present you the while loop: + # while cls.magic_root_sessions contains any items, + # they get popped and unregistered + log.debug(f"unregister {len(cls.magic_root_sessions)} sessions.") + while cls.magic_root_sessions: + # ________ REMOVES 1 ITEM FROM LIST ________ + + _, session = cls.magic_root_sessions.popitem() + session.unregister_notification() + log.info(f":: :: :: unregistered {session}") + + # XXX remove old session: + # this is the only place where it works, + # since it is user controlled and not + # in a windows COM callback. + cls.empty_trash() + + log.info(f"Bye {cls.str()}") + + del cls.magic_apps + del cls.magic_sessions + + cls.magic_activated = None + + +# TODO: +# Make it more pythonic and beautifull +def for_session_in_sessions(func): + """Decorator for looping through sessions in MagicApp.""" + + def wrapper(self, *args): + if self.magic_root_sessions is None: + # nothing to change + return + + # RuntimeError: dictionary changed size during iteration + temp_sessions = dict(self.magic_root_sessions) + rv = [func(self, session, *args) for session in temp_sessions.values()] + + # max([None, None]) -> TypeError: '>' not supported ... + rv_no_none = [r for r in rv if r is not None] + + # when rv = [None] -> rv_no_none = None + if rv_no_none: + # return if multiple sessions the one + # with mute true or the highest volume + # or the session with the highest AudioSessionState.value + return max(rv_no_none) + + return wrapper + + +class _MagicAudioControl: + """Simplifies the audio control by using the self.properties.""" + + # TODO: + # (this TODO applies to MagicApp, MagicSession, for_session_in_sessions) + # handle incorrect input or raise exception. + # also handle failing com calls + # (failing in terms of the retrieved value is not 'S_OK') + # it will happen, when the speaker is gets disconnected! + # can be also fixed by implementing OnSessionDisconnected + # since OnSessionDisconnected will notify if the Speaker is unplugged. + + def toggle_mute(self): + new_mute = not self.mute + self.mute = new_mute + return new_mute + + def step_volume(self, step=0.1): + current = self.volume + if current is None: + return + new = max(0, min(1, current + step)) + if new != current: + self.volume = new + return new + + +class MagicApp(_MagicAudioControl): + """ + When instantiated with at least one app_execs name, + will be able to get/ set volume etc, also if the + session is created after initialize. + """ + + guid = pointer(GUID("{E0BD1A40-9624-44FC-A607-2ED4F00B1CC4}")) + + def __init__( + self, + app_execs, + volume_callback=None, + advanced_volume_callback=None, + mute_callback=None, + advanced_mute_callback=None, + state_callback=None, + session_callback=None, + ): + # normalize app_execs + if type(app_execs) == str: + # if string directly to set: {'a', 'b', 'c'} + app_execs = (app_execs,) + self.app_execs = set(app_execs) + + # latest dict of matching sessions + self.magic_root_sessions = {} + + # callbacks + self.volume_callback = volume_callback + self.mute_callback = mute_callback + self.state_callback = state_callback + self.session_callback = session_callback + + self.advanced_volume_callback = advanced_volume_callback + self.advanced_mute_callback = advanced_mute_callback + + log.info(str(self)) + MagicManager.add_magic_app(self, app_execs) + + def add_magic_root_session(self, iid, magic_root_session): + """called by MagicManager, when a new matching session is found""" + self.magic_root_sessions[iid] = magic_root_session + # tells the magic_root_session to create a connection + # for callbacks. + magic_root_session.use_magic_app(self) + + log.info(f"Added {magic_root_session} to {self}.") + + # session callback is implemented here: + if self.session_callback: + self.session_callback(magic_root_session) + + def __str__(self): + return ( + f"<{self.__class__.__name__} " + f"registered-for='{self.app_execs}' " + f"controls-sessions='{len(self.magic_root_sessions)}'/>" + ) + + # easy control: + @property + @for_session_in_sessions + def state(self, magic_root_session): + return magic_root_session.state + + @property + @for_session_in_sessions + def volume(self, magic_root_session): + return magic_root_session.volume + + @volume.setter + @for_session_in_sessions + def volume(self, magic_root_session, volume): + magic_root_session._sav.SetMasterVolume(volume, self.guid) + + @property + @for_session_in_sessions + def mute(self, magic_root_session): + return magic_root_session.mute + + @mute.setter + @for_session_in_sessions + def mute(self, magic_root_session, mute): + magic_root_session._sav.SetMute(mute, self.guid) + + +class MagicSession(_MagicAudioControl): + """ + When activated and passed via + MagicManager.magic_session(inherited_class_of_MagicSession) + will be created for each new and current session. + Dict of all iid -> MagicSession: + MagicManager.magic_sessions + """ + + guid = pointer(GUID("{34482A3D-37DD-40E3-BB37-63A16036C87C}")) + + def __init__( + self, + volume_callback=None, + advanced_volume_callback=None, + mute_callback=None, + advanced_mute_callback=None, + state_callback=None, + ): + self.magic_root_session = self._passed_magic_root_session + self._passed_magic_root_session = None + + # callbacks + self.volume_callback = volume_callback + self.mute_callback = mute_callback + self.state_callback = state_callback + + self.advanced_volume_callback = advanced_volume_callback + self.advanced_mute_callback = advanced_mute_callback + + # tells the magic_root_session to create a connection + # for callbacks: + self.magic_root_session.use_magic_session(self) + + log.info(str(self)) + + @classmethod + def initialize(cls, new_magic_root_session, *args, **kwargs): + """ + is called by the MagicManager. + + Every new MagicSession needs to have a magic_root_session. + + instead of passing it visible as argument trough the inherited class + back to the super().__init(new_magic_root_session) - + + the new_magic_root_session it is transferred by setting + a class attribute and picking it up in the MagicSession.__init__() + + class PassingViaArg(MagicSession): + def __init__(self, new_magic_root_session, *args, **kwargs): + super().__init__(new_magic_root_session) + + class PassingViaClassAttr(MagicSession): + def __init__(self, *args, **kwargs): + super().__init__() + """ + cls._passed_magic_root_session = new_magic_root_session + + magic_session = cls(*args, **kwargs) + return magic_session + + def __str__(self): + return ( + f"<{self.__class__.__name__} " + f"app_exec='{self.magic_root_session.app_exec}'/>" + ) + + # easy control: + @property + def state(self): + return self.magic_root_session.state + + @property + def volume(self): + return self.magic_root_session.volume + + @volume.setter + def volume(self, volume): + self.magic_root_session._sav.SetMasterVolume(volume, self.guid) + + @property + def mute(self): + return self.magic_root_session.mute + + @mute.setter + def mute(self, mute): + self.magic_root_session._sav.SetMute(mute, self.guid) + + +class _MagicGuidCompare: + """ + Helper that is passed when 'advanced_xxx_callback=True' + see _MagicRootSession._send_callback() + """ + + def __init__(self, master_guid, changer_guid): + # the pointer(GUID("guid")) of the 'master' + self.master = master_guid + # the pointer(GUID("guid")) of the 'changer' + self.changer = changer_guid + + compare = master_guid.contents != changer_guid.contents + # True if changes are made external. + self.compare = compare + + def __str__(self): + return ( + f"<{self.__class__.__name__} " + f"changed-external='{self.compare}' " + f"master='{self.master.contents}' " + f"changer='{self.changer.contents}'/>" + ) + + def __bool__(self): + """Returns True if changes are made external.""" + return self.compare + + +class _MagicRootSession(COMObject): + """Base session control with callback functionality""" + + _com_interfaces_ = (IAudioSessionEvents,) + + def __init__(self, ctl, iid, magic_manager): + self._ctl2 = ctl.QueryInterface(IAudioSessionControl2) + self.app_exec = self._get_app_exec() + self._sav = None + self.magic_manager = magic_manager + self.iid = iid + + self.magic_app = None + self.magic_session = None + + self.volume = None + self.mute = None + + new_state_id = self._ctl2.GetState() + self.state = AudioSessionState(new_state_id) + + self._activated = False + + self.register_notification() + + log.info(f":: created {self}") + + def __str__(self): + return f"<{self.__class__.__name__} app='{self.app_exec}'/>" + + # only one magic_app at the time is able to + # control and view this magic_root_session. + # a second magic_app is blocked via the MagicManager ... + # see: + # MagicManager.add_magic_app() -> if not magic_root_session.magic_app + # MagicManager._match_sess_to_mapp() -> return if match + + # TODO Feature: + # allow multiple magic_app for one magic_root_session? + # If multiple magic_app would be allowed: + # Looping through multiple magic_app and checking for + # callbacks in each would cost lots of time right? + # any solution? + # + # or it could use this sessions without callbacks. + # + # also the session remove handle wouldnt work. + + def use_magic_app(self, magic_app): + self.magic_app = magic_app + self._activate() + + def use_magic_session(self, magic_session): + self.magic_session = magic_session + self._activate() + + def _activate(self): + # activates this magic_root_session. + # callbacks wont be "ignored", and volume and mute + # will be saved to attributes. + if not self._activated: + self._sav = self._ctl2.QueryInterface(ISimpleAudioVolume) + self.volume = self._sav.GetMasterVolume() + self.mute = self._sav.GetMute() + + self._activated = True + + def OnSimpleVolumeChanged(self, new_volume, new_mute, event_context): + """Is fired, when the audio session volume/mute changed.""" + log.debug( + f"OnSimpleVolumeChanged: {self.app_exec} " + f"Vol: ~{new_volume} Mute: {new_mute}" + ) + # only make callbacks and update internal volume and mute + # if someone is listening + + if not self._activated: + return + + # check old volume vs new: + if self.volume != new_volume: + # self.volume will keep the none state + # until self._activate() + self.volume = new_volume + + # send callbacks, if callback exists + self._send_callback( + self.magic_app, "volume_callback", event_context, new_volume + ) + + self._send_callback( + self.magic_session, "volume_callback", event_context, new_volume + ) + return + # check old mute vs new: + if self.mute != new_mute: + # self.mute will keep the none state + # until self._activate() + self.mute = new_mute + + # send callbacks, if callback exists + self._send_callback( + self.magic_app, "mute_callback", event_context, new_mute + ) + + self._send_callback( + self.magic_session, "mute_callback", event_context, new_mute + ) + return + + @staticmethod + def _send_callback(master, callback, changer_guid, value): + """ + Send callbacks, if callback exists. + + The default is, that callbacks which are made + by MagicApp or MagicSession get filtered out + + The hooks 'advanced_volume_callback' or 'advanced_mute_callback' + Dont filter the callback based on the changer. + + The 'compare' argument is an instance of _MagicGuidCompare + + compare == True + not: 'compare is True' + + if the changer guid is external. + """ + + # get the callback method from the master. + # will be None if the user didnt hook into it + # or if master = None + + if master is None: + return + + master_advanced_callback = getattr(master, "advanced_" + callback) + master_callback = getattr(master, callback) + + # TODO: Does this safe resources, since _MagicGuidCompare is not + # always instantiated? + if master_callback is None and master_advanced_callback is None: + return + + # create a new _MagicGuidCompare which will compare + # changer_guid and master.guid + compare = _MagicGuidCompare(master.guid, changer_guid) + + if master_advanced_callback: + master_advanced_callback(value, compare) + # TODO: + # return or allow also simple callback? + + if master_callback and compare: + # if the the callback is not caused by master.guid + # then send simple callback + master_callback(value) + + def OnStateChanged(self, new_state_id): + """Is fired, when the audio session state changed.""" + self.state = AudioSessionState(new_state_id) + + # send callbacks, if defined + if self.magic_app and self.magic_app.state_callback: + self.magic_app.state_callback(self.state) + + if self.magic_session and self.magic_session.state_callback: + self.magic_session.state_callback(self.state) + + if self.state == AudioSessionState.Expired: + """ + calling the MagicManager to remove this magic_root_session. + + The MagicManager will unregister_notification() + """ + + # XXX remove old session: + # self.magic_manager.empty_trash() + # Empty trash should do the following: + # Release + # ... but that would only work if empty_trash() + # is triggered by the user and not via a callback + + self.magic_manager.remove_session(self.iid, self.magic_app) + + # XXX remove old session: + # would crash the app: + # self._ctl2 = None + # or: + # self._ctl2.Release() + + def _get_app_exec(self): + """Returns the executable name based on the process id.""" + self.pid = self._ctl2.GetProcessId() + + if self.pid != 0: + # try: + return psutil.Process(self.pid).name() + # except psutil.NoSuchProcess: + # for some reason GetProcessId returned an non existing pid + + # TODO: + # i didnt wrote the initial try, except psutil.NoSuchProcess: + # but that should not happen right? + # i never had an issue with a non existing process (besides the 0) + + # System Sound: + # self._ctl2.GetDisplayName() returns: + # @%SystemRoot%\System32\AudioSrv.Dll,-202 + # and self._ctl2.IsSystemSoundsSession() returns S_OK + # for system sounds + + returned_HRESULT = self._ctl2.IsSystemSoundsSession() + + # Possible returned_HRESULTs: + S_OK = 0 + # S_FALSE = 1 + + if returned_HRESULT == S_OK: + return "SndVol.exe" + else: + warn = ( + "unidentified app! " + f"pid: {self.pid}, is system sound: {returned_HRESULT}" + ) + log.critical(warn) + raise ValueError(warn) + + # TODO: Feature + # implement: + # def OnSessionDisconnected(self, disconnect_reason_id): pass + + def register_notification(self): + self._ctl2.RegisterAudioSessionNotification(self) + + def unregister_notification(self): + self._ctl2.UnregisterAudioSessionNotification(self) diff --git a/addon/globalPlugins/soundmanager/pycaw/pycaw.py b/addon/globalPlugins/soundmanager/pycaw/pycaw.py index 3749e27..703b1cd 100755 --- a/addon/globalPlugins/soundmanager/pycaw/pycaw.py +++ b/addon/globalPlugins/soundmanager/pycaw/pycaw.py @@ -1,737 +1,53 @@ -""" -Python wrapper around the Core Audio Windows API. -""" -from ctypes import (HRESULT, POINTER, Structure, Union, c_float, c_longlong, - c_uint32) -from ctypes.wintypes import (BOOL, DWORD, INT, LONG, LPCWSTR, LPWSTR, UINT, - ULARGE_INTEGER, VARIANT_BOOL, WORD) -from enum import Enum - -import comtypes -import sys -if sys.version_info.major == 2: - import psutilpy2 as psutil -else: - import psutil -from comtypes import COMMETHOD, GUID, IUnknown -from comtypes.automation import VARTYPE, VT_BOOL, VT_CLSID, VT_LPWSTR, VT_UI4 -from future.utils import python_2_unicode_compatible - -IID_Empty = GUID( - '{00000000-0000-0000-0000-000000000000}') - -CLSID_MMDeviceEnumerator = GUID( - '{BCDE0395-E52F-467C-8E3D-C4579291692E}') - - -UINT32 = c_uint32 -REFERENCE_TIME = c_longlong - - -class PROPVARIANT_UNION(Union): - _fields_ = [ - ('lVal', LONG), - ('uhVal', ULARGE_INTEGER), - ('boolVal', VARIANT_BOOL), - ('pwszVal', LPWSTR), - ('puuid', GUID), - ] - - -class PROPVARIANT(Structure): - _fields_ = [ - ('vt', VARTYPE), - ('reserved1', WORD), - ('reserved2', WORD), - ('reserved3', WORD), - ('union', PROPVARIANT_UNION), - ] - - def GetValue(self): - vt = self.vt - if vt == VT_BOOL: - return self.union.boolVal != 0 - elif vt == VT_LPWSTR: - # return Marshal.PtrToStringUni(union.pwszVal) - return self.union.pwszVal - elif vt == VT_UI4: - return self.union.lVal - elif vt == VT_CLSID: - # TODO - # return (Guid)Marshal.PtrToStructure(union.puuid, typeof(Guid)) - return - else: - return "%s:?" % (vt) - - -class WAVEFORMATEX(Structure): - _fields_ = [ - ('wFormatTag', WORD), - ('nChannels', WORD), - ('nSamplesPerSec', WORD), - ('nAvgBytesPerSec', WORD), - ('nBlockAlign', WORD), - ('wBitsPerSample', WORD), - ('cbSize', WORD), - ] - - -class ERole(Enum): - eConsole = 0 - eMultimedia = 1 - eCommunications = 2 - ERole_enum_count = 3 - - -class EDataFlow(Enum): - eRender = 0 - eCapture = 1 - eAll = 2 - EDataFlow_enum_count = 3 - - -class DEVICE_STATE(Enum): - ACTIVE = 0x00000001 - DISABLED = 0x00000002 - NOTPRESENT = 0x00000004 - UNPLUGGED = 0x00000008 - MASK_ALL = 0x0000000F - - -class AudioDeviceState(Enum): - Active = 0x1 - Disabled = 0x2 - NotPresent = 0x4 - Unplugged = 0x8 - - -class STGM(Enum): - STGM_READ = 0x00000000 - - -class AUDCLNT_SHAREMODE(Enum): - AUDCLNT_SHAREMODE_SHARED = 0x00000001 - AUDCLNT_SHAREMODE_EXCLUSIVE = 0x00000002 - - -class AUDIO_VOLUME_NOTIFICATION_DATA(Structure): - _fields_ = [ - ('guidEventContext', GUID), - ('bMuted', BOOL), - ('fMasterVolume', c_float), - ('nChannels', UINT), - ('afChannelVolumes', c_float * 8), - ] - - -PAUDIO_VOLUME_NOTIFICATION_DATA = POINTER(AUDIO_VOLUME_NOTIFICATION_DATA) - - -class IAudioEndpointVolumeCallback(IUnknown): - _iid_ = GUID('{b1136c83-b6b5-4add-98a5-a2df8eedf6fa}') - _methods_ = ( - # HRESULT OnNotify( - # [in] PAUDIO_VOLUME_NOTIFICATION_DATA pNotify); - COMMETHOD([], HRESULT, 'OnNotify', - (['in'], - PAUDIO_VOLUME_NOTIFICATION_DATA, - 'pNotify')), - ) - - -class IAudioEndpointVolume(IUnknown): - _iid_ = GUID('{5CDF2C82-841E-4546-9722-0CF74078229A}') - _methods_ = ( - # HRESULT RegisterControlChangeNotify( - # [in] IAudioEndpointVolumeCallback *pNotify); - COMMETHOD([], HRESULT, 'RegisterControlChangeNotify', - (['in'], - POINTER(IAudioEndpointVolumeCallback), - 'pNotify')), - # HRESULT UnregisterControlChangeNotify( - # [in] IAudioEndpointVolumeCallback *pNotify); - COMMETHOD([], HRESULT, 'UnregisterControlChangeNotify', - (['in'], - POINTER(IAudioEndpointVolumeCallback), - 'pNotify')), - # HRESULT GetChannelCount([out] UINT *pnChannelCount); - COMMETHOD([], HRESULT, 'GetChannelCount', - (['out'], POINTER(UINT), 'pnChannelCount')), - # HRESULT SetMasterVolumeLevel( - # [in] float fLevelDB, [in] LPCGUID pguidEventContext); - COMMETHOD([], HRESULT, 'SetMasterVolumeLevel', - (['in'], c_float, 'fLevelDB'), - (['in'], POINTER(GUID), 'pguidEventContext')), - # HRESULT SetMasterVolumeLevelScalar( - # [in] float fLevel, [in] LPCGUID pguidEventContext); - COMMETHOD([], HRESULT, 'SetMasterVolumeLevelScalar', - (['in'], c_float, 'fLevel'), - (['in'], POINTER(GUID), 'pguidEventContext')), - # HRESULT GetMasterVolumeLevel([out] float *pfLevelDB); - COMMETHOD([], HRESULT, 'GetMasterVolumeLevel', - (['out'], POINTER(c_float), 'pfLevelDB')), - # HRESULT GetMasterVolumeLevelScalar([out] float *pfLevel); - COMMETHOD([], HRESULT, 'GetMasterVolumeLevelScalar', - (['out'], POINTER(c_float), 'pfLevelDB')), - # HRESULT SetChannelVolumeLevel( - # [in] UINT nChannel, - # [in] float fLevelDB, - # [in] LPCGUID pguidEventContext); - COMMETHOD([], HRESULT, 'SetChannelVolumeLevel', - (['in'], UINT, 'nChannel'), - (['in'], c_float, 'fLevelDB'), - (['in'], POINTER(GUID), 'pguidEventContext')), - # HRESULT SetChannelVolumeLevelScalar( - # [in] UINT nChannel, - # [in] float fLevel, - # [in] LPCGUID pguidEventContext); - COMMETHOD([], HRESULT, 'SetChannelVolumeLevelScalar', - (['in'], DWORD, 'nChannel'), - (['in'], c_float, 'fLevelDB'), - (['in'], POINTER(GUID), 'pguidEventContext')), - # HRESULT GetChannelVolumeLevel( - # [in] UINT nChannel, - # [out] float *pfLevelDB); - COMMETHOD([], HRESULT, 'GetChannelVolumeLevel', - (['in'], UINT, 'nChannel'), - (['out'], POINTER(c_float), 'pfLevelDB')), - # HRESULT GetChannelVolumeLevelScalar( - # [in] UINT nChannel, - # [out] float *pfLevel); - COMMETHOD([], HRESULT, 'GetChannelVolumeLevelScalar', - (['in'], DWORD, 'nChannel'), - (['out'], POINTER(c_float), 'pfLevelDB')), - # HRESULT SetMute([in] BOOL bMute, [in] LPCGUID pguidEventContext); - COMMETHOD([], HRESULT, 'SetMute', - (['in'], BOOL, 'bMute'), - (['in'], POINTER(GUID), 'pguidEventContext')), - # HRESULT GetMute([out] BOOL *pbMute); - COMMETHOD([], HRESULT, 'GetMute', - (['out'], POINTER(BOOL), 'pbMute')), - # HRESULT GetVolumeStepInfo( - # [out] UINT *pnStep, - # [out] UINT *pnStepCount); - COMMETHOD([], HRESULT, 'GetVolumeStepInfo', - (['out'], POINTER(DWORD), 'pnStep'), - (['out'], POINTER(DWORD), 'pnStepCount')), - # HRESULT VolumeStepUp([in] LPCGUID pguidEventContext); - COMMETHOD([], HRESULT, 'VolumeStepUp', - (['in'], POINTER(GUID), 'pguidEventContext')), - # HRESULT VolumeStepDown([in] LPCGUID pguidEventContext); - COMMETHOD([], HRESULT, 'VolumeStepDown', - (['in'], POINTER(GUID), 'pguidEventContext')), - # HRESULT QueryHardwareSupport([out] DWORD *pdwHardwareSupportMask); - COMMETHOD([], HRESULT, 'QueryHardwareSupport', - (['out'], POINTER(DWORD), 'pdwHardwareSupportMask')), - # HRESULT GetVolumeRange( - # [out] float *pfLevelMinDB, - # [out] float *pfLevelMaxDB, - # [out] float *pfVolumeIncrementDB); - COMMETHOD([], HRESULT, 'GetVolumeRange', - (['out'], POINTER(c_float), 'pfMin'), - (['out'], POINTER(c_float), 'pfMax'), - (['out'], POINTER(c_float), 'pfIncr'))) - - -class IAudioSessionControl(IUnknown): - _iid_ = GUID('{F4B1A599-7266-4319-A8CA-E70ACB11E8CD}') - _methods_ = ( - # HRESULT GetState ([out] AudioSessionState *pRetVal); - COMMETHOD([], HRESULT, 'NotImpl1'), - # HRESULT GetDisplayName([out] LPWSTR *pRetVal); - COMMETHOD([], HRESULT, 'GetDisplayName', - (['out'], POINTER(LPWSTR), 'pRetVal')), - # HRESULT SetDisplayName( - # [in] LPCWSTR Value, - # [in] LPCGUID EventContext); - COMMETHOD([], HRESULT, 'NotImpl2'), - # HRESULT GetIconPath([out] LPWSTR *pRetVal); - COMMETHOD([], HRESULT, 'NotImpl3'), - # HRESULT SetIconPath( - # [in] LPCWSTR Value, - # [in] LPCGUID EventContext); - COMMETHOD([], HRESULT, 'NotImpl4'), - # HRESULT GetGroupingParam([out] GUID *pRetVal); - COMMETHOD([], HRESULT, 'NotImpl5'), - # HRESULT SetGroupingParam( - # [in] LPCGUID Grouping, - # [in] LPCGUID EventContext); - COMMETHOD([], HRESULT, 'NotImpl6'), - # HRESULT RegisterAudioSessionNotification( - # [in] IAudioSessionEvents *NewNotifications); - COMMETHOD([], HRESULT, 'NotImpl7'), - # HRESULT UnregisterAudioSessionNotification( - # [in] IAudioSessionEvents *NewNotifications); - COMMETHOD([], HRESULT, 'NotImpl8')) - - -class IAudioSessionControl2(IAudioSessionControl): - _iid_ = GUID('{BFB7FF88-7239-4FC9-8FA2-07C950BE9C6D}') - _methods_ = ( - # HRESULT GetSessionIdentifier([out] LPWSTR *pRetVal); - COMMETHOD([], HRESULT, 'GetSessionIdentifier', - (['out'], POINTER(LPWSTR), 'pRetVal')), - # HRESULT GetSessionInstanceIdentifier([out] LPWSTR *pRetVal); - COMMETHOD([], HRESULT, 'GetSessionInstanceIdentifier', - (['out'], POINTER(LPWSTR), 'pRetVal')), - # HRESULT GetProcessId([out] DWORD *pRetVal); - COMMETHOD([], HRESULT, 'GetProcessId', - (['out'], POINTER(DWORD), 'pRetVal')), - # HRESULT IsSystemSoundsSession(); - COMMETHOD([], HRESULT, 'IsSystemSoundsSession'), - COMMETHOD([], HRESULT, 'SetDuckingPreferences', - (['in'], BOOL, 'optOut'))) - - -class ISimpleAudioVolume(IUnknown): - _iid_ = GUID('{87CE5498-68D6-44E5-9215-6DA47EF883D8}') - _methods_ = ( - # HRESULT SetMasterVolume( - # [in] float fLevel, - # [in] LPCGUID EventContext); - COMMETHOD([], HRESULT, 'SetMasterVolume', - (['in'], c_float, 'fLevel'), - (['in'], POINTER(GUID), 'EventContext')), - # HRESULT GetMasterVolume([out] float *pfLevel); - COMMETHOD([], HRESULT, 'GetMasterVolume', - (['out'], POINTER(c_float), 'pfLevel')), - # HRESULT SetMute( - # [in] BOOL bMute, - # [in] LPCGUID EventContext); - COMMETHOD([], HRESULT, 'SetMute', - (['in'], BOOL, 'bMute'), - (['in'], POINTER(GUID), 'EventContext')), - # HRESULT GetMute([out] BOOL *pbMute); - COMMETHOD([], HRESULT, 'GetMute', (['out'], POINTER(BOOL), 'pbMute'))) - - -class IAudioSessionEnumerator(IUnknown): - _iid_ = GUID('{E2F5BB11-0570-40CA-ACDD-3AA01277DEE8}') - _methods_ = ( - # HRESULT GetCount([out] int *SessionCount); - COMMETHOD([], HRESULT, 'GetCount', - (['out'], POINTER(INT), 'SessionCount')), - # HRESULT GetSession( - # [in] int SessionCount, - # [out] IAudioSessionControl **Session); - COMMETHOD([], HRESULT, 'GetSession', - (['in'], INT, 'SessionCount'), - (['out'], - POINTER(POINTER(IAudioSessionControl)), 'Session'))) - - -class IAudioSessionManager(IUnknown): - _iid_ = GUID('{BFA971F1-4d5e-40bb-935e-967039bfbee4}') - _methods_ = ( - # HRESULT GetAudioSessionControl( - # [in] LPCGUID AudioSessionGuid, - # [in] DWORD StreamFlags, - # [out] IAudioSessionControl **SessionControl); - COMMETHOD([], HRESULT, 'NotImpl1'), - # HRESULT GetSimpleAudioVolume( - # [in] LPCGUID AudioSessionGuid, - # [in] DWORD CrossProcessSession, - # [out] ISimpleAudioVolume **AudioVolume); - COMMETHOD([], HRESULT, 'GetSimpleAudioVolume', - (['in'], POINTER(GUID), 'AudioSessionGuid'), - (['in'], DWORD, 'CrossProcessSession'), - (['out'], - POINTER(POINTER(ISimpleAudioVolume)), 'AudioVolume'))) - - -class IAudioSessionManager2(IAudioSessionManager): - _iid_ = GUID('{77aa99a0-1bd6-484f-8bc7-2c654c9a9b6f}') - _methods_ = ( - # HRESULT GetSessionEnumerator( - # [out] IAudioSessionEnumerator **SessionList); - COMMETHOD([], HRESULT, 'GetSessionEnumerator', - (['out'], - POINTER(POINTER(IAudioSessionEnumerator)), 'SessionList')), - # HRESULT RegisterSessionNotification( - # IAudioSessionNotification *SessionNotification); - COMMETHOD([], HRESULT, 'NotImpl1'), - # HRESULT UnregisterSessionNotification( - # IAudioSessionNotification *SessionNotification); - COMMETHOD([], HRESULT, 'NotImpl2'), - # HRESULT RegisterDuckNotification( - # LPCWSTR SessionID, - # IAudioVolumeDuckNotification *duckNotification); - COMMETHOD([], HRESULT, 'NotImpl1'), - # HRESULT UnregisterDuckNotification( - # IAudioVolumeDuckNotification *duckNotification); - COMMETHOD([], HRESULT, 'NotImpl2')) - - -class IAudioClient(IUnknown): - _iid_ = GUID('{1cb9ad4c-dbfa-4c32-b178-c2f568a703b2}') - _methods_ = ( - # HRESULT Initialize( - # [in] AUDCLNT_SHAREMODE ShareMode, - # [in] DWORD StreamFlags, - # [in] REFERENCE_TIME hnsBufferDuration, - # [in] REFERENCE_TIME hnsPeriodicity, - # [in] const WAVEFORMATEX *pFormat, - # [in] LPCGUID AudioSessionGuid); - COMMETHOD([], HRESULT, 'Initialize', - (['in'], DWORD, 'ShareMode'), - (['in'], DWORD, 'StreamFlags'), - (['in'], REFERENCE_TIME, 'hnsBufferDuration'), - (['in'], REFERENCE_TIME, 'hnsPeriodicity'), - (['in'], POINTER(WAVEFORMATEX), 'pFormat'), - (['in'], POINTER(GUID), 'AudioSessionGuid')), - # HRESULT GetBufferSize( - # [out] UINT32 *pNumBufferFrames); - COMMETHOD([], HRESULT, 'GetBufferSize', - (['out'], POINTER(UINT32), 'pNumBufferFrames')), - # HRESULT GetStreamLatency( - # [out] REFERENCE_TIME *phnsLatency); - COMMETHOD([], HRESULT, 'NotImpl1'), - # HRESULT GetCurrentPadding( - # [out] UINT32 *pNumPaddingFrames); - COMMETHOD([], HRESULT, 'GetCurrentPadding', - (['out'], POINTER(UINT32), 'pNumPaddingFrames')), - # HRESULT IsFormatSupported( - # [in] AUDCLNT_SHAREMODE ShareMode, - # [in] const WAVEFORMATEX *pFormat, - # [out,unique] WAVEFORMATEX **ppClosestMatch); - COMMETHOD([], HRESULT, 'NotImpl2'), - # HRESULT GetMixFormat( - # [out] WAVEFORMATEX **ppDeviceFormat - # ); - COMMETHOD([], HRESULT, 'GetMixFormat', - (['out'], POINTER(POINTER(WAVEFORMATEX)), 'ppDeviceFormat')), - # HRESULT GetDevicePeriod( - # [out] REFERENCE_TIME *phnsDefaultDevicePeriod, - # [out] REFERENCE_TIME *phnsMinimumDevicePeriod); - COMMETHOD([], HRESULT, 'NotImpl4'), - # HRESULT Start(void); - COMMETHOD([], HRESULT, 'Start'), - # HRESULT Stop(void); - COMMETHOD([], HRESULT, 'Stop'), - # HRESULT Reset(void); - COMMETHOD([], HRESULT, 'Reset'), - # HRESULT SetEventHandle([in] HANDLE eventHandle); - COMMETHOD([], HRESULT, 'NotImpl5'), - # HRESULT GetService( - # [in] REFIID riid, - # [out] void **ppv); - COMMETHOD([], HRESULT, 'GetService', - (['in'], POINTER(GUID), 'iid'), - (['out'], POINTER(POINTER(IUnknown)), 'ppv'))) - - -@python_2_unicode_compatible -class PROPERTYKEY(Structure): - _fields_ = [ - ('fmtid', GUID), - ('pid', DWORD), - ] - - def __str__(self): - return "%s %s" % (self.fmtid, self.pid) - - -class IPropertyStore(IUnknown): - _iid_ = GUID('{886d8eeb-8cf2-4446-8d02-cdba1dbdcf99}') - _methods_ = ( - # HRESULT GetCount([out] DWORD *cProps); - COMMETHOD([], HRESULT, 'GetCount', - (['out'], POINTER(DWORD), 'cProps')), - # HRESULT GetAt( - # [in] DWORD iProp, - # [out] PROPERTYKEY *pkey); - COMMETHOD([], HRESULT, 'GetAt', - (['in'], DWORD, 'iProp'), - (['out'], POINTER(PROPERTYKEY), 'pkey')), - # HRESULT GetValue( - # [in] REFPROPERTYKEY key, - # [out] PROPVARIANT *pv); - COMMETHOD([], HRESULT, 'GetValue', - (['in'], POINTER(PROPERTYKEY), 'key'), - (['out'], POINTER(PROPVARIANT), 'pv')), - # HRESULT SetValue([out] LPWSTR *ppstrId); - COMMETHOD([], HRESULT, 'SetValue', - (['out'], POINTER(LPWSTR), 'ppstrId')), - # HRESULT Commit(); - COMMETHOD([], HRESULT, 'Commit')) - - -class IMMDevice(IUnknown): - _iid_ = GUID('{D666063F-1587-4E43-81F1-B948E807363F}') - _methods_ = ( - # HRESULT Activate( - # [in] REFIID iid, - # [in] DWORD dwClsCtx, - # [in] PROPVARIANT *pActivationParams, - # [out] void **ppInterface); - COMMETHOD([], HRESULT, 'Activate', - (['in'], POINTER(GUID), 'iid'), - (['in'], DWORD, 'dwClsCtx'), - (['in'], POINTER(DWORD), 'pActivationParams'), - (['out'], - POINTER(POINTER(IUnknown)), 'ppInterface')), - # HRESULT OpenPropertyStore( - # [in] DWORD stgmAccess, - # [out] IPropertyStore **ppProperties); - COMMETHOD([], HRESULT, 'OpenPropertyStore', - (['in'], DWORD, 'stgmAccess'), - (['out'], - POINTER(POINTER(IPropertyStore)), 'ppProperties')), - # HRESULT GetId([out] LPWSTR *ppstrId); - COMMETHOD([], HRESULT, 'GetId', - (['out'], POINTER(LPWSTR), 'ppstrId')), - # HRESULT GetState([out] DWORD *pdwState); - COMMETHOD([], HRESULT, 'GetState', - (['out'], POINTER(DWORD), 'pdwState'))) - - -class IMMDeviceCollection(IUnknown): - _iid_ = GUID('{0BD7A1BE-7A1A-44DB-8397-CC5392387B5E}') - _methods_ = ( - # HRESULT GetCount([out] UINT *pcDevices); - COMMETHOD([], HRESULT, 'GetCount', - (['out'], POINTER(UINT), 'pcDevices')), - # HRESULT Item([in] UINT nDevice, [out] IMMDevice **ppDevice); - COMMETHOD([], HRESULT, 'Item', - (['in'], UINT, 'nDevice'), - (['out'], POINTER(POINTER(IMMDevice)), 'ppDevice'))) - - -class IMMDeviceEnumerator(IUnknown): - _iid_ = GUID('{A95664D2-9614-4F35-A746-DE8DB63617E6}') - _methods_ = ( - # HRESULT EnumAudioEndpoints( - # [in] EDataFlow dataFlow, - # [in] DWORD dwStateMask, - # [out] IMMDeviceCollection **ppDevices); - COMMETHOD([], HRESULT, 'EnumAudioEndpoints', - (['in'], DWORD, 'dataFlow'), - (['in'], DWORD, 'dwStateMask'), - (['out'], - POINTER(POINTER(IMMDeviceCollection)), 'ppDevices')), - # HRESULT GetDefaultAudioEndpoint( - # [in] EDataFlow dataFlow, - # [in] ERole role, - # [out] IMMDevice **ppDevice); - COMMETHOD([], HRESULT, 'GetDefaultAudioEndpoint', - (['in'], DWORD, 'dataFlow'), - (['in'], DWORD, 'role'), - (['out'], POINTER(POINTER(IMMDevice)), 'ppDevices')), - # HRESULT GetDevice( - # [in] LPCWSTR pwstrId, - # [out] IMMDevice **ppDevice); - COMMETHOD([], HRESULT, 'GetDevice', - (['in'], LPCWSTR, 'pwstrId'), - (['out'], - POINTER(POINTER(IMMDevice)), 'ppDevice')), - # HRESULT RegisterEndpointNotificationCallback( - # [in] IMMNotificationClient *pClient); - COMMETHOD([], HRESULT, 'NotImpl1'), - # HRESULT UnregisterEndpointNotificationCallback( - # [in] IMMNotificationClient *pClient); - COMMETHOD([], HRESULT, 'NotImpl2')) - - -@python_2_unicode_compatible -class AudioDevice(object): - """ - http://stackoverflow.com/a/20982715/185510 - """ - def __init__(self, id, state, properties): - self.id = id - self.state = state - self.properties = properties - - def __str__(self): - return "AudioDevice: %s" % (self.FriendlyName) - - @property - def FriendlyName(self): - DEVPKEY_Device_FriendlyName = \ - u"{a45c254e-df1c-4efd-8020-67d146a850e0} 14".upper() - value = self.properties.get(DEVPKEY_Device_FriendlyName) - return value - - -@python_2_unicode_compatible -class AudioSession(object): - """ - http://stackoverflow.com/a/20982715/185510 - """ - - def __init__(self, audio_session_control2): - self._ctl = audio_session_control2 - self._process = None - self._volume = None - - def __str__(self): - s = self.DisplayName - if s: - return "DisplayName: " + s - if self.Process is not None: - return "Process: " + self.Process.name() - return "Pid: %s" % (self.ProcessId) - - @property - def Process(self): - if self._process is None and self.ProcessId != 0: - try: - self._process = psutil.Process(self.ProcessId) - except psutil.NoSuchProcess: - # for some reason GetProcessId returned an non existing pid - return None - return self._process - - @property - def ProcessId(self): - return self._ctl.GetProcessId() - - @property - def Identifier(self): - s = self._ctl.GetSessionIdentifier() - return s - - @property - def InstanceIdentifier(self): - s = self._ctl.GetSessionInstanceIdentifier() - return s - - @property - def State(self): - s = self._ctl.GetState() - return s - - @property - def GroupingParam(self): - g = self._ctl.GetGroupingParam() - return g - - @GroupingParam.setter - def GroupingParam(self, value): - self._ctl.SetGroupingParam(value, IID_Empty) - - @property - def DisplayName(self): - s = self._ctl.GetDisplayName() - return s - - @DisplayName.setter - def DisplayName(self, value): - s = self._ctl.GetDisplayName() - if s != value: - self._ctl.SetDisplayName(value, IID_Empty) - - @property - def IconPath(self): - s = self._ctl.GetIconPath() - return s - - @IconPath.setter - def IconPath(self, value): - s = self._ctl.GetIconPath() - if s != value: - self._ctl.SetIconPath(value, IID_Empty) - - @property - def SimpleAudioVolume(self): - if self._volume is None: - self._volume = self._ctl.QueryInterface(ISimpleAudioVolume) - return self._volume - - -class AudioUtilities(object): - """ - http://stackoverflow.com/a/20982715/185510 - """ - @staticmethod - def GetSpeakers(): - """ - get the speakers (1st render + multimedia) device - """ - deviceEnumerator = comtypes.CoCreateInstance( - CLSID_MMDeviceEnumerator, - IMMDeviceEnumerator, - comtypes.CLSCTX_INPROC_SERVER) - speakers = deviceEnumerator.GetDefaultAudioEndpoint( - EDataFlow.eRender.value, ERole.eMultimedia.value) - return speakers - - @staticmethod - def GetAudioSessionManager(): - speakers = AudioUtilities.GetSpeakers() - if speakers is None: - return None - # win7+ only - o = speakers.Activate( - IAudioSessionManager2._iid_, comtypes.CLSCTX_ALL, None) - mgr = o.QueryInterface(IAudioSessionManager2) - return mgr - - @staticmethod - def GetAllSessions(): - audio_sessions = [] - mgr = AudioUtilities.GetAudioSessionManager() - if mgr is None: - return audio_sessions - sessionEnumerator = mgr.GetSessionEnumerator() - count = sessionEnumerator.GetCount() - for i in range(count): - ctl = sessionEnumerator.GetSession(i) - if ctl is None: - continue - ctl2 = ctl.QueryInterface(IAudioSessionControl2) - if ctl2 is not None: - audio_session = AudioSession(ctl2) - audio_sessions.append(audio_session) - return audio_sessions - - @staticmethod - def GetProcessSession(id): - for session in AudioUtilities.GetAllSessions(): - if session.ProcessId == id: - return session - # session.Dispose() - return None - - @staticmethod - def CreateDevice(dev): - if dev is None: - return None - id = dev.GetId() - state = dev.GetState() - properties = {} - store = dev.OpenPropertyStore(STGM.STGM_READ.value) - if store is not None: - propCount = store.GetCount() - for j in range(propCount): - pk = store.GetAt(j) - value = store.GetValue(pk) - v = value.GetValue() - # TODO - # PropVariantClear(byref(value)) - name = str(pk) - properties[name] = v - audioState = AudioDeviceState(state) - return AudioDevice(id, audioState, properties) - - @staticmethod - def GetAllDevices(): - devices = [] - deviceEnumerator = comtypes.CoCreateInstance( - CLSID_MMDeviceEnumerator, - IMMDeviceEnumerator, - comtypes.CLSCTX_INPROC_SERVER) - if deviceEnumerator is None: - return devices - - collection = deviceEnumerator.EnumAudioEndpoints( - EDataFlow.eAll.value, DEVICE_STATE.MASK_ALL.value) - if collection is None: - return devices - - count = collection.GetCount() - for i in range(count): - dev = collection.Item(i) - if dev is not None: - devices.append(AudioUtilities.CreateDevice(dev)) - return devices +""" +Python wrapper around the Core Audio Windows API. +""" + +# import here all newly split up modules, +# to keep backwards compatibility + +# flake8: noqa +# yes, the imports are unused + +from pycaw.api.audioclient import IAudioClient, ISimpleAudioVolume +from pycaw.api.audioclient.depend import WAVEFORMATEX +from pycaw.api.audiopolicy import ( + IAudioSessionControl, + IAudioSessionControl2, + IAudioSessionEnumerator, + IAudioSessionEvents, + IAudioSessionManager, + IAudioSessionManager2, + IAudioSessionNotification, + IAudioVolumeDuckNotification, +) +from pycaw.api.endpointvolume import ( + IAudioEndpointVolume, + IAudioEndpointVolumeCallback, + IAudioMeterInformation, +) +from pycaw.api.endpointvolume.depend import ( + AUDIO_VOLUME_NOTIFICATION_DATA, + PAUDIO_VOLUME_NOTIFICATION_DATA, +) +from pycaw.api.mmdeviceapi import ( + IMMDevice, + IMMDeviceCollection, + IMMDeviceEnumerator, + IMMEndpoint, + IMMNotificationClient, +) +from pycaw.api.mmdeviceapi.depend import IPropertyStore +from pycaw.api.mmdeviceapi.depend.structures import ( + PROPERTYKEY, + PROPVARIANT, + PROPVARIANT_UNION, +) +from pycaw.constants import ( + AUDCLNT_SHAREMODE, + DEVICE_STATE, + STGM, + AudioDeviceState, + EDataFlow, + ERole, +) +from pycaw.utils import AudioDevice, AudioSession, AudioUtilities diff --git a/addon/globalPlugins/soundmanager/pycaw/utils.py b/addon/globalPlugins/soundmanager/pycaw/utils.py new file mode 100644 index 0000000..40b7a02 --- /dev/null +++ b/addon/globalPlugins/soundmanager/pycaw/utils.py @@ -0,0 +1,305 @@ +import warnings + +import comtypes +import psutil +from _ctypes import COMError + +from pycaw.api.audioclient import IChannelAudioVolume, ISimpleAudioVolume +from pycaw.api.audiopolicy import IAudioSessionControl2, IAudioSessionManager2 +from pycaw.api.endpointvolume import IAudioEndpointVolume +from pycaw.api.mmdeviceapi import IMMDeviceEnumerator, IMMEndpoint +from pycaw.constants import ( + DEVICE_STATE, + STGM, + AudioDeviceState, + CLSID_MMDeviceEnumerator, + EDataFlow, + ERole, + IID_Empty, +) + + +class AudioDevice: + """ + https://stackoverflow.com/a/20982715/185510 + """ + + def __init__(self, id, state, properties, dev): + self.id = id + self.state = state + self.properties = properties + self._dev = dev + self._volume = None + + def __str__(self): + return "AudioDevice: %s" % (self.FriendlyName) + + @property + def FriendlyName(self): + DEVPKEY_Device_FriendlyName = ( + "{a45c254e-df1c-4efd-8020-67d146a850e0} 14".upper() + ) + value = self.properties.get(DEVPKEY_Device_FriendlyName) + return value + + @property + def EndpointVolume(self): + if self._volume is None: + iface = self._dev.Activate( + IAudioEndpointVolume._iid_, comtypes.CLSCTX_ALL, None + ) + self._volume = iface.QueryInterface(IAudioEndpointVolume) + return self._volume + + +class AudioSession: + """ + https://stackoverflow.com/a/20982715/185510 + """ + + def __init__(self, audio_session_control2): + self._ctl = audio_session_control2 + self._process = None + self._volume = None + self._channelVolume = None + self._callback = None + + def __str__(self): + s = self.DisplayName + if s: + return "DisplayName: " + s + if self.Process is not None: + return "Process: " + self.Process.name() + return "Pid: %s" % (self.ProcessId) + + @property + def Process(self): + if self._process is None and self.ProcessId != 0: + try: + self._process = psutil.Process(self.ProcessId) + except psutil.NoSuchProcess: + # for some reason GetProcessId returned an non existing pid + return None + return self._process + + @property + def ProcessId(self): + return self._ctl.GetProcessId() + + @property + def Identifier(self): + s = self._ctl.GetSessionIdentifier() + return s + + @property + def InstanceIdentifier(self): + s = self._ctl.GetSessionInstanceIdentifier() + return s + + @property + def State(self): + s = self._ctl.GetState() + return s + + @property + def GroupingParam(self): + g = self._ctl.GetGroupingParam() + return g + + @GroupingParam.setter + def GroupingParam(self, value): + self._ctl.SetGroupingParam(value, IID_Empty) + + @property + def DisplayName(self): + """ + Please, note that this returns an empty string if + the client hadn't called the setter method before. + """ + s = self._ctl.GetDisplayName() + return s + + @DisplayName.setter + def DisplayName(self, value): + s = self._ctl.GetDisplayName() + if s != value: + self._ctl.SetDisplayName(value, IID_Empty) + + @property + def IconPath(self): + """ + Please, note that this returns an empty string if + the client hadn't called the setter method before. + """ + s = self._ctl.GetIconPath() + return s + + @IconPath.setter + def IconPath(self, value): + s = self._ctl.GetIconPath() + if s != value: + self._ctl.SetIconPath(value, IID_Empty) + + @property + def SimpleAudioVolume(self): + if self._volume is None: + self._volume = self._ctl.QueryInterface(ISimpleAudioVolume) + return self._volume + + def channelAudioVolume(self): + if self._channelVolume is None: + self._channelVolume = self._ctl.QueryInterface(IChannelAudioVolume) + return self._channelVolume + + def register_notification(self, callback): + if self._callback is None: + self._callback = callback + self._ctl.RegisterAudioSessionNotification(self._callback) + + def unregister_notification(self): + if self._callback: + self._ctl.UnregisterAudioSessionNotification(self._callback) + + +class AudioUtilities: + """ + https://stackoverflow.com/a/20982715/185510 + """ + + @staticmethod + def GetSpeakers(): + """ + get the speakers (1st render + multimedia) device + """ + deviceEnumerator = comtypes.CoCreateInstance( + CLSID_MMDeviceEnumerator, IMMDeviceEnumerator, comtypes.CLSCTX_INPROC_SERVER + ) + speakers = deviceEnumerator.GetDefaultAudioEndpoint( + EDataFlow.eRender.value, ERole.eMultimedia.value + ) + return speakers + + @staticmethod + def GetMicrophone(): + """ + get the microphone (1st capture + multimedia) device + """ + deviceEnumerator = comtypes.CoCreateInstance( + CLSID_MMDeviceEnumerator, IMMDeviceEnumerator, comtypes.CLSCTX_INPROC_SERVER + ) + microphone = deviceEnumerator.GetDefaultAudioEndpoint( + EDataFlow.eCapture.value, ERole.eMultimedia.value + ) + return microphone + + @staticmethod + def GetAudioSessionManager(): + speakers = AudioUtilities.GetSpeakers() + if speakers is None: + return None + # win7+ only + o = speakers.Activate(IAudioSessionManager2._iid_, comtypes.CLSCTX_ALL, None) + mgr = o.QueryInterface(IAudioSessionManager2) + return mgr + + @staticmethod + def GetAllSessions(): + audio_sessions = [] + mgr = AudioUtilities.GetAudioSessionManager() + if mgr is None: + return audio_sessions + sessionEnumerator = mgr.GetSessionEnumerator() + count = sessionEnumerator.GetCount() + for i in range(count): + ctl = sessionEnumerator.GetSession(i) + if ctl is None: + continue + ctl2 = ctl.QueryInterface(IAudioSessionControl2) + if ctl2 is not None: + audio_session = AudioSession(ctl2) + audio_sessions.append(audio_session) + return audio_sessions + + @staticmethod + def GetProcessSession(id): + for session in AudioUtilities.GetAllSessions(): + if session.ProcessId == id: + return session + # session.Dispose() + return None + + @staticmethod + def CreateDevice(dev): + if dev is None: + return None + id = dev.GetId() + state = dev.GetState() + properties = {} + store = dev.OpenPropertyStore(STGM.STGM_READ.value) + if store is not None: + propCount = store.GetCount() + for j in range(propCount): + try: + pk = store.GetAt(j) + value = store.GetValue(pk) + v = value.GetValue() + except COMError as exc: + warnings.warn( + "COMError attempting to get property %r " + "from device %r: %r" % (j, dev, exc) + ) + continue + value.clear() + name = str(pk) + properties[name] = v + audioState = AudioDeviceState(state) + return AudioDevice(id, audioState, properties, dev) + + @staticmethod + def GetAllDevices(): + devices = [] + deviceEnumerator = comtypes.CoCreateInstance( + CLSID_MMDeviceEnumerator, IMMDeviceEnumerator, comtypes.CLSCTX_INPROC_SERVER + ) + if deviceEnumerator is None: + return devices + + collection = deviceEnumerator.EnumAudioEndpoints( + EDataFlow.eAll.value, DEVICE_STATE.MASK_ALL.value + ) + if collection is None: + return devices + + count = collection.GetCount() + for i in range(count): + dev = collection.Item(i) + if dev is not None: + devices.append(AudioUtilities.CreateDevice(dev)) + return devices + + @staticmethod + def GetDeviceEnumerator(): + """ + Get an instance of IMMDeviceEnumerator. + """ + device_enumerator = comtypes.CoCreateInstance( + CLSID_MMDeviceEnumerator, IMMDeviceEnumerator, comtypes.CLSCTX_INPROC_SERVER + ) + return device_enumerator + + @staticmethod + def GetEndpointDataFlow(devId, outputType=0): + """ + Get data flow information of a given endpoint. + Two input arguments: + - devId: id of the device + - outputType: 0 (default) for text, 1 for code. + """ + DataFlow = ["eRender", "eCapture", "eAll", "EDataFlow_enum_count"] + devEnum = AudioUtilities.GetDeviceEnumerator() + dev = devEnum.GetDevice(devId) + value = dev.QueryInterface(IMMEndpoint).GetDataFlow() + if outputType: + return value + else: + return DataFlow[value] diff --git a/buildVars.py b/buildVars.py index 149bcb3..457a82f 100755 --- a/buildVars.py +++ b/buildVars.py @@ -19,7 +19,7 @@ # Translators: Long description to be shown for this add-on on add-on information from add-ons manager "addon_description" : _("""Manages Windows and applications volumes directly within NVDA"""), # version - "addon_version" : "2023.07.26", + "addon_version" : "2024.04.26", # Author(s) "addon_author" : u"Yannick PLASSIARD , Danstiv , Beqa Gozalishvili ", # URL for the add-on documentation support @@ -27,9 +27,9 @@ # Documentation file name "addon_docFileName" : "readme.html", # Minimum NVDA version supported (e.g. "2018.3") - "addon_minimumNVDAVersion" : "2019.1", + "addon_minimumNVDAVersion" : "2024.1", # Last NVDA version supported/tested (e.g. "2018.4", ideally more recent than minimum version) - "addon_lastTestedNVDAVersion" : "2023.1", + "addon_lastTestedNVDAVersion" : "2024.4", # Add-on update channel (default is stable or None) "addon_updateChannel" : "stable" } diff --git a/soundmanager-2019.05.6.nvda-addon b/soundmanager-2024.04.26.nvda-addon similarity index 54% rename from soundmanager-2019.05.6.nvda-addon rename to soundmanager-2024.04.26.nvda-addon index 0378bc9..79cb6d9 100644 Binary files a/soundmanager-2019.05.6.nvda-addon and b/soundmanager-2024.04.26.nvda-addon differ